深刻理解Java并发框架AQS系列(一):线程
深刻理解Java并发框架AQS系列(二):AQS框架简介及锁概念html
重剑无锋,大巧不工java
读j.u.c
包下的源码,永远没法绕开的经典并发框架AQS
,其设计之精妙堪比一件艺术品,令众多学者绝不吝惜溢美之词。近期准备出一系列关于AQS的文章,系统的来说解AQS,我将跟你们一块儿带着敬畏之心去读她,但也会对关键部分提出质疑及思考api
原本打算直接以阐述锁概念做为开头,但发现始终都绕不过线程这关,再加上如今好多讲述线程的文章概念混淆不清,误人子弟,索性开此文,一来作一些基础工做的铺垫,二来咱们把线程的一些概念聊透安全
名词 | 描述 |
---|---|
j.u.c |
本文特指java.util.concurrent 包 |
AQS |
本文特指围绕j.u.c 包下的类AbstractQueuedSynchronizer.java 提供的一套轻量级并发框架 |
线程状态属于老生常谈的话题,在网上一搜一大把,但发现不少文章都是人云亦云。咱们将结合代码实例来逐一论述线程状态。并发
我尝试想用一张图把状态流转描述清楚,发现很是困难,因为wait/notify
使用的特殊性,会将整个流程图搅得很乱,因此此处咱们把状态流转拆分为(非wait方法)及(wait方法)。若是你在某些文章中看到用一张图来描述线程状态流转的,那么要留心了,仔细甄别下,看其是否遗漏了某些场景框架
站在JVM的视角,将线程状态分红了6种状态:ide
NEW-初始
RUNNABLE-可运行
BLOCKED-阻塞
WAITING-等待
TIMED_WAITING-超时等待
TERMINATED-结束
为了论述的更为完全,咱们站在操做系统的角度,将RUNNABLE-可运行
状态拆分为runnable-就绪状态
及running-运行状态
,故一共7种状态this
线程在新建后,且在调用start
方法前的状态为初始状态,此时操做系统感知不到线程的存在,仅存在于JVM内部操作系统
就绪状态表示当前线程已经启动,只要操做系统调度了cpu时间片,便可运行,其本质上仍是处于等待;例如3个正常启动且无阻塞的线程,运行在一个2核的计算机上,那么在某一个时刻,必定至少有1个线程处于就绪状态,等待着cpu资源线程
惟一一个正在运行中的状态,且当前线程没有阻塞、休眠、挂起等;处于此状态的线程,经过主动调用Thread.yield()
方法,可变为就绪状态
线程被动地处于synchronized
的阻塞队列中,没有超时概念、不响应中断
顾名思义,线程处于主动等待中,且响应中断;当线程主动调用了如下3个方法时,即处于等待状态,等待其余线程的唤起
Thread.join()
LockSupport.park()
Object.wait()
与阻塞状态的区别:
synchronized
代码块时,它不知道本身立刻抢到锁并执行后续逻辑仍是会被阻塞此状态与waiting
状态定义基本一致,只是引入了超时概念;进入timed_waiting
的方法以下:
Thread.sleep(long)
Thread.join(long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(long)
Object.wait(long)
线程运行完毕,处于此状态的线程不能再次启动,也不能转换为其余状态,等待垃圾回收
线程调用Thread.start()
方法便可进入就绪状态
操做系统调度,JVM层面没法干预
分主动、被动2种方式
Thread.yield()
2种场景可将一个运行状态的线程变为阻塞状态,且都与synchronized
相关
场景1:线程因争抢synchronized
锁失败,从而进入等待队列时,线程状态置为blocked
@Test public void test5() throws Exception { Object obj = new Object(); Thread thread1 = new Thread(() - > { synchronized(obj) { int sum = 0; // 模拟线程运行 while(1 == 1) { sum++; } } }); thread1.start(); // 停顿1秒钟后再启动线程2,保证线程1已启动运行 Thread.sleep(1000); Thread thread2 = new Thread(() - > { synchronized(obj) { System.out.println("进入锁中"); } }); thread2.start(); System.out.println("线程1状态:" + thread1.getState()); System.out.println("线程2状态:" + thread2.getState()); } ----------运行结果---------- 线程1状态:RUNNABLE 线程2状态:BLOCKED
场景2:处于Object.wait()
的线程在被唤醒后,不会当即去执行后续代码,并且是会从新争抢synchronized
锁,争抢失败的即会进入同步队列排序,此时的线程状态一样为blocked
@Test public void test6() throws Exception { Object obj = new Object(); Thread[] threads = new Thread[2]; for(int i = 0; i < threads.length; i++) { threads[i] = new Thread(() - > { synchronized(obj) { try { obj.wait(); // 模拟后续运算,线程不会立刻结束 while(1 == 1) {} } catch(InterruptedException e) { e.printStackTrace(); } } }); threads[i].setName("线程" + (i + 1)); threads[i].start(); } Thread.sleep(1000); // 激活全部阻塞线程 synchronized(obj) { obj.notifyAll(); } Thread.sleep(1000); System.out.println("线程1状态:" + threads[0].getState()); System.out.println("线程2状态:" + threads[1].getState()); } ----------运行结果---------- 线程1状态:BLOCKED 线程2状态:RUNNABLE
场景1:调用Thread.join()
@Test public void test7() throws Exception { Thread thread1 = new Thread(() - > { // 死循环,模拟运行 while(1 == 1) {} }); thread1.start(); Thread thread2 = new Thread(() - > { try { thread1.join(); System.out.println("线程2开始执行"); } catch(InterruptedException e) { e.printStackTrace(); } }); thread2.start(); Thread.sleep(1000); System.out.println("线程2状态:" + thread2.getState()); } ----------运行结果---------- 线程2状态:WAITING
场景2:调用LockSupport.park()
,即挂起线程,且只能挂起当前线程
@Test public void test8() throws Exception { Thread thread1 = new Thread(LockSupport::park); thread1.start(); Thread.sleep(1000); System.out.println("线程1状态:" + thread1.getState()); } ----------运行结果---------- 线程1状态:WAITING
Thread.sleep(long)
Thread.join(long)
LockSupport.parkNanos(long)
LockSupport.parkUntil(long)
读者可自行写代码验证,此处再也不赘述
当执行完Object.wait()/Object.wait(long)
后,不会立刻进入就绪状态,线程间还要继续争抢同步队列的锁,争抢失败的便会进入阻塞状态;在AQS后续的条件队列Condition
文章中,还会继续说明
线程正常执行完毕,结束了run
方法后便进入终止状态,没法再被唤起,等待GC回收
从线程api那些被@Deprecated
标记的方法就能看出,线程的设计发展不是一路顺风的,那些被标记过期的方法都带来了哪些问题?咱们举两个例子来讲明
Thread.stop()
这个方法不就是将线程停掉么,能带来什么问题?并且调用此方法后,即使获取了synchronized
锁也会自动释放,咱们要挂起线程的时候,不也要调用LockSupport.park()
方法么
的确,其实万恶之源在于stop()
方法可由其余线程调用,其余线程在调用时,不知道目标线程是什么状态,也不知道其是否加锁,或正在执行一些原子操做。
最直接的是会带来2个问题,且都是灾难级别的
例如:
public class MyThread extends Thread { private int i = 0; private int j = 0; @Override public void run() { synchronized(this) { ++i; try { //休眠10秒,模拟耗时操做 Thread.sleep(10000); } catch(InterruptedException e) { e.printStackTrace(); } ++j; } } public void print() { System.out.println("i=" + i + " j=" + j); } }
咱们必定认为synchronized
方法中的逻辑是原子操做,即全部线程都尘埃落定后,i
与j
的值必定相等;然而事与愿违,因为stop()
的介入,破坏了程序的完整性
其次若是目标线程正在修改某个线程共享变量 ,stop()
从天而降,这个共享变量最终形态谁也没法预测,为何会变成这样,全部线程都大眼瞪小眼;就比如把一头狮子放进澡堂洗澡,出来的时候变成了一只鸡,谁都没法解释,程序也即进入了混乱
语言层面的锁synchronized
在执行stop()
方法时会被释放,但j.u.c
下或自定义锁就没那么好运了
@Test public void test10() throws Exception { ReentrantLock reentrantLock = new ReentrantLock(); Thread thread1 = new Thread(() - > { reentrantLock.lock(); try { Thread.sleep(1000000); } catch(InterruptedException e) { e.printStackTrace(); } reentrantLock.unlock(); }); thread1.start(); Thread.sleep(500); System.out.println("thread1 状态:" + thread1.getState()); thread1.stop(); // 等待线程1结束 while(thread1.getState() != Thread.State.TERMINATED) {} System.out.println("主线程尝试获取锁"); reentrantLock.lock(); System.out.println("主线程拿到了锁"); } ----------运行结果---------- thread1 状态:TIMED_WAITING 主线程尝试获取锁
咱们看到目标锁永远没法再进入
Thread.suspend() / Thread.resume()
从字面意思能够看出,这2个方法是成对儿出现的
Thread.suspend()
线程暂停Thread.resume()
线程恢复它们带来的了那个臭名昭著的问题:死锁
@Test public void test11() throws Exception { Object lock = new Object(); Thread thread1 = new Thread(() - > { synchronized(lock) { try { Thread.sleep(2000000); } catch(InterruptedException e) { e.printStackTrace(); } finally { System.out.println("执行 finally"); } } }); thread1.start(); Thread.sleep(500); thread1.suspend(); System.out.println("已经将线程1暂停"); System.out.println("准备获取lock锁"); synchronized(lock) { System.out.println("主抢到锁了"); } } ----------运行结果---------- 已经将线程1暂停 准备获取lock锁
上述程序陷入了无尽的等待;由于目标线程虽然已经被suspend
,但并不会释放锁,当主线程去尝试加锁时,便陷入了无尽等待
为何会产生这样的现象?其实终其缘由是由于其余线程在没法得知目标线程运行状态的前提下,强制进行kill或暂停,所带来的一系列问题;举个不恰当的例子:张三经过小推车持续搬砖了2个小时,工头在办公室经过传呼下达命令:中止工做!此时张三当即放下手中的活儿,小推车因被张三占用,其余人没法开战工做。因此咱们是否应该去提醒,而不是直接下达命令,至于在什么时间、什么地点中止工做由张三来决定呢?这就引出了咱们要聊得下一个话题:中断
线程中断并非将一个正在运行的线程中断而导致其终止;
线程中断仅仅是设置线程的中断标记位,不会对目标线程的运行产生干扰。而只有当目标线程响应了中断,从而自发的抛出异常或结束waiting
;
后续文章中将讲到的AQS提供的方法都是支持响应中断的,此处咱们简单罗列一下经常使用的响应线程中断的方法
Object.wait() / Object.wait(long)
Thread.join() / Thread.join(long)
Thread.sleep(long)
LockSupport.park() / LockSupport.parkNanos(long) / LockSupport.parkUntil(long)
那么JVM内部是如何实现响应中断呢?拿Thread.sleep(long)
举例,看其C++源码会发现,JVM会将一次长睡眠分割为屡次小的睡眠,目标就是及时响应中断
咱们延续3.1小节的例子:张三经过小推车持续搬砖了2个小时,妻子看到后说“喝口水,歇会儿吧”(发送打断命令),此时张三的反应可分为如下2类:
主要讨论wait/notify
与park/unpark
,二者既然都支持线程的挂起及激活,有什么异同点吗?各自的应用场景何在?
相同点
不一样点
功能点 | 精准控制 | 执行顺序 | 中断 |
---|---|---|---|
wait/notify |
挂起:指定当前线程挂起 唤醒:随机唤醒 1 个线程或所有唤醒 |
执行顺序须要严格保证wait 操做发生在notify 以前,若是notify 在wait 以前执行了,那么wait 操做将进入无限等待的窘境 |
响应中断,且需处理编译期异常 |
park/unpark |
挂起:指定当前线程挂起 唤醒:精确唤醒指定的 1 个线程 注:虽然唤醒可指定某线程,但挂起操做只会针对当前线程生效,由于当前线程并不了解被挂起线程的真实状态,若是一旦可操控,势必会带来不可预期的安全问题 |
unpark 操做可发生在park 以前,但仅会生效一次;例如针对线程A首先执行了2次unpark 操做,而后对A第1次执行park 操做时不会有阻塞,但第2次执行park 时会进入等待 |
响应中断,但不抛出异常,发生中断后,park() 方法会自动结束,经过Thread.interrupted() 来判断是中断仍是unpark() 致使的 |