前言
在java并发包下各种同步组件的底层实现中,LockSupport的身影处处可见。JDK中的定义为用来创建锁和其他同步类的线程阻塞原语。
我们可以使用它来阻塞和唤醒线程,功能和wait,notify有些相似,但是LockSupport比起wait,notify功能更强大,也好用的多。
本文主要介绍使用wait,notify阻塞唤醒线程和使用LockSupport阻塞唤醒线程
一、使用wait,notify
阻塞唤醒线程
public class Test2 {
private static final Object obj = new Object();
public static void waitThread(){
synchronized (obj) {
System.out.println("start wait!");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end wait!");
}
}
public static void notifyThread(){
synchronized (obj) {
System.out.println("start notify!");
obj.notifyAll();
System.out.println("end notify");
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(()->waitThread()).start();
Thread.sleep(1000);
new Thread(()->notifyThread()).start();
}
}
//start wait!
//start notify!
//end notify
//end wait!
使用wait,notify来实现等待唤醒功能至少有两个缺点:
1.wait和notify都是Object中的方法,在调用这两个方法前必须先获得锁对象,这限制了其使用场合:只能在同步代码块中。
2.另一个缺点可能上面的例子不太明显,当对象的等待队列中有多个线程时,notifyAll无法唤醒指定的线程。
而使用LockSupport的话,我们可以在任何场合使线程阻塞,同时也可以指定要唤醒的线程,相当的方便。
二、使用LockSupport阻塞唤醒线程
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。
主要有两类方法:park
和unpark
。
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
我们写一个例子来看看这个工具类怎么用的。
public class Test2 {
public static void main(String[] args) {
Thread thread = new Thread(()->parkThread());
thread.start();
System.out.println("开始线程唤醒");
LockSupport.unpark(thread);
System.out.println("结束线程唤醒");
}
public static void parkThread(){
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
//开始线程阻塞
//开始线程唤醒
//结束线程唤醒
//结束线程阻塞
LockSupport.park();可以用来阻塞当前线程,park是停车的意思,把运行的线程比作行驶的车辆,线程阻塞则相当于汽车停车,相当直观。该方法还有个变体LockSupport.park(Object blocker),指定线程阻塞的对象blocker,该对象主要用来排查问题。方法LockSupport.unpark(Thread thread)用来唤醒线程,因为需要线程作参数,所以可以指定线程进行唤醒。
1、可以先唤醒线程再阻塞线程
在阻塞线程前睡眠1秒中,使唤醒动作先于阻塞发生,看看会发生什么
public class Test2 {
public static void main(String[] args) {
Thread parkThread = new Thread(()->parkThread());
parkThread.start();
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");
}
public static void parkThread (){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
//开始线程唤醒
//结束线程唤醒
//开始线程阻塞
//结束线程阻塞
先唤醒指定线程,然后阻塞该线程,但是线程并没有真正被阻塞而是正常执行完后退出了。这是怎么回事?我们试着在改动下代码,先唤醒线程两次,在阻塞线程两次,看看会发生什么。
2、先唤醒线程两次再阻塞两次会发生什么
public class Test2 {
public static void main(String[] args) {
Thread parkThread = new Thread(()->parkThread());
parkThread.start();
for(int i=0;i<2;i++){
System.out.println("开始线程唤醒");
LockSupport.unpark(parkThread);
System.out.println("结束线程唤醒");
}
}
public static void parkThread (){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
System.out.println("开始线程阻塞");
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
}
//开始线程唤醒
//结束线程唤醒
//开始线程唤醒
//结束线程唤醒
//开始线程阻塞
//结束线程阻塞
//开始线程阻塞
可以看到线程被阻塞导致程序一直无法结束掉。对比上面的例子,我们可以得出一个匪夷所思的结论,先唤醒线程,在阻塞线程,线程不会真的阻塞;但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。那么这到底是为什么?
3、 LockSupport阻塞和唤醒线程原理浅析
既然是浅析,那就不抠底层细节,只讲关键,细节可能有疏漏和不到位的地方。
每个线程都有Parker实例,如下面的代码所示
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次会阻塞线程。
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。
4. 总结
LockSupport是JDK中用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。