Java线程的几种状态 以及synchronized和Lock造成的线程状态差异,一篇让你搞明白
RUNNABLE 包含就绪(Ready)和运行中(Running),Java是把这2个整体称为RUNNABLE状态。synchronized拿不到锁,导致的线程状态是blocked状态。而ReentrantLock这种lock,拿不到锁而导致的线程状态是waiting状态,不是blocked。blocked是当持有锁的线程释放了锁时,JVM会从锁对象监视器的同步队列里唤醒一个blocked状态的线
关于作者:
一个深耕自己,不内耗的长期主义者。一个对技术充满激情,对工作对生活充满热情的热血青年。坚持用通俗易懂的大白话写技术博文,并会持续更新。
和之前一样,今天还是用通俗易懂的大白话来写点我自己的理解和总结,今天讲一下线程的几种状态之间的转换以及synchronized和Lock造成的线程状态差异。看完如果有什么疑问的地方,可以留言讨论,也可以私信。我坚信,真正能让大家看懂的技术文章才是好文章,这也是我最初决定写文章最主要的目标和动力,希望这篇文章对你有所帮助。
一、线程的几种状态
大白话描述一下线程几种状态:
1、New 新建状态:此时线程刚被new出来,还没执行start()方法。
2、Runnable 状态:具体又分为Ready就绪和Running运行。Ready就绪状态是线程已经准备好了,就差CPU轮转的时间片了。获得CPU时间片被调度后,就会从Ready变成Running运行。
3、Blocked 阻塞状态:Blocked这种状态是线程竞争资源失败而被挂起阻塞的,通常是竞争synchronized锁失败后被阻塞。Blocked状态的线程会被放入jvm内部维护的一个同步队列(Entry List)里,进入Blocked状态后,是在等待一个不可避免的竞争资源(比如锁)。当持有锁的线程释放了锁时,JVM会让锁对象监视器的同步队列里所有的线程都去尝试抢锁,最终只会有一个线程抢锁成功,状态从blocked变为runnable。这就是经常说的持有锁的线程释放锁后,会从同步队列里唤醒一个线程,其他抢锁失败的,状态都不变还是blocked。虽然blocked状态的线程只有持有锁的线程释放了锁后才有机会和同步队列里的其他线程去抢锁,而且可能抢成功也有可能抢失败。但是总体来说blocked状态的线程,还是有机会拿到CPU执行权的。
4、Waiting 等待状态,是持有锁的线程主动调用object.wait()或者调用LockSupport.park()而导致的waiting。waiting是只要没有notify、notifyAll或LockSupport.unpark() 去主动唤醒,那它就一直是waiting状态。waiting期间,CPU调度的时候会直接忽略这些waiting的线程。和blocked状态相比,waiting状态的线程在被唤醒之前,它们是连机会都没有,是不可能拿到CPU执行权的。
5、Timed_waiting 限时等待状态,是调用object.wait(1000)、Thread.sleep(1000)这种方法传入一个时间,时间到了以后线程自己会醒。它也是在唤醒之前直接放弃竞争cpu资源的资格,cpu轮转切换执行权的时候连考虑都不会考虑它的。
6、Terminal 终止消亡状态:表示该线程已经执行完毕。
有的书或文章又将 waiting和time_waiting统称为冻结状态。
了解过线程的几种状态后,下面再来说一下几种状态之间的转换
二、线程几种状态之间的转换
下面是根据我自己的总结画的图,图中已经非常全面的描述了Java线程各种状态,以及各种状态之间的切换流转。

