Java 并发编程笔记 第四章:共享模型_管程

网课链接: 黑马程序员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| 01Normal
thread: 23| epoch:2| age:4| biased_lock:1| 01Biased
ptr_to_lock_record:30 |00Lightweight Locked
ptr_to_heavyweight_monitor:30 |10Heavyweight Locked
|11Marked for GC

Monitor(锁) 原理

监控器或管程

每个java对象都可以关联一个Monitor对象, 如果使用synchronized给对象上锁(重量级)之后, 该对象头的mark word中就被设置指向Monitor对象的指针

在这里插入图片描述

Monitor

  • WaitSet: 等待队列
  • EntryList: 阻塞队列
  • Owner: 当前运行thread

运行逻辑

  1. 刚开始时 Monitor 中的 Owner 为 null
  2. 当 Thread-2 执行 synchronized(obj){} 就会将 Monitor 的所有者Owner 设置为 Thread-2,上锁成功,Monitor 中同一时刻只能有一个 Owner
  3. 当 Thread-2 占据锁时,如果线程 Thread-3 ,Thread-4 也来执行synchronized(obj){}代码,就会进入 EntryList(阻塞队列) 中变成BLOCKED(阻塞) 状态
  4. Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  5. 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING状态的线程,后面讲 wait-notify 时会分析

注意:

  • Synchronized 必须是进入同一个对象的monitor才有上述的效果
  • 不加Synchronized的对象不会关联监控器, 不遵从以上规则

synchronized 优化原理

1. Synchronized 用于同步代码块与同步方法原理

深入分析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 
  }
}
  1. 创建锁记录对象, 每个线程的栈帧都会包含一个锁记录的结构, 内部可以存储锁定对象的mark word
在这里插入图片描述
  1. 让锁记录中Object Reference只想锁对象, 并尝试用CAS (Compare and swap) 替换Object的Mark Word
在这里插入图片描述
  1. 如果cas替换成功, 对象头中存储了 锁记录地址和状态 00 , 代表着给对象加锁
在这里插入图片描述
  1. 如果CAS失败, 有两种情况
    1. 如果是其他线程已经持有了该Object的轻量级锁, 这时表明有竞争, 进入锁膨胀过程
    2. 如果是自己执行了synchronized锁重入, 那么在添加一条lock record作为重入的计数
在这里插入图片描述
  1. 当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
在这里插入图片描述
  1. 当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
    1. 成功则解锁成功
    2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
3. 锁膨胀

如果在尝试加轻量级锁的过程中, CAS操作无法成功, 这时候一种情况就是其他线程为此对象加上了轻量级锁(有竞争), 这时需要进行锁膨胀, 将轻量级锁变为重量级

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

    在这里插入图片描述
  2. 这时候, Thread-1 加轻量级锁失败, 进入锁膨胀流程

    • Object对象申请Monitor锁, 让Object指向重量级锁地址
    • 然后自己进入Monitor 的EntryList Blocked
    在这里插入图片描述
  3. 当Thread-的同步块解锁时, 使用CAS将MarkWord的值回复给对象头. 若失败, 这时会进入重量级锁流程, 即按照monitor地址找到monitor对象, Owner设置为null , 唤醒EntryList中的Thread-1线程

4. 自旋优化

重量级锁竞争, 自旋优化, 如果当前线程自旋成功 (即这时候持锁线程已经退出了同步块,释放了锁), 这时当前线程就可以避免阻塞.

自旋: 反复尝试获取锁, 而不是进入阻塞队列, 进入阻塞队列意味着会进行上下文切换.

两种情况:

  1. 自旋重试成功的情况
  2. 自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁

适合多核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()

  1. Sleep是Thread的静态方法, 而wait是Object的方法, Object 又是所有类的父类,所以所有类都有Wait方法。
  2. Sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起
  3. Sleep在睡眠的同时不会释放对象锁, 但wait在等待的时候会释放对象锁
  4. 共同点: 线程状态一致

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相比, 保护性暂停优势在于

  1. 被等待的线程可以继续执行而不用立即返回, 而join的使用需要结果返回才会继续进行, join 等待另一个线程的结束, 而 保护性暂停 等待结果
  2. 等待结果变量必须设置为全局
拓展- 多任务

多任务版 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&notify相比

  • wiat, notify 和 notifyAll 必须配合Object Monitor 一起使用, 而unpark不必
  • Park & unpark是以线程为单位来阻塞和唤醒, 而notify只能随机唤醒一个等待线程, notifyAll是唤醒所用等待线程, 就不那么精确
  • park & unpark 可以先unpark, 而wait & notify不能先notify
原理

每个线程都有自己的一个parker对象, 由三个部分组成_counter, _cond, _mutex

知乎讲解

park:

  1. 当前线程调用 Unsafe.park() 方法
  2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁(mutex对象有个等待队列 _cond)
  3. 线程进入 _cond 条件变量阻塞
  4. 设置 _counter = 0
在这里插入图片描述

unpark:

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 唤醒 _cond 条件变量中的 Thread_0
  3. Thread_0 恢复运行
  4. 设置 _counter 为 0
在这里插入图片描述

先调用upark再调用park的过程

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _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 后继续执行
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值