1、初识LockSupport
这个类是java.util.concurrent.locks包下的一个线程阻塞工具类。我们打开这个类的源码,他的注释第一句就是:
Basic thread blocking primitives for creating locks and other synchronization classes.
翻译过来:用于创建锁和其他同步类的基本线程阻塞原语。阅读了源码之后其实也不难理解这句话,其实这个类它的最主要作用是挂起和唤醒线程,而且这个工具类是创建锁和其他同步类的基础。
LockSupport这个类底层是Unsafe类来实现的,而且他的每个方法都是静态方法,方便我们直接调用。
2、LockSupport的常用方法及使用
再讲方法之前,有一个要注意的地方。我们的LockSupport类与每个使用它的的线程都会关联一个许可证,而且在默认情况下调用LockSupport类的方法的线程是没有许可证的。
2.1 void park()方法
这个方法翻译过来是停车的意思,对于我们也很好理解。线程就像一辆行驶的汽车,我们这个方法就是让线程阻塞,让这辆车停下来。
如果调用park方法的线程拿到了许可证,那么调用LockSupport.park()方法就会马上返回,否则就会被禁止参与线程调度,也就是我们所说的阻塞。其实用起来十分简单,这里我举了一个简单的例子:
public class LockSupportDemo {
public static void main(String[] args) {
System.out.println("main thread begin");
LockSupport.park();
System.out.println("main thread end");
}
}
如上代码,如果直接在main线程里面调用park方法,最终只会输main thread begin,然后main线程被挂起,因为在默认情况下,调用的线程是没有许可证的,所以main线程会一直阻塞。这个时候如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,则main线程也会返回(这里是不会抛出InterruptedException的),所以在调用park方法时,最好也使用循环条件判断。
那么我们要如何唤醒呢?
2.2 void unpark(Thread thread)方法
其实这个方法也很简单,你可以把它理解成为给它的参数,thread线程发放许可证。当一个线程调用unpark的时候,如果参数thread线程没有持有许可证,这个时候就会让thread线程持有。如果这个线程之前因为调用了park()方法而阻塞了,那么调用了unpark()就会立即被唤醒。如果thread之前没有调用park,那么调用unpark后,在调用park也会立即唤醒。
我们把上面的代码稍作修改,你就会明白了:
public class LockSupportDemo {
public static void main(String[] args) {
System.out.println("main thread begin");
LockSupport.unpark(Thread.currentThread());
LockSupport.park();
System.out.println("main thread end");
}
}
我把unpark放在了前面,这样做就会直接输出这两句话,因为我们在park方法之前,给main线程发放了我们的许可证。下面我们再来看一个例子,加深对park和unpark的理解。
public class UnparkDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程开始了");
LockSupport.park();
System.out.println("子线程结束了");
}
});
//启动子线程
thread.start();
//主线程休眠1s
Thread.sleep(1000);
System.out.println("mian线程开始调用unpark");
//给thread线程发放许可证
LockSupport.unpark(thread);
}
}
如上代码:我们首先创建了一个子线程thread,子线程启动后调用park方法,由于在默认情况下子线程是没有许可证的,所以他会把自己挂起。主线程休眠1s,是为了有充分的时间,让子线程输出"子线程开始了"。主线程执行unpark方法,参数为子线程,这样会让子线程拥有许可证,然后子线程就不会被阻塞了。
我们再来看一个例子:
public class UnparkDemo2 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程开始了");
//调用park方法挂起自己,只有线程被中断才会退出循环
while (! Thread.currentThread().isInterrupted()){
LockSupport.park();
}
System.out.println("子线程结束了");
}
});
//启动子线程
thread.start();
//主线程休眠1s
Thread.sleep(1000);
System.out.println("mian线程开始调用interrupt()");
//中断thread线程
thread.interrupt();
}
}
如上代码。只有中断了子线程,子线程的运行才会结束,如果子线程没有中断,即使你调用了unpark,子线程也不会结束。我们也可以使用像如上代码一样的方法,来对比线程状态。
2.3 void parkNanos(long nanos)方法
这个方法其实和park方法类似,如果调用该方法的线程已经拿到了许可证,那么这个方法就会被立即返回,如果没有拿到许可证,那么调用的线程就会等待nanos时间后,修改为自动返回。使用起来也和park一样,十分简单。
2.4 void park(Object blocker)方法
带有blocker参数的park方法,当线程在没有许可证的情况下,调用park方法而被阻塞挂起,那么这个blocker就会被记录到该线程内部。使用诊断工具我们可以查看线程被阻塞的原因,诊断工具可以通过getBlocker(Thread thread)方法来获取blocker对象,所以JDK推荐我们使用带blocker的park方法,并且blocker被设置为this,这样当在打印线程堆栈排除问题时,就能知道是哪个类被阻塞了。
举个例子,如下代码:
public class ParkDemo {
public void testPark(){
LockSupport.park();
}
public static void main(String[] args) {
ParkDemo parkDemo = new ParkDemo();
parkDemo.testPark();
}
}
运行代码后,我们先不着急把它关闭。在cmd里面输入
wmic process where caption=“java.exe” get processid,caption,commandline /value
这个命令,然后找到你当前运行的mian方法的那个进程id
然后使用jstack pid(这里的pid是你刚才查出来的id),然后你就会发现如下代码:
修改上面的java代码为:
public class ParkDemo {
public void testPark(){
LockSupport.park(this);
}
public static void main(String[] args) {
ParkDemo parkDemo = new ParkDemo();
parkDemo.testPark();
}
}
在执行如上步骤,得到的结果:
我们看到,我们使用带blocker参数的park方法,我们的堆栈会给我们更多的有关与阻塞对象的信息。
2.5 void parkNanos(Object blocker,long nanos)方法
这个方法相比我们的park(Object blocker)多了一个超时的时间而已,用法大致相同,这里不做过多的赘述。
2.6 void parkUntil(Object blocker , long deadline)方法
这个方法我们来稍微看一下源码:
public static void parkUntil(Object blocker, long deadline) {
//先获取当前线程
Thread t = Thread.currentThread();
//给当前线程设置blocker
setBlocker(t, blocker);
//等待到deadline这个时间,然后自动返回
UNSAFE.park(true, deadline);
//清除blocker
setBlocker(t, null);
}
上述代码的deadline是一个时间点,long类型,到达了这个时间点,该方法就会自动返回。
3、简单例子
到这里我们常用的方法就讲的差不多了,我们来看一个小小的例子
public class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean(false);
private final Queue<Thread> waiters = new ConcurrentLinkedDeque<Thread>();
public void lock(){
boolean wasInterrupted = false;
Thread current = Thread.currentThread();
waiters.add(current);
//判断当前线程是否在队首(1)
while(waiters.peek() != current || !locked.compareAndSet(false,true)){
LockSupport.park(this);
//(2)
if(Thread.interrupted()){
wasInterrupted = true;
}
}
waiters.remove();
//(3)
if(wasInterrupted){
current.interrupt();
}
}
public void unlock(){
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
其实上面的代码是一个先进先出的锁,也就是只有队列的首元素可以获取锁。在代码(1)处,如果当前线程不是队首或者当前锁已经被其他线程获取了,那么就调用park方法挂起自己。
然后在代码(2)处判断,如果park方法是因为被中断而返回的,则忽略中断,并且重置中断标记,做个标记,然后再次判断当前线程是不是队首,或者当前锁是否已经被其他线程获取,如果是则继续调用park方法挂起自己。
然后在代码(3)中判断一下标记,如果标记为true则中断该线程,这个怎么理解呢?其实就是其他线程中断了该线程,虽然我对中断信号并不感兴趣,忽略他,但是不代表其他线程对该标志不感兴趣,所以要恢复一下