三、关键点总结
结合上图需要知道的一些关键的点:
1、首先需要知道JVM内部维护了两个队列,jvm底层c++写的:
一个是同步队列(Entry List):用来存储抢synchronized锁失败而被blocked的线程。
一个是等待队列(Wait Set):用来存储抢synchronized锁成功后,又主动调用object.wait(),而被waiting的线程。
jvm里的这两个队列,由于是jvm内部底层用c++写的,所以看不了源码。
2、还需要知道AQS内部也维护了两个队列,Java层面代码写的:
一个是同步队列:用来存储抢AQS锁失败而被waiting的线程,这个同步队列是AQS内部的Node类实现的双链表结构。
一个是条件等待队列:用来存储抢AQS锁成功后,又主动调用condition.await(),而被waiting的线程,这个条件队列是AQS内部的ConditionObject和ConditionNode类构成的单链表结构。
AQS里的两个队列,是java写的,有时间了可以看下里边具体的源码。
3、AQS阻塞和唤醒线程最后是通过调用LockSupport.park()和LockSupport.unpark(),来实现的。
4、synchronized拿不到锁,导致的线程状态是blocked状态,并且会把当前线程放入jvm内部维护的同步队列中(Entry List)。
5、而AQS、ReentrantLock这种lock,拿不到锁导致的线程状态是waiting,不是blocked。因为看AQS和ReentrantLock的源码,调用acquire、tryAcquire尝试获取锁,获取失败后会调用addWaiter把当前线程封装成一个Node节点放入AQS内部维护的同步队列中。然后调用acquireQueue方法,通过LockSupport.park()让自己进入waiting。
根据上图再总结一下线程进入blocked和waiting的几种途径:
线程进入blocked的途径:
第一种:抢synchronized锁没抢到导致的blocked。synchronized导致的抢不到锁的这些线程状态会变成blocked,并且会被包装成一个节点,放入jvm内部维护的同步队列中。
第二种:调用notify、notifyAll方法后从jvm等待队列(Wait Set)里转移到同步队列(Entry List)里的线程,状态会从waiting变为blocked。具体情况是:抢synchronized锁成功的线程,又调用了object.wait() 方法,主动放弃CPU的执行权,这种情况线程状态会变成waiting,并且会被放入另外一个等待队列中。然后等另一个线程调用了notify()后,会从等待队列中随机选一个线程转移到同步队列里,转移过来的这个线程的状态会从waiting变成blocked。
第三种:object.wait(1000),Timed_waiting时间到了之后的线程状态从Timed_waiting变成blocked。
线程进入waiting状态的途径:
第一种:抢synchronized锁成功的线程,又调用了object.wait() 方法,主动放弃CPU的执行权,这种情况线程状态会变成waiting,并且会被放入jvm内部维护的等待队列中。
第二种:通过调用LockSupport.park(),使线程进入waiting。这种具体又可以分两种情况:
1、没抢到AQS,ReentrantLock这种锁最后通过调用LockSupport.park()让线程进入waiting,并且会把线程放入AQS的同步队列中。
2、抢到ReentrantLock这种锁了,但是又主动调用了condition.await()方法,await()方法最后也是调用LockSupport.park()让线程进入waiting,并且会把当前的线程放入AQS的condition条件队列中。
四、理清楚线程最常用的几个方法
最后再来说一下线程中常用的几个方法,wait()、notify()、notifyAll()、sleep()、yield()、join() 这几个方法,估计很多人搞不清楚是什么情况去调用,也弄不清楚到底是锁对象的方法,还是线程thread对象的方法,还是线程Thread类的静态方法,下面就来总结说一下,一次搞清楚。
1、wait()、notify()、notifyAll()
obj.wait()
obj.notify()
obj.notifyAll()
这3个方法都是synchronize锁对象上的方法,调用这3个方法的前提是必须已经获取了锁资源,没有获得锁的情况下去直接调用,会抛异常。上边例子中obj就是synchronize的那个锁对象。
obj.wait()导致线程进入waiting状态
obj.notify() 是唤醒一个该锁监视器上挂起的线程,我个人理解的,上边那个图里有个地方画的不是太对,其实调用了notify()方法后,线程并不是直接进入runnable状态的,而是会先变成blocked状态,等到之前持有锁的线程释放锁后,它再和其他所有blocked状态的线程去尝试抢锁,抢成功了就变成runnable,失败了状态不变还是blocked。
obj.notifyAll() 是唤醒所有挂在该锁监视器上的线程,一般不要用,可能会引起惊群效应,就是你调用obj.notifyAll() 把这个锁上所有的线程都从waiting变成blocked,然后持有锁的线程把锁释放后,这些线程会一窝蜂的都去尝试获取锁资源,但最后只能有一个线程获得成功状态变为RUNNABLE,剩下其他线程都没获得成功状态还得是blocked。
注意:就算进入RUNNABLE了,也并不是直接就拿到cpu去执行了,而是需要等待cpu时间片,拿到cpu时间片后才进入最后的执行阶段。
2、sleep()、yield()
sleep()、yield()这2个方法是线程类Thread类里的静态方法
Thread.sleep(1000) 在哪个线程里执行的,就睡眠哪个线程
Thread.yield() 在哪个线程里执行的,就让出哪个线程的执行权,但不保证效果
3、join()
join()方法是 thread线程对象调用的方法
比如,thread6.join() 阻塞线程,是这句话在哪个线程里执行,就阻塞哪个线程。比如在主线程里执行thread6.join();那主线程就会暂时阻塞,让thread6这个线程先执行,直到thread6执行完成,主线程才会继续执行。从join这个方法名也能大概看出来,就是让thread6这个线程先加入执行,让执行这句话的那个线程等一等。
ok,今天就写到这里吧。我坚信,真正能让大家看懂的技术文章才是好文章。这也是我最初决定写文章最主要的目标和动力。希望这篇文章对你有所帮助,欢迎留言讨论,后续会更新更多用大白话来讲的内容。
下一篇继续深入的讲一下:Java中Synchronized的逻辑
以上写的都是自己的理解和总结,纯手敲,原创不易,如果觉得文章对你有帮助,可以关注一下,感谢。
更多推荐


所有评论(0)