前面学习了很多Java并发编程的知识,很多知识点都离不开锁的支持,从这期开始,主要讲解一些Java并发包中的锁的原理,让我们来一起揭开它的神秘面纱吧。
LockSupport工具类
LockSupport是JDK中rt.jar包里的工具类,其主要作用是挂起和唤醒线程,它也是创建锁和其他同步类的基础。
LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的
LockSupport类是使用Unsafe类实现的
下面介绍LockSupport类的几个主要方法。
1、void park()
方法
- 若调用park方法的线程已经拿到了LockSupport关联的许可证,则调用
LockSupport.park()
时会马上返回 - 否则调用线程会被禁止参与线程的调度,也就是会被阻塞挂起。
如下代码:
public class LockSupportTest { public static void main(String[] args) { System.out.println("开始park"); LockSupport.park(); System.out.println("结束park"); } }
结果只会输出:开始park ,因为当前线程被挂起了,也就证明了上面所说的:默认情况下调用线程是不持有LockSupport的许可证的。
在其他线程调用void unpark(Thread thread)方法并将当前的线程作为参数时,调用park方法被阻塞的线程会返回。
如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,调用park方法被阻塞的线程也会返回。
注意:调用park方法而被阻塞的线程被其他线程中断而返回时,并不会抛出InterruptedException异常。
2、void unpark(Thread thread)
方法
当一个线程调用void unpark(Thread thread)
方法时:
如果作为参数的thread线程没有持有thread与LockSupport类关联的许可证,则让thread持有。
如果thread之前因为调用park()而被挂起,则调用了upark后,thread会被唤醒。
如果thread之前没有调用park(),则调用upark后,再次调用park()方法,会立即返回。
修改上述代码如下:
public class LockSupportTest { public static void main(String[] args) { System.out.println("开始park"); //使当前线程获取到许可证 LockSupport.unpark(Thread.currentThread()); //调用park方法 LockSupport.park(); System.out.println("结束park"); } }
输出结果为:
开始park 结束park
下面再看一个例子加深park和unpark的理解:
public class LockSupportTest { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { System.out.println("子线程开始park"); LockSupport.park();//调用park,挂起自己 System.out.println("子线程结束park"); }); //启动子线程 thread.start(); //主线程休眠1s Thread.sleep(1000); System.out.println("主线程开始unPark"); //调用unpark方法,让thread线程持有许可证,然后子线程的park方法返回 LockSupport.unpark(thread); } }
输出结果为:
子线程开始park 主线程开始unPark 子线程结束park
park方法返回时不会告诉调用者因何种原因返回,所以调用者需要根据之前调用park方法的原因,再次检查条件是否满足,如果不满足则还需要再次调用park方法。
例如,根据调用前后中断状态的对比就可以判断是不是因为被中断才返回的。为了说明调用park方法后的线程被中断后返回,我们修改上述代码,删除LockSupport.unpark(thread),然后添加thread.interrupt():
public class LockSupportTest { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { System.out.println("子线程开始park"); //只有被中断时退出循环 while (!Thread.currentThread().isInterrupted()){ LockSupport.park();//调用park,挂起自己 } System.out.println("子线程结束park"); }); //启动子线程 thread.start(); //主线程休眠1s Thread.sleep(1000); thread.interrupt(); } }
输出结果:
子线程开始park 子线程结束park
如上代码中,只有中断了子线程,子线程才会运行结束;如果子线程不被中断,即使是调用了upark(thread)
方法也无济于事。
3、void parkNanos(long nanos)
方法
与park()
方法不同的是,如果调用void parkNanos(long nanos)
方法的线程没有获取到许可证,则调用线程会被挂起nanos
时间后自动返回。
4、void park(Object blocker)
方法
当线程在没有持有许可证的情况下调用park
方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。
public static void park(Object blocker) { //获取调用线程 Thread t = Thread.currentThread(); //设置该线程的blocker变量 setBlocker(t, blocker); //挂起线程 UNSAFE.park(false, 0L); //线程被激活后清除blocker变量,因为一般都是在线程阻塞时才分析原因 setBlocker(t, null); }
Thread类里面有个变量volatile Object parkBlocker,里面用来存放调用park(Object blocker)的blocker对象。
5、void parkNanos(Object blocker, long nanos)
方法
比void park(Object blocker)方法多了个nanos超时时间。
6、void parkUntil(Object blocker, long deadline)
方法
此方法与void parkNanos(Object blocker, long nanos)方法的区别在于,parkUntil是到达某一个时间点(把这个时间点转换成从1970年到这个时间点的总毫秒数作为参数deadline)后返回;而parkNanos则是经过nanos的时间后返回。
7、先进先出锁的实现例子
这是一个先进先出锁的实现,也就是只有队列的首元素可以获取锁:
import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.LockSupport; public class FIFOMutex { private final AtomicBoolean locked = new AtomicBoolean(false); private final Queue<Thread> waiters = new ConcurrentLinkedDeque<>(); public void lock() { boolean wasInterrupted = false; Thread currentThread = Thread.currentThread(); waiters.offer(currentThread); //(1) while (waiters.peek() != currentThread || !locked.compareAndSet(false, true)) { LockSupport.park(this); //(2) if (Thread.interrupted()) { wasInterrupted = true; } } waiters.poll(); //(3) if (wasInterrupted) { currentThread.interrupt(); } } public void unlock() { locked.set(false); LockSupport.unpark(waiters.peek()); } }
代码(1),如果当前线程不是队头或者当前锁已经被其他线程获取,则调用park方法挂起自己。
代码(2),如果park方法是因为被中断而返回,则忽略中断,并重置中断标志,做个标记,然后再次判断当前线程是不是队头或者当前锁已经被其他线程获取,如果是则继续调用park方法挂起自己。
代码(3),判断标记,如果标记为true则中断该线程。
总结
本期主要学习了LockSupport的知识点以及其常用的park
和unpark
的方法,另外通过一个先进先出锁的例子来加深了对前面知识的理解。LockSupport类是创建锁和其他同步类的基础,了解了它会对后续的学习更有帮助。下期预告:抽象同步队列AQS。