深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(一):线程
深入理解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方法)。如果你在某些文章中看到用一张图来描述线程状态流转的,那么要留心了,仔细甄别下,看其是否遗漏了某些场景
站在JVM的视角,将线程状态分成了6种状态:
NEW-初始
RUNNABLE-可运行
BLOCKED-阻塞
WAITING-等待
TIMED_WAITING-超时等待
TERMINATED-结束
为了论述的更为彻底,我们站在操作系统的角度,将RUNNABLE-可运行
状态拆分为runnable-就绪状态
及running-运行状态
,故一共7种状态
2.1、状态定义
2.1.1、初始状态(new)
线程在新建后,且在调用start
方法前的状态为初始状态,此时操作系统感知不到线程的存在,仅存在于JVM内部
2.1.2、就绪状态(runnable)
就绪状态表示当前线程已经启动,只要操作系统调度了cpu时间片,即可运行,其本质上还是处于等待;例如3个正常启动且无阻塞的线程,运行在一个2核的计算机上,那么在某一个时刻,一定至少有1个线程处于就绪状态,等待着cpu资源
2.1.3、运行状态(running)
唯一一个正在运行中的状态,且当前线程没有阻塞、休眠、挂起等;处于此状态的线程,通过主动调用Thread.yield()
方法,可变为就绪状态
2.1.4、阻塞状态(blocked)
线程被动地处于synchronized
的阻塞队列中,没有超时概念、不响应中断
2.1.5、等待状态(waiting)
顾名思义,线程处于主动等待中,且响应中断;当线程主动调用了以下3个方法时,即处于等待状态,等待其他线程的唤起
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()
方法即可进入就绪状态
就绪 -> 运行
操作系统调度,JVM层面无法干预
运行 -> 就绪
分主动、被动2种方式
- 1、当前线程的cpu时间片用完,被动进入就绪状态
- 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
运行 -> 超时等待
- 1、
Thread.sleep(long)
- 2、
Thread.join(long)
- 3、
LockSupport.parkNanos(long)
- 4、
LockSupport.parkUntil(long)
读者可自行写代码验证,此处不再赘述
等待/超时等待 -> 阻塞
当执行完Object.wait()/Object.wait(long)
后,不会马上进入就绪状态,线程间还要继续争抢同步队列的锁,争抢失败的便会进入阻塞状态;在AQS后续的条件队列Condition
文章中,还会继续说明
运行 -> 终止
线程正常执行完毕,结束了run
方法后便进入终止状态,无法再被唤起,等待GC回收
三、线程概念
3.1、曲折中前进
从线程api那些被@Deprecated
标记的方法就能看出,线程的设计发展不是一帆风顺的,那些被标记过时的方法都带来了哪些问题?我们举两个例子来说明
3.1.1、Thread.stop()
这个方法不就是将线程停掉么,能带来什么问题?而且调用此方法后,即便获取了synchronized
锁也会自动释放,我们要挂起线程的时候,不也要调用LockSupport.park()
方法么
的确,其实万恶之源在于stop()
方法可由其他线程调用,其他线程在调用时,不知道目标线程是什么状态,也不知道其是否加锁,或正在执行一些原子操作。
最直接的是会带来2个问题,且都是灾难级别的
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()
从字面意思可以看出,这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
,但并不会释放锁,当主线程去尝试加锁时,便陷入了无尽等待
3.1.3、思考
为什么会产生这样的现象?其实终其原因是因为其他线程在无法得知目标线程运行状态的前提下,强制进行kill或暂停,所带来的一系列问题;举个不恰当的例子:张三通过小推车持续搬砖了2个小时,工头在办公室通过传呼下达命令:停止工作!此时张三立即放下手中的活儿,小推车因被张三占用,其他人无法开战工作。所以我们是否应该去提醒,而不是直接下达命令,至于在什么时间、什么地点停止工作由张三来决定呢?这就引出了我们要聊得下一个话题:中断
3.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类:
- 感觉不累,继续工作(不响应中断)
- 把东西归置完毕、小推车归还后,开始休息(让出资源,并在合适的时机休息)
3.3、线程阻塞与挂起
主要讨论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()
导致的
深入理解Java并发框架AQS系列(一):线程的更多相关文章
- 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...
- 深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock) 深入 ...
- 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock)
一.前言 优秀的源码就在那里 经过了前面两章的铺垫,终于要切入正题了,本章也是整个AQS的核心之一 从本章开始,我们要精读AQS源码,在欣赏它的同时也要学会质疑它.当然本文不会带着大家逐行过源码(会有 ...
- Java并发框架——AQS中断的支持
线程的定义给我们提供了并发执行多个任务的方式,大多数情况下我们会让每个任务都自行执行结束,这样能保证事务的一致性,但是有时我们希望在任务执行中取消任务,使线程停止.在java中要让线程安全.快速.可靠 ...
- Java并发框架??AQS中断的支持
线程的定义给我们提供了并发执行多个任务的方式,大多数情况下我们会让每个任务都自行执行结束,这样能保证事务的一致性,但是有时我们希望在任务执行中取消任务,使线程停止.在java中要让线程安全.快速.可靠 ...
- 《深入理解Java集合框架》系列文章
Introduction 关于C++标准模板库(Standard Template Library, STL)的书籍和资料有很多,关于Java集合框架(Java Collections Framewo ...
- Java并发框架——AQS超时机制
AQS框架提供的另外一个优秀机制是锁获取超时的支持,当大量线程对某一锁竞争时可能导致某些线程在很长一段时间都获取不了锁,在某些场景下可能希望如果线程在一段时间内不能成功获取锁就取消对该锁的等待以提高性 ...
- Java并发框架——AQS阻塞队列管理(三)——CLH锁改造
在CLH锁核心思想的影响下,Java并发包的基础框架AQS以CLH锁作为基础而设计,其中主要是考虑到CLH锁更容易实现取消与超时功能.比起原来的CLH锁已经做了很大的改造,主要从两方面进行了改造:节点 ...
- 深入理解Java并发类——AQS
目录 什么是AQS 为什么需要AQS AQS的核心思想 AQS的内部数据和方法 如何利用AQS实现同步结构 ReentrantLock对AQS的利用 尝试获取锁 获取锁失败,排队竞争 参考 什么是AQ ...
随机推荐
- c++ cin 读入txt的问题
源程序 #include <iostream> using namespace std; struct Stack { int tos; int stackarray[1000]; }; ...
- μC/OS-III---I笔记13---中断管理
中断管理先看一下最常用的临界段进入的函数:进入临界段 OS_CRITICAL_ENTER() 退出临界段OS_CRITICAL_EXIT()他们两个的宏是这样的. 在使能中断延迟提交时: #if OS ...
- vuepress config favicon
vuepress config favicon .vuepress/public favicons https://vuepress.vuejs.org/guide/assets.html#publi ...
- js jsonParse
mdn const rx_one = /^[\],:{}\s]*$/; const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; // 匹配 ...
- PAUL ADAMS ARCHITECT:费城东北区的房地产市场逆势而行
根据Zillow.com的房产数据,大费城地区前三季度成交房产的平均价格为27.2万美元,较去年同期增长了13.4%,为10年同期最高.即使如此,27.2万的均价与纽约相比依然相距甚远,其中尤其是费城 ...
- 从微信小程序到鸿蒙js开发【08】——表单组件&注册登录模块
目录: 1.登录模块 2.注册模块 3.系列文章导读 牛年将至,祝大家行行无bug,页页so easy- 在微信小程序中,提供了form组件,可以将input.picker.slider.button ...
- Debian 基本使用进阶
系统安装好了我们,迫不及待的想要在Linux系统中肆意翱翔.如果是刚刚接触Linux的系统的话,可能一时间还无法适应Linux的系统环境.对于使用Debian来做服务器的选择,最好的练习方式的就是使用 ...
- Vue学习笔记-Django REST framework3后端接口API学习
一 使用环境 开发系统: windows 后端IDE: PyCharm 前端IDE: VSCode 数据库: msyql,navicat 编程语言: python3.7 (Windows x86- ...
- 导出----用Excel导出数据库表
根据条件导出表格: 前端 <el-form-item label=""> <el-button type="warning" icon=&qu ...
- Centos8.2安装Mongodb4.4.2(社区版)
1:下载 wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel80-4.4.2.tgz 官网地址: 2:解压 tar -zxv ...