网课链接: 黑马程序员java并发.
第四章:共享模型 管程
第四章 共享模型_管程
章节总结
本章我们需要重点掌握的是
- 分析多线程访问共享资源时,哪些代码片段属于临界区
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加载成员方法和静态方法语法
- 掌握 wait/notify 同步方法
- 使用 lock 互斥解决临界区的线程安全问题 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
应用方面
- 互斥:使用 Synchronized 或 Lock 达到共享资源互斥效果,实现原子性效果,保证线程安全。
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果。
原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制
4.1 共享带来的问题
线程出现问题的根本原因是因为线程上下文切换,上下文切换会导致线程里的指令没有执行完就切换执行其它线程了。
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
public static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join(); log.debug("{}",counter);
}
问题
以上的结果可能是正数、负数、零。为什么呢?
因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析.
两个线程是交错进行对一个共享的资源进行更改, 并且没有上下文切换.
1. 临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的, 问题出在多个线程访问共享资源
- 多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
2. 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
4.2 synchronized解决方案
应用之互斥
1. 解决手段
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案: 原子变量
synchronized 是阻塞式的解决方案,即俗称的【对象锁】:
它采用互斥的方式让同一 时刻至多只有一个线程能持有对象锁
,其它线程再想获取这个对象锁
时就会阻塞住。
这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
加了Synchronize的标记后, 函数内临界区的代码就会变成串行的, 当一个线程执行完毕后下一个线程才会执行.
总结思考
synchronized实际是用对象锁保证了临界区内代码的原子性, 临界区内的代码对外是不可分割的, 不会被线程切换所打断
语法
synchronized(对象{
//临界区
}
4.3 方法上的synchronized
- 加在成员方法上,锁住的是
对象
public Test{
// 在静态方法上加上 synchronized 关键字
public synchronized void test(){
// code
}
}
/* equivalent to*/
class Test {
public void test() {
synchronized(this){
// code
}
}
}
- 加在静态方法上,锁住的是
类
public class Test {
// 在静态方法上加上 synchronized 关键字
public synchronized static void test() {
}
//等价于
public void test() {
synchronized(Test.class) { // 锁住的是类
}
}
}
对于不加syncrhronized的方法, 就等于不遵守线程规则的方法方法, 没有执行原子性.
4.4 变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果它们的变量没有在线程间共享,则
线程安全
- 如果它们被共享了
- 单纯读取操作, 线程安全
- 有读有写, 需要考虑操作原子性, 考虑临界点
局部变量是否线程安全?
- 局部变量【局部变量被初始化为基本数据类型】 是安全的
- 局部变量是引用类型或者是对象引用则未必是安全的, 局部变量有可能位于堆中
- 如果局部变量引用的对象没有引用线程共享的对象,那么是线程安全的
- 如果局部变量引用的对象引用了一个线程共享的对象, 需要考虑线程安全
线程安全的情况
局部变量被初始化为基本数据类型是安全的,代码如下,因为每个线程都会有一份 test() 放在线程私有的栈中,多个线程就有多个,是不被多个线程共享的,所有就没有线程安全问题。
public static void test() {
int i = 10;
i++;
}
成员变量案例
class ThreadUnsafe{
ArrayList <String> list = new ArrrayList<>();
public void method1(int loopNumber){
for(int i =0; i<loopNumber; i++){
method2();
method3();
}
}
private void method2(){
list.add("1");
}
private void method3(){
list.remove(0);
}
}
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args){
ThreadUnsafe test = new ThreadUnsafe();
for(int i =0; i< THREAD_NUMBER; i++){
new Thread(()-> {
test.method1(LOOP_NUMBER);
}, "Thread"+i).start();
}
}
分析:
- 无论哪个线程中的method2 引用的都是同一个对象中的成员变量, 所以在method1还没有add情况下调用method2,就会报错
更改为局部变量则可以解决这个问题
class ThreadUnsafe{
ArrayList <String> list = new ArrrayList<>();
public void method1(int loopNumber){
for(int i =0; i<loopNumber; i++){
method2(list);
method3(list);
}
}
// list作为输入成为局部变量
private void method2(ArrayList <String> list){
list.add("1");
}
private void method3(ArrayList <String> list){
list.remove(0);
}
}
分析:
- list是局部变量, 每个线程调用时会创建其不同实例, 没有共享
- 而method2的参数是从method1中传递过来的, 与method1中引用同一个对象
- method3的参数分析与method2相同
Private 或 final的重要性
在上述代码中,其实存在线程安全的问题,因为 method2,method3 方法都是用 public 声明的,如果一个类继承 SafeTest 类,对 method2,method3 方法进行了重写,比如重写 method3 方法,代码如下:
class UnsafeSubTest extends UnsafeTest {
@Override
public void method3(List<Integer> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
可以看到重写的方法中又使用到了线程,当主线程和重写的 method3 方法的线程同时存在,此时 list 就是这两个线程的共享资源了,就会出现线程安全问题,我们可以用 private 访问修饰符解决此问题,代码实现如下:
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
局部变量被public修饰符暴露时情况
有可能子类对现有的类方法进行重写导致之局部变量的更改
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- Java.util.concurrent包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的
- 它们的每个方法是原子的
- 但注意多个方法的组合不是原子的, 当不同的操作组合在一起时, 难以确保原子性
不可变线程安全性
- String Integer 等都是不可变类, 因为其内部状态不可以改变, 因此它们的线程安全
- 因为它没有改变类型的属性 而是新建了一个存储对象进行赋值再进行返回.
日期类型时可以修改的, 即使加了Final修饰符
4.6 Monitor概念
Java对象头(32bit)
普通对象
Object Header(64bits) | |
---|---|
Mark Word (32 bits) | klass Word (32 bits) |
数组对象
Object Header (96bits) | ||
---|---|---|
Mark Word (32 bits) | klass Word (32 bits) | array length (32 bits) |
其中Mark Word结构为
Mark Word (32 bits) | State |
---|---|
Hash code:25 | age: 4| biased_lock:0| 01 | Normal |
thread: 23| epoch:2| age:4| biased_lock:1| 01 | Biased |
ptr_to_lock_record:30 |00 | Lightweight Locked |
ptr_to_heavyweight_monitor:30 |10 | Heavyweight Locked |
|11 | Marked for GC |
Monitor(锁) 原理
监控器或管程
每个java对象都可以关联一个Monitor对象, 如果使用synchronized给对象上锁(重量级)之后, 该对象头的mark word中就被设置指向Monitor对象的指针
Monitor
- WaitSet: 等待队列
- EntryList: 阻塞队列
- Owner: 当前运行thread
运行逻辑
- 刚开始时 Monitor 中的 Owner 为 null
- 当 Thread-2 执行
synchronized(obj){}
就会将 Monitor 的所有者Owner
设置为 Thread-2,上锁成功
,Monitor 中同一时刻只能有一个 Owner - 当 Thread-2 占据锁时,如果线程 Thread-3 ,Thread-4 也来执行
synchronized(obj){}
代码,就会进入 EntryList(阻塞队列) 中变成BLOCKED
(阻塞) 状态 - Thread-2 执行完同步代码块的内容,然后唤醒
EntryList
中等待的线程来竞争锁
,竞争时是非公平的 - 图中
WaitSet
中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入WAITING
状态的线程,后面讲 wait-notify 时会分析
注意:
- Synchronized 必须是进入同一个对象的monitor才有上述的效果
- 不加Synchronized的对象不会关联监控器, 不遵从以上规则
synchronized 优化原理
1. Synchronized 用于同步代码块与同步方法原理
2. 轻量级锁
使用场景. 如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized ,假设有两个方法同步块,利用同一个对象加锁.
synchronized会自动先使用轻量级, 当竞争多了才会转换为重量级
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
// critical area1
method2()
}
}
public static void method2(){
synchronized(obj){
// critical area2
}
}
- 创建锁记录对象, 每个线程的栈帧都会包含一个锁记录的结构, 内部可以存储锁定对象的mark word
- 让锁记录中Object Reference只想锁对象, 并尝试用CAS (Compare and swap) 替换Object的Mark Word
- 如果cas替换成功, 对象头中存储了
锁记录地址和状态 00
, 代表着给对象加锁
- 如果CAS失败, 有两种情况
- 如果是其他线程已经持有了该Object的轻量级锁, 这时表明有竞争, 进入锁膨胀过程
- 如果是自己执行了synchronized锁重入, 那么在添加一条lock record作为重入的计数
- 当线程退出 synchronized 代码块的时候,如果
获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
- 成功则解锁成功
- 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
3. 锁膨胀
如果在尝试加轻量级锁的过程中, CAS操作无法成功, 这时候一种情况就是其他线程为此对象加上了轻量级锁(有竞争), 这时需要进行锁膨胀, 将轻量级锁变为重量级
-
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
-
这时候, Thread-1 加轻量级锁失败, 进入锁膨胀流程
- Object对象申请Monitor锁, 让Object指向重量级锁地址
- 然后自己进入Monitor 的EntryList Blocked
-
当Thread-的同步块解锁时, 使用CAS将MarkWord的值回复给对象头. 若失败, 这时会进入重量级锁流程, 即按照monitor地址找到monitor对象, Owner设置为null , 唤醒EntryList中的Thread-1线程
4. 自旋优化
重量级锁竞争, 自旋优化, 如果当前线程自旋成功 (即这时候持锁线程已经退出了同步块,释放了锁), 这时当前线程就可以避免阻塞.
自旋: 反复尝试获取锁, 而不是进入阻塞队列, 进入阻塞队列意味着会进行上下文切换.
两种情况:
- 自旋重试成功的情况
- 自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁
适合多核cpu
- Java6之后, 自旋锁是自适应的,
成功过会导致成功可能性升高, 就会反复尝试
, 若失败则相反. - 自旋会占用CPU时间, 单核CPU自旋会浪费
- Java7之后不能控制是否开启自旋功能
5. 偏向锁
在轻量级的锁中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行 CAS 操作
,会增加时间损耗,那么 java6 开始引入了偏向锁,只有第一次使用 CAS 时将对象的 Mark Word 头设置为偏向线程 ID,之后这个入锁线程再进行重入锁时,发现线程 ID 是自己的,那么就不用再进行CAS了。
**Java 6 前的设计中 , 一个线程重入的时候要执行CAS操作, 过于耗时.Java 6 后, 只要发现线程ID是自己的就不再CAS **
偏向状态
一个对象的创建过程
- 如果开启了偏向锁(默认是开启的),初始Mark Word 最后三位的值101,并且这是它的 Thread,epoch,age 都是 0 ,在加锁的时候进行设置这些的值.
- 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:
-XX:BiasedLockingStartupDelay=0 来禁用延迟 - 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
撤销偏向
- 调用对象的 hashCode 方法
- 多个线程使用该对象
- 调用了 wait/notify 方法(调用wait方法会导致锁膨胀而使用重量级锁)
6. 批量重偏向
- 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向 t1 的对象仍有机会重新偏向 t2
- 重偏向会重置Thread ID
- 当撤销超过20次后(超过阈值),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程。
7. 批量撤销
当撤销偏向锁的阈值超过 40 以后,就会将整个类的对象都改为不可偏向的
4.7, 4.8 wait notify
1. 原理
- 锁对象调用wait方法(obj.wait),就会使当前线程进入 WaitSet 中,变为 WAITING 状态。
- 处于BLOCKED和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:
- BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于 BLOCKED 状态
- WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了 wait 方法而进入了 WaitSet 中,处于 WAITING 状态
- BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法(obj.notify/obj.notifyAll),才会被唤醒。
注:只有当对象加锁以后,才能调用 wait 和 notify 方法
API介绍
- obj.notify()
- obj.wait()
- obj.notifyall()
必须获得对象锁才能调用
2. sleep() vs wait()
- Sleep是Thread的静态方法, 而wait是Object的方法, Object 又是所有类的父类,所以所有类都有Wait方法。
- Sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起
- Sleep在睡眠的同时不会释放对象锁, 但wait在等待的时候会释放对象锁
- 共同点: 线程状态一致
3. 使用 wait/notify
- 当线程不满足某些条件,需要暂停运行时,可以使用 wait 。这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用 sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行。
注意⚠️: 当有多个线程在运行时,对象调用了 wait 方法,此时这些线程都会进入 WaitSet 中等待。如果这时使用了 notify 方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用 notifyAll 方法
这是因为在notify会随机唤醒等待线程
用while代替if来解决虚假唤醒的问题
synchronized(lock){
while(/*条件不成立*/) {
lock.wait();
} // 干活
}//另一个线程 synchronized(lock) { lock.notifyAll(); }
4. [设计模式] 同步模式之保护性暂停
即Guarded Suspension, 用在一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
- JDK 中,join 的实现、Future 的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
与线程join相比, 保护性暂停优势在于
- 被等待的线程可以继续执行而不用立即返回, 而join的使用需要结果返回才会继续进行, join 等待另一个线程的结束, 而 保护性暂停 等待结果
- 等待结果变量必须设置为全局
拓展- 多任务
多任务版 GuardedObject 图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个生产者和消费者之间是一一对应的关系,但是生产者消费者模式并不是。rpc 框架的调用中就使用到了这种模式。
5. 异步模式之生产者/消费者
“异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。
消息队列是线程之间通信的. rabbitMq 是进程之间通信的工具
要点
-
与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
-
消费队列可以用来平衡生产和消费的线程资源
-
生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
-
消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
-
JDK 中各种阻塞队列,采用的就是这种模式
小结
- 当调用 wait 时,首先需要确保调用了 wait 方法的线程已经持有了对象的锁(调用 wait 方法的代码片段需要放在 sychronized 块或者时 sychronized 方法中,这样才可以确保线程在调用wait方法前已经获取到了对象的锁)
- 当调用 wait 时,该线程就会释放掉这个对象的锁,然后进入等待状态 (wait set)
- 当线程调用了 wait 后进入到等待状态时,它就可以等待其他线程调用相同对象的 notify 或者 notifyAll 方法使得自己被唤醒
- 一旦这个线程被其它线程唤醒之后,该线程就会与其它线程以同开始竞争这个对象的锁(公平竞争);只有当该线程获取到对象的锁后,线程才会继续往下执行
- 当调用对象的 notify 方法时,他会随机唤醒对象等待集合 (wait set) 中的任意一个线程,当某个线程被唤醒后,它就会与其它线程一同竞争对象的锁
- 当调用对象的 notifyAll 方法时,它会唤醒该对象等待集合 (wait set) 中的所有线程,这些线程被唤醒后,又会开始竞争对象的锁
- 在某一时刻,只有唯一的一个线程能拥有对象的锁
4.9 Park & Unpark
基本使用
它们是 LockSupport 类中的方法
// 暂停当前线程 LockSupport.park();// 恢复某个线程的运行 LockSupport.unpark(暂停线程对象)
特点
与Object的wait¬ify相比
- wiat, notify 和 notifyAll 必须配合Object Monitor 一起使用, 而unpark不必
- Park & unpark是以线程为单位来阻塞和唤醒, 而notify只能随机唤醒一个等待线程, notifyAll是唤醒所用等待线程, 就不那么精确
- park & unpark 可以先unpark, 而wait & notify不能先notify
原理
每个线程都有自己的一个parker对象, 由三个部分组成_counter, _cond, _mutex
park:
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁(mutex对象有个等待队列 _cond)
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
unpark:
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
先调用upark再调用park的过程
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
4.10 java 线程状态和转换
情况一:NEW –> RUNNABLE
当调用了 t.start() 方法时,由 NEW –> RUNNABLE
情况二: RUNNABLE <–> WAITING
- 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争
- 竞争锁成功,t 线程从 WAITING –> RUNNABLE
- 竞争锁失败,t 线程从 WAITING –> BLOCKED
情况三:RUNNABLE <–> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING
- 注意是当前线程在 t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
情况四: RUNNABLE <–> WAITING
当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
情况五: RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
情况六:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING
注意是当前线程在 t 线程对象的监视器上等待 - 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
情况七:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
情况八:RUNNABLE <–> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
情况九:RUNNABLE <–> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况十: RUNNABLE <–> TERMINATED
当前线程所有代码运行完毕,进入 TERMINATED
4.11 多把锁
多把不相干的锁 : 针对对象中的不同方法设立锁, 使得程序可以调用并发执行到方法
-
好处: 是可以增强可开发度
-
坏处: 如果一个线程需要同时获得多把锁, 就容易发生死锁
4.12 活跃性
线程因为某些原因,导致代码一直无法执行完毕,这种的现象叫做活跃性。
- 互斥条件
在一段时间内,一种资源只能被一个进程所使用 - 请求和保持条件
进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源 - 不可抢占条件
进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放 - 循环等待条件
发生死锁时,必然存在一个进程——资源的循环链。
死锁
一个线程需要同时获取多把锁, 这时容易出现死锁
两个线程希望同时获取对方的锁
定位死锁
- 检测死锁可以使用jconsole工具, 或者jps(定位进程id)+jstack(定位死锁)
活锁
活锁出现在两个线程互相改变对方的结束条件, 最后谁也无法结束
避免活锁的方法
在线程执行时,中途给予不同的间隔时间即可。
死锁与活锁的区别
- 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象
- 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象
在开发中, 可以随机增加睡眠时间尽可能避免活锁的产生
饥饿
很多教程中把饥饿定位为: 一个线程由于优先级太低, 始终得不到CPU调度执行, 也不能够结束.
在使用顺序加锁时,可能会出现饥饿现象
4.13 ReentrantLock
与synchronized相比:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与synchronized一样, 都支持可重入
基本语法
reentrantLock.lock();try{ // crit area}finally{ // release lock reentrantLock.unlock();}
可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
可打断
如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,获得锁失败
简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {// 加锁,可打断锁
lock.lockInterruptibly();
}
catch (InterruptedException e) {
e.printStackTrace();
return;
}
finally { // 释放锁
lock.unlock();
}
});
lock.lock();
try {
t1.start();
Thread.sleep(1000); // 打断
t1.interrupt();
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
}
}
锁超时
使用lock.tryLock方法会返回获取锁是否成功, 如果成功则返回true, vice versa
并且tryLock方法可以指定等待时间, 参数为
tryLock(long timeout, TimeUnit unit)
- timeout 为最长等待时间
- TimeUnit 为时间单位
总结:获取锁失败了、获取超时了或者被打断了,不再阻塞,直接停止运行。
公平锁
在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。
ReentrantLock lock = new ReentrantLock(boolean fair);
一般选择用try lock来实现公平性, 使用公平锁会降低并发度
条件变量
synchronized中也有条件变量, waitSet. 当条件不足时进入waitSet等待
ReEntrantLock的条件变量比synchronized强大之处在于, ReEntrantLock支持多个条件变量
- synchronized中, 那些不满足条件条件的线程在一件休息室
- 而ReEntrantLock支持多个休息室
使用流程
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行