JAVA高并发—LockSupport的学习及简单使用
1、简单介绍
LockSupport是JDK中比较底层的类,用来创建锁和其他同步工具类的基本线程阻塞原语。可以做到与join()
、wait()/notifyAll()
功能一样,使线程自由的阻塞、释放。
Java锁和同步器框架的核心AQS(AbstractQueuedSynchronizer 抽象队列同步器)
,就是通过调用LockSupport.park()
和LockSupport.unpark()
实现线程的阻塞和唤醒的。
补充:AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,
如常用的ReentrantLock/Semaphore/CountDownLatch...。
2、简单原理
LockSupport
方法底层都是调用Unsafe
的方法实现。全名sun.misc.Unsafe,该类可以直接操控内存,被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是不建议在生产环境中使用这个类。因为这个API十分不安全、不轻便、而且不稳定。
LockSupport提供park()
和unpark()
方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit是相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就会将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。意思就是说unpark
之后,如果permit
已经变为1,之后,再执行unpark
,permit
依旧是1。下边有例子会说到。
3、简单例子
以下边的做饭
例子,正常来说,做饭
之前,要有锅
、有菜
才能开始做饭
。具体如下:
(1)先假设已经有了锅
,那只需要买菜
就可以做饭
。如下,即注释掉了买锅的步骤:
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//买锅
// Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
// t1.start();
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
// LockSupport.park();
// System.out.println("锅买回来了...");
LockSupport.park();
System.out.println("菜买回来 了...");
System.out.println("开始做饭");
}
}
class BuyGuo implements Runnable{
private Object threadObj;
public BuyGuo(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("去买锅...");
LockSupport.unpark((Thread)threadObj);
}
}
class BuyCai implements Runnable{
private Object threadObj;
public BuyCai(Object threadObj) {
this.threadObj = threadObj;
}
@Override
public void run() {
System.out.println("买菜去...");
LockSupport.unpark((Thread)threadObj);
}
}
执行后,可出现下面的结果:
买菜去...
菜买回来了...
开始做饭
如上所述,可以达到阻塞主线程等到买完菜之后才开始做饭。这即是park()
、unpark()
的用法。简单解释一下上述的步骤:
main
方法启动后,主线程
和买菜线程
同时开始执行。- 因为两者同时进行,当
主线程
走到park()
时,发现permit
还为0
,即会等待在这里。 - 当
买菜线程
执行进去后,走到unpark()
会将permit
变为1
。 主线程
park()
处发现permit
已经变成1
,就可以继续往下执行了,同时消费掉permit
,重新变成0
。
以上permit
只是park/unpark
执行的一种逻辑开关,执行的步骤大致如此。
4、注意点及思考
(1)必须将park()
与uppark()
配对使用才更高效。
如果上边也把买锅
的线程放开,main
方法改为如下:
//买锅
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("锅买回来了...");
LockSupport.park();
System.out.println("菜买回来了...");
System.out.println("开始做饭");
即调用了两次park()
和unpark()
,发现有时候可以,有时候会使线程卡在那里,然后我又换了下顺序,如下:
//买锅
Thread t1 = new Thread(new BuyGuo(Thread.currentThread()));
t1.start();
LockSupport.park();
System.out.println("锅买回来了...");
//买菜
Thread t2 = new Thread(new BuyCai(Thread.currentThread()));
t2.start();
LockSupport.park();
System.out.println("菜买回来了...");
System.out.println("开始做饭");
原理没有详细去研究,不过想了想,上边两种其实并无区别,只是执行顺序有了影响,park()
和unpark()
既然是成对配合使用,通过标识permit
来控制,如果像前边那个例子那样,出现阻塞的情况原因,我分析可能是这么个原因:
当买锅的时候,通过unpark()
将permit
置为1,但是还没等到外边的main方法执行第一个park()
,买菜的线程又调了一次unpark()
,但是这时候permit
还是从1变成了1,等回到主线程调用park()的时候,因为还有两个park()
需要执行,也就是需要两个消费permit
,因为permit
只有1个,所以,可能会剩下一个park()
卡在那里了。
(2)使用park(Object blocker)
方法更能明确问题
其实park()
有个重载方法park(Object blocker)
,这俩方法效果差不多,但是有blocker的可以传递给开发人员更多的现场信息,可以查看到当前线程的阻塞对象,方便定位问题。所以java6新增加带blocker入参的系列park方法,替代原有的park方法。
5、与wait()/notifyAll()
的比较
LockSupport
的 park/unpark
方法,虽然与平时Object
中wait/notify
同样达到阻塞线程的效果。但是它们之间还是有区别的。
- 面向的对象主体不同。
LockSupport()
操作的是线程对象,直接传入的就是Thread
,而wait()
属于具体对象,notifyAll()
也是针对所有线程进行唤醒。 wait/notify
需要获取对象的监视器,即synchronized
修饰,而park/unpark
不需要获取对象的监视器。- 实现的机制不同,因此两者没有交集。也就是说
LockSupport
阻塞的线程,notify/notifyAll
没法唤醒。但是park
之后,同样可以被中断(interrupt()) !