记住这张图
两种阻塞模型
- 争夺型阻塞 争夺型阻塞是多个线程争夺锁,抢到锁的线程执行同步代码,没抢到锁的线程进入等待队列,常见的是synchronized和juc里面的拓展类。
- 等待型阻塞 等待型阻塞是一个线程主动阻塞,等待另一个线程执行完毕讲起唤醒,常见有Thread.join(), future.get(), Object.wait(), LockSupport.park()等。
下面细说这两种阻塞模型。
争夺型阻塞
synchronized
还记得synchronized的用法吗?可以锁方法,可以锁对象,可以锁类.class,还可以锁this;四种锁法大同小异,都需要一个对象,而这个对象可以是任意的,为什么可以任意?因为每个对象内部都有个Monitor对象。synchrond需要的是这个Monitor对象。
monitor
每一个对象都有一把看不见的锁,称为内部锁或者
Monitor锁
- Entry Set,blocked状态的线程,等待抢锁
- The Owner,获得锁的线程
- Wait Set,wait的线程,等待notify()/notifyAll()
- 争夺锁成功的线程赋值给The Owner,用于后期重入。
- 争夺锁失败的线程进入Entry Set队列,进入阻塞状态。
- The Owner执行完毕遍历Wait Set队列唤醒线程,被唤醒的线程进入Entry Set一起争夺锁。
为什么需要Wait Set队列,如果没有Wait Set,阻塞的线程放在哪。而Wait Set又是Monitor对象里面的,所以你知道为什么不同对象锁不一样了吧,因为排队阻塞的线程都不一样。
JUC-AQS
如果你理解了synchronized,你一定知道AQS里面必然有个阻塞队列,也必然有个标识持有锁线程的变量。如果你能想到,那你就知道aqs和synchronized的套路是一样的,你需要做的就只剩下区分使用场景。
事实上JUC的拓展类基本都继承AQS,AQS内部有个exclusiveOwnerThread存储持有锁的线程,CLH队列存储没抢到锁的线程。
Aqs和synchronized使用场景差异
synchronized是关键字,jdk1.6优化性能已经很强了,能锁升级,不需要手动解锁,异常情况也能解锁。
aqs属于api级别,支持拓展,支持公平锁/非公平锁,其麾下子孙种类丰富,使用上比synchronized灵活。
两者都可以用的场景,推荐用synchronized,因为它是关键字,性能上比lock强。
等待型阻塞
回忆一下学习线程的时候经常看到的代码,忍不住思考为什么main要等待t1执行完毕时,要在main线程调用t1.join()方法?
t1.start();
t1.join();
System.out.println("main thread");
我们从join方法进去会发现,join会调用Object.wait()方法,从方法红色下划线注释可以看到一句话,把当前线程放到wait set。看到这里是不是领悟了,这不就是synchronzied吗?t1即是锁,main调用t1.join()相当于main要争夺t1。但因为对于t1这把锁,The Owner是t1本身,所以main需要进入Wait Set等待t1执行完毕将其唤醒。瞬间醒悟,这居然用的也是monitor锁!!!
wait/notify/notifyAll为什么需要synchronized修饰
- wait将线程加入minitor的wait队列,并且会释放锁,所以需要synchronized。
- notify/notifyAll会唤醒minitor的wait队列的线程,加锁的目的是防止唤醒的过程中,打断wait操作。
那wait和sleep呢
看到这里你可能会问,那wait是怎么阻塞的,其实join就是调用的wait阻塞的。而sleep不一样,还记得sleep的用法吗?
t1.start();
try {Thread.sleep(1000);} catch(InterruptedException e) {}
System.out.println("main thread");
看到这里你是否会疑问,为什么sleep是Thread.sleep(),还不是t1.sleep()?我们看看源码注释怎么说的,sleep使得当前线程sleep,但又不丢掉monitor,老八股了,wait丢锁sleep不丢锁,可不是我们瞎说,是jdk说的。
future/futrueTask
仔细看图片,一个代表持有锁的线程的runner变量,另一个用于保存线程的阻塞队列。是不是跟Monitor很像,不能说很像,可以说是照着模子刻出来的,而这里的futrue就是上面说的monitor。
当你在使用future的时候,runner线程在跑任务,当调用future.get()的时候,如果任务未完成,那当前线程就进入等待队列阻塞,当任务完成之后,会唤醒阻塞线程,也就是调用future.get()的线程。
总结
无论是争夺型阻塞还是等待型阻塞,都是同个套路,一个代表当前线程的变量,另一个存放阻塞线程的等待队列。