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