简述JAVA线程阻塞原理-重制

650 阅读4分钟

记住这张图

image.png

两种阻塞模型

  • 争夺型阻塞 争夺型阻塞是多个线程争夺锁,抢到锁的线程执行同步代码,没抢到锁的线程进入等待队列,常见的是synchronized和juc里面的拓展类。
  • 等待型阻塞 等待型阻塞是一个线程主动阻塞,等待另一个线程执行完毕讲起唤醒,常见有Thread.join(), future.get(), Object.wait(), LockSupport.park()等。

下面细说这两种阻塞模型。

争夺型阻塞

synchronized

还记得synchronized的用法吗?可以锁方法,可以锁对象,可以锁类.class,还可以锁this;四种锁法大同小异,都需要一个对象,而这个对象可以是任意的,为什么可以任意?因为每个对象内部都有个Monitor对象。synchrond需要的是这个Monitor对象。

monitor

每一个对象都有一把看不见的锁,称为内部锁或者Monitor

image.png

  • Entry Set,blocked状态的线程,等待抢锁
  • The Owner,获得锁的线程
  • Wait Set,wait的线程,等待notify()/notifyAll()
  1. 争夺锁成功的线程赋值给The Owner,用于后期重入。
  2. 争夺锁失败的线程进入Entry Set队列,进入阻塞状态。
  3. 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");

截屏2023-01-29 09.46.03.png 我们从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说的。 截屏2023-01-29 10.04.17.png

future/futrueTask

image.png 仔细看图片,一个代表持有锁的线程的runner变量,另一个用于保存线程的阻塞队列。是不是跟Monitor很像,不能说很像,可以说是照着模子刻出来的,而这里的futrue就是上面说的monitor。
当你在使用future的时候,runner线程在跑任务,当调用future.get()的时候,如果任务未完成,那当前线程就进入等待队列阻塞,当任务完成之后,会唤醒阻塞线程,也就是调用future.get()的线程。

总结

无论是争夺型阻塞还是等待型阻塞,都是同个套路,一个代表当前线程的变量,另一个存放阻塞线程的等待队列。