JUC-管程

章节目录:

    • 一、什么是管程
      • 1.1 概述
      • 1.2 管程组成部分
      • 1.3 变量共享问题
      • 1.4 临界区概述
      • 1.5 解决方案
    • 二、synchronized
      • 2.1 概述
      • 2.2 使用 synchronized 解决变量共享问题
      • 2.3 其它加锁情况
      • 2.4 经典案例-线程八锁
    • 三、变量的线程安全
      • 3.1 成员变量和静态变量是否线程安全?
      • 3.2 成员变量竞争示例
      • 3.3 局部变量是否线程安全?
      • 3.4 局部变量示例
      • 3.5 暴露引用会出现的问题
      • 3.6 常见线程安全类
    • 四、Monitor 概念
      • 4.1 概述
      • 4.2 Java 对象头
      • 4.3 Monitor 原理
    • 五、synchronized 原理
      • 5.1 字节码指令
      • 5.2 轻量级锁
      • 5.3 锁膨胀
      • 5.4 自旋优化
      • 5.5 偏向锁
    • 六、wait notify
      • 6.1 API 介绍
      • 6.2 wait notify 原理
      • 6.3 Sleep(long n) 与 wait(long n)的区别
      • 6.4 正确使用方式
      • 6.5 保护性暂停模式
      • 6.6 生产者消费者模式
    • 七、Park & Unpark
      • 7.1 基本使用
      • 7.2 与 wait notify 比较
      • 7.3 park unpark 原理
    • 八、多把锁
      • 8.1 说明及代码示例
      • 8.2 优点与缺点
    • 九、活跃性
      • 9.1 死锁
      • 9.2 活锁
      • 9.3 饥饿
    • 十、ReentrantLock
      • 10.1 概述
      • 10.2 可重入
      • 10.3 可打断
      • 10.4 锁超时
      • 10.5 公平锁
      • 10.6 条件变量
    • 十一、同步模式之顺序控制
      • 11.1 固定顺序
      • 11.2 交替输出
    • 十二、结束语

一、什么是管程

在并发编程中,有两大核心问题,一是互斥(即同一时刻只允许一个线程访问共享资源);另一个是同步(即线程之间如何通信协作)。而这两大问题,可以通过管程来进行解决。

1.1 概述

  • 管程 ( Monitor,也称为监视器) :是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。
  • 简而言之,管程是管理共享变量以及对共享变量的操作过程,使其支持并发
  • 而在 Javasynchronized 关键字及 wait()notify()notifyAll() 这三个方法都是管程的组成部分。管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。

1.2 管程组成部分

  • 管程的名称。
  • 局部于管程内部的共享数据结构说明。
  • 对该数据结构进行操作的一组过程。
  • 对局部于管程内部的共享数据设置初始值的语句。

1.3 变量共享问题

  • 代码示例
@Slf4j
public class Sample {

    private static int num = 0;

    public static void main(String[] args) throws InterruptedException {

        // t1线程完成对静态成员变量自增操作。
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }, "t1");

        // t2线程完成对静态成员变量自减操作。
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                num--;
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.debug("num={}", num);
        // 执行第一次:num=-6821
        // 执行第二次:num=1953

    }
}
  • 注意:以上执行结果可能会出现三种情况:负数,0,正数。
  • 为0的情况 - 示意图

JUC-管程_第1张图片

  • 为负数情况 - 示意图

JUC-管程_第2张图片

  • 为正数的情况 - 示意图

JUC-管程_第3张图片

1.4 临界区概述

一个程序运行多个线程本身是没有问题的,问题出在多个线程共享资源读写操作时发生指令交错,就会出现问题。

  • 临界区(Critical Section):指的是一个访问共享资源的程序片段,而这些共享资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共享资源是被互斥获得使用的
  • 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件(Race Condition)

1.5 解决方案

  • 为了避免临界区的竞态条件发生,有多种手段可以达到目的:
    • 阻塞式的解决方案:synchronizedLock
    • 非阻塞式的解决方案:原子变量。

二、synchronized

2.1 概述

  • synchronized 关键字:即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

  • 虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。
    • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点。
  • 语法示例

synchronized(Object){ // 线程1进入执行,线程2阻塞等待。
    // 临界区
}

2.2 使用 synchronized 解决变量共享问题

  • 代码示例
@Slf4j
public class Sample {

    private static int num = 0;

    /**
     * 两个线程对同一对象加锁。
     */

    private static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {

        // t1线程完成对成员变量自增操作。
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (obj) {
                    num++;
                }
            }
        }, "t1");

        // t2线程完成对成员变量自减操作。
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (obj) {
                    num--;
                }
            }
        }, "t2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        log.debug("num={}", num);
        // 执行第一次:num=0
        // 执行第二次:num=0
    }
}
  • 示意图

JUC-管程_第4张图片

  • 总结synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

2.3 其它加锁情况

  • synchronized 放在循环体外? — 只是锁粒度不同,依然能锁住对象,num=0
        // t1线程完成对成员变量自增操作。
        Thread t1 = new Thread(() -> {
            // 将 synchronized 放在循环体外,原子性。
            synchronized (obj) {
                for (int i = 0; i < 10000; i++) {
                    num++;
                }
            }
        }, "t1");
  • 锁不同对象?— 相当于上了两把不同的对象锁,无法锁住。
    private static final Object obj1 = new Object();
    private static final Object obj2 = new Object();

        // t1线程完成对成员变量自增操作。
        Thread t1 = new Thread(() -> {
            // 锁 obj1
            synchronized (obj1) {
                for (int i = 0; i < 10000; i++) {
                    num++;
                }
            }
        }, "t1");

        // t2线程完成对成员变量自减操作。
        Thread t2 = new Thread(() -> {
            // 锁 obj2
            synchronized (obj2) {
                for (int i = 0; i < 10000; i++) {
                    num--;
                }
            }
        }, "t2");
  • 拿一个线程不加锁?— 相当于没有限制,无法锁住。

2.4 经典案例-线程八锁

此处通过 8 种情况的案例,明白锁住的对象究竟是谁。

  • 锁案例1:同一对象调用两个普通同步方法 — 锁住了同一个实例对象,同步执行。
@Slf4j
class Sample1 {

    /**
     * 普通同步方法 a。
     */

    public synchronized void a() {
        log.debug("a");
    }

    /**
     * 普通同步方法 b。
     */

    public synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {
        // 通过一个对象去调用普通同步方法。
        Sample1 sample1 = new Sample1();
        new Thread(sample1::a).start();
        new Thread(sample1::b).start();
        // a b
    }
}
  • 锁案例2:普通同步方法 a() 中加入睡眠。 — 因为都是锁同一个对象,即便加了睡眠也是同步执行。
@Slf4j
class Sample2 {

    /**
     * 普通同步方法 a。
     */

    public synchronized void a()  {
        try {
            // 增加1s睡眠。
            TimeUnit.SECONDS.sleep(1);
            log.debug("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 普通同步方法 b。
     */

    public synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {
        Sample2 sample2 = new Sample2();
        new Thread(sample2::a).start();
        new Thread(sample2::b).start();
        // a b
    }
}
  • 锁案例3:新加入普通非同步方法 c() — 方法 a()b() 共享一把对象锁仍是同步执行,非同步方法 c() 无锁则并行执行
@Slf4j
class Sample3 {

    /**
     * 普通同步方法 a。
     */

    public synchronized void a() {
        try {
            TimeUnit.SECONDS.sleep(1);
            log.debug("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 普通同步方法 b。
     */

    public synchronized void b() {
        log.debug("b");
    }

    /**
     * 新增:普通非同步方法 c。
     */

    public void c() {
        log.debug("c");
    }

    public static void main(String[] args) {
        Sample3 sample3 = new Sample3();
        new Thread(sample3::a).start();
        new Thread(sample3::b).start();
        new Thread(sample3::c).start();
        // c a b
    }
}
  • 锁案例4:通过不同对象,调用不同的同步方法。 — 相当于持有两把不同的对象锁并行执行。
@Slf4j
class Sample4 {

    /**
     * 普通同步方法 a。
     */

    public synchronized void a() {
        log.debug("a");
    }

    /**
     * 普通同步方法 b。
     */

    public synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {
        Sample4 sample = new Sample4();
        Sample4 sample2 = new Sample4();
        // 创建不同对象调用不同的同步方法。
        new Thread(sample::a).start();
        new Thread(sample2::b).start();
        // 执行第一次:b a
        // 执行第二次:a b
    }
}
  • 锁案例5:将 普通同步方法 a() 修改为静态同步方法。 — 静态同步方法 a() 锁住的是 Sample5.class普通同步方法 b() 锁住的是实例对象。锁的对象不同,则并行执行
@Slf4j
class Sample5 {

    /**
     * 静态同步方法 a。
     */

    public static synchronized void a() {
        try {
            // 睡眠1s。
            TimeUnit.SECONDS.sleep(1);
            log.debug("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 普通同步方法 b。
     */

    public synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {
        
        Sample5 sample5 = new Sample5();
        new Thread(() -> {sample5.a();}).start();
        new Thread(() -> {sample5.b();}).start();
        // b a
    }
}
  • 锁案例6a()b() 均为静态同步方法。 — 共享一把 Sample6.class 对象锁,同步执行。
@Slf4j
class Sample6 {

    /**
     * 静态同步方法 a。
     */

    public static synchronized void a() {
        try {
            // 睡眠1s。
            TimeUnit.SECONDS.sleep(1);
            log.debug("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 静态同步方法 b。
     */

    public static synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {

        new Thread(Sample6::a).start();
        new Thread(Sample6::b).start();
        // a b
    }
}
  • 锁案例7:通过不同实例对象分别调用静态同步方法 a()普通同步方法 b() — 锁对象不同,并行执行。
@Slf4j
class Sample7 {

    /**
     * 静态同步方法 a。
     */

    public static synchronized void a() {
        try {
            // 睡眠1s。
            TimeUnit.SECONDS.sleep(1);
            log.debug("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 普通同步方法 b。
     */

    public synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {

        Sample7 sample1 = new Sample7();
        Sample7 sample2 = new Sample7();
        new Thread(() -> {sample1.a();}).start();
        new Thread(() -> {sample2.b();}).start();
        // b a
    }
}
  • 锁案例8:通过不同实例对象调用静态同步方法 a()b() — 同时锁住 Sample8.class 对象,同步执行。
@Slf4j
class Sample8 {

    /**
     * 静态同步方法 a。
     */

    public static synchronized void a() {
        try {
            // 睡眠1s。
            TimeUnit.SECONDS.sleep(1);
            log.debug("a");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 静态同步方法 b。
     */

    public static synchronized void b() {
        log.debug("b");
    }

    public static void main(String[] args) {

        Sample8 sample1 = new Sample8();
        Sample8 sample2 = new Sample8();
        new Thread(() -> {sample1.a();}).start();
        new Thread(() -> {sample2.b();}).start();
        // a b
    }
}

三、变量的线程安全

3.1 成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全

  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:

    • 如果只有操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

3.2 成员变量竞争示例

@Slf4j
class ThreadUnsafe {

    /**
     * 成员变量列表。
     */

    private ArrayList<String> list = new ArrayList<>();

    /**
     * 添加元素方法。
     */

    private void add() {
        this.list.add("element");
    }

    /**
     * 删除列表中第一个元素方法。
     */

    private void removeFirst() {
        this.list.remove(0);
    }

    /**
     * 使添加与删除操作互相竞争的方法。
     */

    public void contest() {
        for (int i = 0; i < 3000; i++) {
            // 临界区。
            this.add();
            this.removeFirst();
        }
    }

    public static void main(String[] args) {
        ThreadUnsafe unsafe = new ThreadUnsafe();
        // 创建两个线程执行。
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                log.debug(Thread.currentThread().getName());
                unsafe.contest();
                // Exception in thread "t_0" java.lang.IndexOutOfBoundsException
            }, "t_" + i).start();
        }
    }
}
  • 示意图

JUC-管程_第5张图片

  • 分析说明
    • removeFirst()add() 引用的都是同一个 list 成员变量。
    • 当线程执行发生竞争时,add() 还没来得及添加上元素,removeFirst() 去通过索引移除元素就会抛 IndexOutOfBoundsException 异常。

3.3 局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必:

    • 如果该对象没有逃离方法的作用范围访问,它是线程安全的。
    • 如果该对象逃离方法的作用范围需要考虑线程安全

3.4 局部变量示例

将上述例子进行调整,去掉成员变量 list ,并在 contest() 方法中创建成员变量add()removeFirst() 方法接收传入的list进行操作。

@Slf4j
class ThreadSafe {

    /**
     * 添加元素方法。
     *
     * @param list 列表
     */

    private void add(ArrayList<String> list) {
        list.add("element");
    }

    /**
     * 删除列表中第一个元素方法。
     *
     * @param list 列表
     */

    private void removeFirst(ArrayList<String> list) {
        list.remove(0);
    }

    /**
     * 使添加与删除操作互相竞争的方法。
     */

    public void contest() {
        // 创建成员变量列表。
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 3000; i++) {
            // 临界区。
            add(list);
            removeFirst(list);
        }
    }

    public static void main(String[] args) {
        ThreadSafe safe = new ThreadSafe();
        // 创建两个线程执行。
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                log.debug(Thread.currentThread().getName());
                safe.contest();
            }, "t_" + i).start();
        }
    }
}
  • 示意图

JUC-管程_第6张图片

  • 分析说明
    • list 是局部变量,每个线程调用时会创建其不同实例没有共享
    • add()removeFirst() 方法接收的对象都是通过 contest() 方法传递过来的,也就是说操作的都是同一个对象。

3.5 暴露引用会出现的问题

将上述示例中 ThreadSafe 类中的 removeFirst() 方法修改为 public 修饰符。

  • 问题示例
@Slf4j
class SubClass extends ThreadSafe {

    /**
     * 【覆盖】删除列表第一个元素的方法。
     *
     * @param list 列表
     */

    @Override
    public void removeFirst(ArrayList<String> list) {
        // 【创建了新的线程访问到了 list 对象,此时的 list 相当于成了共享资源。】
        new Thread(() -> list.remove(0)).start();
    }

    public static void main(String[] args) {
        // 创建子类实例。
        SubClass sc = new SubClass();
        // 创建两个线程执行。
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                log.debug(Thread.currentThread().getName());
                // 调用父类方法。
                sc.contest();
                // java.lang.IndexOutOfBoundsException
            }, "t_" + i).start();
        }
    }
  • 代码优化(将读写操作方法改为 private 修饰,将公共访问方法加上 final 修饰。):
    private void add(ArrayList<String> list) {
        list.add("element");
    }

    private void removeFirst(ArrayList<String> list) {
        list.remove(0);
    }

    public final void contest() {
        // 创建成员变量列表。
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 3000; i++) {
            // 临界区。
            add(list);
            removeFirst(list);

        }
    }
  • 总结
    • privatefinal 修饰符对多线程下的变量安全问题是有意义的
    • 它们对访问或修改进行关闭,则能在一定程度上保护我们的方法不受其他线程影响。

3.6 常见线程安全类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。即它们的每个方法都是原子的,但是需要注意的是它们多个方法组合不是原子的。

  • String
  • Integer
  • StringBuffffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类

四、Monitor 概念

4.1 概述

  • Monitor 被翻译为监视器管程
  • 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。

4.2 Java 对象头

由于 Java 面向对象的思想,在 JVM 中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

  • HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。即 JAVA 对象 = 对象头 + 实例数据 + 对象填充。

  • 而对象头又由两部分组成:一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针,及对象指向它的类元数据的指针。

  • 64 位虚拟机 Mark Word

|-----------------------------------------------------------------------------------------------------------------|
|                                             Object Header(128bits)                                              |
|-----------------------------------------------------------------------------------------------------------------|
|                                   Mark Word(64bits)               |  Klass Word(64bits)    |      State         |
|-----------------------------------------------------------------------------------------------------------------|
|    unused:25|identity_hashcode:31|unused:1|age:4|biase_lock:0| 01 | OOP to metadata object |      Nomal         |
|-----------------------------------------------------------------------------------------------------------------|
|    thread:54|      epoch:2       |unused:1|age:4|biase_lock:1| 01 | OOP to metadata object |      Biased        |
|-----------------------------------------------------------------------------------------------------------------|
|                        ptr_to_lock_record:62                 | 00 | OOP to metadata object | Lightweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                       ptr_to_heavyweight_monitor:62          | 10 | OOP to metadata object | Heavyweight Locked |
|-----------------------------------------------------------------------------------------------------------------|
|                                                              | 11 | OOP to metadata object |    Marked for GC   |
|-----------------------------------------------------------------------------------------------------------------|
  • Mark Word说明
标记字 状态
unused(25bit)| 对象的hashcode值(31bit)| unused(1bit)| 分代年龄(4bit) | 是否偏向锁(0)| 锁标志位 (01) 无锁态
线程ID(54bit) | Epoch(2bit) | unused(1bit)| 分代年龄(4bit) | 是否偏向锁(1)| 锁标志位 (01) 偏向锁
指向栈中锁记录的指针(ptr_to_lock_record 62bit) | 锁标志位 (00) 轻量级锁
指向管程 Monitor 的指针(ptr_to_heavyweight_monitor 62bit) | 锁标志位 (10) 重量级锁
空 | 锁标志位 (11) GC标记
  • 对象的哈希码值identity_hashcode):运行期间调用 System.identityHashCode() 来延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode 会被转移到 Monitor 中。
  • 分代年龄age):表示对象被 GC 的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  • 是否偏向锁biased_lock):由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • 锁标志位lock):区分锁状态,11时表示对象待 GC 回收状态,只有最后2位锁标识(11)有效。
  • 偏向锁的线程IDthread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的 ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
  • 偏向性标识epoch):偏向锁在 CAS 锁操作过程中,表示对象更偏向哪个锁。
  • 轻量级锁状态下,指向栈中锁记录的指针ptr_to_lock_record):当锁获取是无竞争的时,JVM 使用原子操作而不是 OS 互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM 通过 CAS 操作在对象的标题字中设置指向锁记录的指针。
  • 重量级锁状态下,指向对象监视器 Monitor 的指针ptr_to_heavyweight_monitor):如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM 在对象的 ptr_to_heavyweight_monitor 设置指向 Monitor 的指针。

4.3 Monitor 原理

  • 示意图

JUC-管程_第7张图片

  • 分析说明

    • 刚开始 MonitorOwnernull
    • 当线程-1执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为线程-1,Monitor只能有一个 Owner
    • 在线程-1上锁的过程中,如果有其他线程执行 synchronized(obj) 则会进入 EntryList 此时为阻塞状态;
    • 线程-1执行完同步代码块的内容后,唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。
    • 图中 WaitSet 是用于存放已经获得了锁的线程(因为缺少某些外部条件,而无法继续进行下去)。
  • 注意

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

五、synchronized 原理

5.1 字节码指令

注意:方法级别的 synchronized 不会在字节码指令中有所体现。

  • 代码示例
public class SynchronizedSample {

    private static final Object obj = new Object();
    private static int counter = 0;

    public static void main(String[] args) {
        synchronized (obj) {
            counter++;
        }
    }
}
  • 对应字节码及说明
 0 getstatic #2      // <- lock引用 (synchronized开始)
 3 dup
 4 astore_1          // lock引用 -> slot 1
 5 monitorenter      // 将 lock对象 MarkWord 置为 Monitor 指针
 6 getstatic #3      // <- i
 9 iconst_1          // 准备常数 1
10 iadd				 // +1
11 putstatic #3      // -> i
14 aload_1			 // <- lock引用
15 monitorexit 		 // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16 goto 24 (+8)
19 astore_2 		 // e -> slot 2
20 aload_1 			 // <- lock引用
21 monitorexit 		 // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22 aload_2 			 // <- slot 2 (e)
23 athrow 			 // throw e
24 return

5.2 轻量级锁

  • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized
  • 假设有两个方法同步块,利用同一个对象加锁(代码示例如下):
public class SynchronizedSample {

    private static final Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            // 同步块 A 
            method2();
        }
    }

    public static void method2() {
        synchronized (obj) {
            // 同步块 B
        }
    }
}
  • 加锁过程如下:
    • 步骤一创建锁记录Lock Record对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word
    • 步骤二:让锁记录中 Object reference 指向锁对象,并尝试cas 替换 ObjectMark Word,将 Mark Word 的值存入锁记录
    • 步骤三:如果 cas 替换成功,对象头中存储了锁记录地址和状态 00 ,表示由该线程给对象加锁

JUC-管程_第8张图片

  • 上述步骤二 cas 可能会替换失败,这时候又有两种情况
    • 情况一:如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争进入锁膨胀过程。
    • 情况二:如果是自己执行synchronized重入,那么再添加一条 Lock Record 作为重入的计数。

JUC-管程_第9张图片

  • 当退出 synchronized 代码块(解锁时)如果有取值 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

JUC-管程_第10张图片

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 casMark Word值恢复给对象头
    • 成功,则解锁成功。
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

5.3 锁膨胀

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

  • 代码示例

public class SynchronizedSample {

    private static final Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            // 同步块 
        }
    }
}
  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。

JUC-管程_第11张图片

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程:
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址。
    • 然后自己进入 MonitorEntryList BLOCKED

JUC-管程_第12张图片

  • 当 Thread-0 退出同步块解锁时,使用 casMark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Ownernull,唤醒 EntryListBLOCKED 线程。

5.4 自旋优化

  • 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
  • 自旋会占用 cpu 时间,单核 cpu 自旋就是浪费,多核 cpu 自旋才能发挥优势
  • Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能。

5.5 偏向锁

  • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 cas 操作。

  • Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 cas 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争不用重新 cas ,以后只要不发生竞争,这个对象就归该线程所有。

  • 代码示例

public class SynchronizedSample {

    private static final Object obj = new Object();

    public static void method1() {
        synchronized (obj) {
            // 同步块 A
            method2();
        }
    }

    public static void method2() {
        synchronized (obj) {
            // 同步块 B
            method3();
        }
    }

    public static void method3() {
        synchronized (obj) {
            // 同步块 C
        }
    }
}
  • 偏向锁优化示意图

JUC-管程_第13张图片

  • 其他补充
    • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 threadepochage 都为 0。
    • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟。
    • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcodeage 都为 0,第一次用到 hashcode才会赋值

六、wait notify

6.1 API 介绍

方法 功能
wait() 释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无时限等待,直到 notify 为止。
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
notify() object 上正在 waitSet 等待的线程中挑一个唤醒
notifyAll() object 上正在 waitSet 等待的线程全部唤醒
  • 代码示例
@Slf4j
public class WaitNotifySample {

    private static final Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            // 对象锁。
            synchronized (obj) {
                log.debug("t1 run...");
                try {
                    // 线程等待。
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("t1 wake,do other things...");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("t2 run...");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("t2 wake,do other things...");
            }
        }, "t2").start();

        // 主线程等待2s后执行。
        TimeUnit.SECONDS.sleep(2);
        log.debug("wake obj thread");
        // 通过同一把对象锁去唤醒。
        synchronized (obj) {
            // 若此处调用 notifyAll() 则唤醒当前对象锁的所有线程。
            obj.notify();
            // 执行结果如下:
            // t1 run...
            // t2 run...
            // wake obj thread
            // t1 wake,do other things...
        }
    }
}

6.2 wait notify 原理

  • 示意图

JUC-管程_第14张图片

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。
  • BLOCKEDWAITING 的线程都处于阻塞状态,不占用 cpu 时间片。
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒。
  • WAITING 线程会在 Owner 线程调用 notifynotifyAll 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList 重新竞争。

6.3 Sleep(long n) 与 wait(long n)的区别

  • sleep()Thread 方法,而 wait()Object 的方法。
  • sleep() 不需要强制和 synchronized 配合使用,但 wait() 需要和 synchronized 一起用。
  • sleep() 在睡眠的同时,不会释放对象锁的,但 wait() 在等待的时候会释放对象锁。
  • 它们的状态都是 TIMED_WAITING

6.4 正确使用方式

  • 解决某些场景下,使用 sleep() 一直阻塞导致执行效率太低的场景,此时就可以采用 wait-notify 机制。

  • notify() 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,这种情况称之为“虚假唤醒”。故需要 notifyAll() 进行唤醒。

  • notifyAll() 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了,故需要 while 进行循环判断。

  • 最终模板如下

synchronized(lock){
    while(条件不成立){
        lock.wait();
    }
    // 干活
}

// 另一个线程
synchronized(lock){
    lock.notifyAll();
}

6.5 保护性暂停模式

  • 概念:即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,它属于同步模式。
  • 要点:有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject
  • jdk 中,join 的实现、Future 的实现,采用的就是此模式。
  • 示意图
               +- - - - - - - - - +
               ' GuardedObject:   '
               '                  '
+----+  wait   ' +--------------+ '  task finish notify t1   +----+
| t1 | ------> ' |   response   | ' <----------------------- | t2 |
+----+         ' +--------------+ '                          +----+
               '                  '
               +- - - - - - - - - +

  • 代码示例
@Slf4j
public class GuardedObject {

    private Object response;

    /**
     * 获取响应对象。
     *
     * @return {@link Object}
     */

    public Object getResponseObj() {
        synchronized (this) {
            // 不满足条件,一直等待,避免虚假唤醒。
            while (null == response) {
                try {
                    log.debug("{}: waiting...", Thread.currentThread().getName());
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    /**
     * 设置响应对象后进行通知。
     *
     * @param response 响应
     */

    public void setResponseObjAndNotify(Object response) {
        synchronized (this) {
            this.response = response;
            log.debug("{}: notify all thread", Thread.currentThread().getName());
            this.notifyAll();
        }
    }

    /**
     * 模拟业务下载。
     *
     * @return {@link List}<{@link String}>
     */

    public List<String> download() {
        try {
            // 模拟执行耗时 1s。
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 返回空集合。
        return Collections.emptyList();
    }

    public static void main(String[] args) throws InterruptedException {

        GuardedObject gObj = new GuardedObject();

        new Thread(() -> {
            // t1 线程 waiting。
            Object response = gObj.getResponseObj();
            log.debug("t1: get response {}", response);
        }, "t1").start();


        new Thread(() -> {
            // t2 线程执行完任务后为对象赋值,再唤醒 t1 线程。
            List<String> response = gObj.download();
            log.debug("t2: download finish");
            gObj.setResponseObjAndNotify(response);
        }, "t2").start();

        // t1: waiting...
        // t2: download finish
        // t2: notify all thread
        // t1: get response []
    }
}
  • 但是上述代码示例有个问题,执行下载的业务如果卡主,t2 线程一直未response 进行赋值那么就会导致 t1 线程一直阻塞

  • 接下来,我们加入一个超时机制,为 getResponseObj() 方法添加一个超时时间,如果超过了超时时间,就算是还没有结果,也进行返回,不再阻塞

  • 改造后代码示例如下

@Slf4j
public class GuardedObject {

    private Object response;

    /**
     * 获取响应对象。
     *
     * @param timeout 超时时间。
     * @return {@link Object}
     */

    public Object getResponseObj(long timeout) {
        synchronized (this) {
            // 记录最初时间。
            long begin = System.currentTimeMillis();
            // 已经经过的时间。
            long passTime = 0;
            while (null == response) {

                // 计算等待了多长时间。(假设 timeout 是 1000,结果在 400 时唤醒了,那么还有 600 要等)
                long waitTime = timeout - passTime;
                if (waitTime <= 0) {
                    log.debug("waitTime <= 0,break");
                    break;
                }

                try {
                    log.debug("{}: waiting...", Thread.currentThread().getName());
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 求出已经经历的时间。
                passTime = System.currentTimeMillis() - begin;
                boolean flag = response == null;
                log.debug("timePassed: {}ms, get object is null ={}", passTime, flag);
            }
            return response;
        }
    }

    /**
     * 设置响应对象后进行通知。
     *
     * @param response 响应
     */

    public void setResponseObjAndNotify(Object response) {
        synchronized (this) {
            this.response = response;
            log.debug("{}: notify all thread", Thread.currentThread().getName());
            this.notifyAll();
        }
    }

    /**
     * 模拟业务下载。
     *
     * @return {@link List}<{@link String}>
     */

    public List<String> download() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }

    public static void main(String[] args) throws InterruptedException {

        GuardedObject gObj = new GuardedObject();

        new Thread(() -> {
            // 设置等待时限 2s。
            Object response = gObj.getResponseObj(2000L);
            if (null != response) {
                log.debug("t1: get response {}", response);
            } else {
                log.debug("can't get response");
            }
        }, "t1").start();


        new Thread(() -> {
            try {
                // 假设此时任务线程卡主,一直未赋值 response。
                TimeUnit.MILLISECONDS.sleep(3000L);
                gObj.setResponseObjAndNotify(null);
                
                // 卡了3s后才开始执行下载逻辑。
                List<String> response = gObj.download();
                log.debug("t2: download finish");
                gObj.setResponseObjAndNotify(response);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();

        // t1: waiting...
        // t2: notify all thread
        // timePassed: 3001ms, get object is null =true
        // waitTime <= 0,break
        // can't get response
        // 2: download finish
        // t2: notify all thread
    }
}

6.6 生产者消费者模式

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据。
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
  • JDK 中各种阻塞队列,采用的就是这种模式。
  • 示意图

JUC-管程_第15张图片

  • 代码示例如下
public class ProducerAndConsumerSample {
    
    @AllArgsConstructor
    @Getter
    static class Message {
        private int id;
        private Object message;
    }


    @Slf4j
    static final class MessageQueue {

        /**
         * 消息队列。
         */
        
        private final LinkedList<Message> queue;

        /**
         * 容量。
         */
        
        private final int capacity;

        public MessageQueue(int capacity) {
            this.capacity = capacity;
            queue = new LinkedList<>();
        }
        
        public Message take() {
            synchronized (queue) {
                // 没消息等待。
                while (queue.isEmpty()) {
                    log.debug("No messages in the queue,wait...");
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 有消息就出列,并通知其他线程。
                Message message = queue.removeFirst();
                queue.notifyAll();
                return message;
            }
        }
        
        public void put(Message message) {
            synchronized (queue) {
                // 消息个数如果等于指定容量就等待。
                while (queue.size() == capacity) {
                    log.debug("there are too many message,wait...");
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 添加消息,并通知。
                queue.addLast(message);
                queue.notifyAll();
            }
        }

        public static void main(String[] args) {

            // 创建指定容量为 1 的 mq 对象。
            MessageQueue mq = new MessageQueue(1);

            // 创建3个生产者线程。
            for (int i = 0; i < 3; i++) {

                int id = i + 1;
                String threadId = "thread-" + id;

                new Thread(() -> {

                    log.debug("({}) ,try put message", threadId);
                    mq.put(new Message(id, threadId + " message info"));
                    log.debug("({}) ,put message success", threadId);

                }, "producer_" + threadId).start();

            }

            // 消费者线程。
            new Thread(() -> {
                while (true) {
                    Message msg = mq.take();
                    log.debug("(Thread-4) get message:" + msg.getMessage());
                }
            }, "consumer_thread-4").start();


            // 某次运行结果如下:
            // No messages in the queue,wait...
            // (thread-2) ,try put message
            // (thread-3) ,try put message
            // (thread-1) ,try put message
            // (thread-2) ,put message success
            // (thread-1) ,put message success
            // there are too many message,wait...
            // (Thread-4) get message:thread-2 message info
            // (Thread-4) get message:thread-1 message info
            // (thread-3) ,put message success
            // (Thread-4) get message:thread-3 message info
            // No messages in the queue,wait...
        }
    }
}

七、Park & Unpark

7.1 基本使用

  • 它们是 LockSupport 类中的方法。
// 暂停当前线程。
LockSupport.park(); 

// 恢复某个线程的运行。(暂停线程对象)
LockSupport.unpark(Thread thread);
  • 代码示例
@Slf4j
public class ParkAndUnpark {

    /**
     * 测试先 park 再 unpark 场景。
     *
     * @throws InterruptedException 中断异常
     */

    @Test
    public void testParkThenUnpark() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");

        t1.start();

        TimeUnit.MILLISECONDS.sleep(5);

        log.debug("unpark...");
        LockSupport.unpark(t1);
        t1.join();
        // park...
        // unpark...
        // resume...
    }

    /**
     * 测试先 unpark 再 park 场景。
     *
     * @throws InterruptedException 中断异常
     */
    
    @Test
    public void testUnparkThenPark() throws InterruptedException {

        Thread t2 = new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(3);
                log.debug("park...");
                LockSupport.park();
                log.debug("resume...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");

        t2.start();

        log.debug("unpark...");
        LockSupport.unpark(t2);
        t2.join();
        // unpark...
        // park...
        // resume...
    }
}

7.2 与 wait notify 比较

park() & unpark()Objectwait() & notify() 相比较。

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

7.3 park unpark 原理

  • 每个线程都有自己的一个 Parker 对象,由三部分组成 _counter_cond_mutex

  • 情况一(先调用 park(),再调用 unpark()):

JUC-管程_第16张图片

  • park()

    1. 当前线程调用 Unsafe.park() 方法;
    2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁;
    3. 线程进入 _cond 条件变量阻塞;
    4. 设置 _counter = 0
  • unpark(Thread thread)

    1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1;
    2. 唤醒 _cond 条件变量中的 Thread_0;
    3. Thread_0 恢复运行;
    4. 设置 _counter 为 0 。
  • 情况二(先调用 unpark(),再调用 park()):

JUC-管程_第17张图片

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1;
  2. 当前线程调用 Unsafe.park() 方法;
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行;
  4. 设置 _counter 为 0 。

八、多把锁

8.1 说明及代码示例

假设我们有两个互不相干的任务,而它们都使用同一把对象锁,那么并发度就很低。

  • 代码示例
    static class SameLock {

        private void task1() throws InterruptedException {
            synchronized (this) {
                TimeUnit.SECONDS.sleep(2);
            }
        }

        private void task2() throws InterruptedException {
            synchronized (this) {
                TimeUnit.SECONDS.sleep(3);
            }
        }

        public static void main(String[] args) throws InterruptedException {

            SameLock lock = new SameLock();

            Thread t1 = new Thread(() -> {
                try {
                    lock.task1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1");

            Thread t2 = new Thread(() -> {
                try {
                    lock.task2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t2");

            Instant start = Instant.now();
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            Instant end = Instant.now();
            log.debug("task1 and task2 spend time:{}s", Duration.between(start, end).toSeconds());
            // task1 and task2 spend time:5s
        }
    }
  • 优化为多把锁(代码示例如下):
    static class DifferentLock {

        private final Object lock1 = new Object();
        private final Object lock2 = new Object();

        private void task1() throws InterruptedException {
            synchronized (lock1) {
                TimeUnit.SECONDS.sleep(2);
            }
        }

        private void task2() throws InterruptedException {
            synchronized (lock2) {
                TimeUnit.SECONDS.sleep(3);
            }
        }

        public static void main(String[] args) throws InterruptedException {

            DifferentLock lock = new DifferentLock();

            Thread t1 = new Thread(() -> {
                try {
                    lock.task1();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t1");

            Thread t2 = new Thread(() -> {
                try {
                    lock.task2();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "t2");

            Instant start = Instant.now();
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            Instant end = Instant.now();
            log.debug("task1 and task2 spend time:{}s", Duration.between(start, end).toSeconds());
            // task1 and task2 spend time:3s
        }
    }

8.2 优点与缺点

  • 优点:将锁的粒度细分,可以增强并发度
  • 缺点:如果一个线程需要同时获得多把锁,就容易发生死锁

九、活跃性

9.1 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。

  • 假设: t1 线程已有对象锁A,而 t2 线程已有对象锁B,此时 t1 准备获取锁B,t2 准备获取锁A。
  • 代码示例
@Slf4j
public class DeadLockSample {

    public static void main(String[] args) {

        Object lockA = new Object();
        Object lockB = new Object();

        new Thread(()->{
            synchronized (lockA){
                try {
                    log.debug("t1 lock A");
                    TimeUnit.SECONDS.sleep(1);
                    synchronized (lockB){
                        log.debug("t1 lock B");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();

        new Thread(()->{
            synchronized (lockB){
                try {
                    log.debug("t2 lock lockB");
                    TimeUnit.SECONDS.sleep(1);
                    synchronized (lockA){
                        log.debug("t2 lock lockA");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t2").start();

        // t1 lock A
        // t2 lock lockB
        // 程序一直处于运行中状态...
    }
}
  • 定位死锁问题

检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁。

# jvm进程查看
$ jps

# 线程快照分析
$ jstack id

JUC-管程_第18张图片

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 cpu 占用高的 Java 进程,再利用 top -Hp Pid 来定位是哪个线程,最后再用 jstack 排查。

9.2 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。

  • 代码示例
@Slf4j
public class LiveLockSample {

    private static volatile int count = 10;

    public static void main(String[] args) {

        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                    count--;
                    log.debug("count: {}", count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                try {
                    TimeUnit.MILLISECONDS.sleep(1);
                    count++;
                    log.debug("count: {}", count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
        
        // 一直运行....
    }
}

9.3 饥饿

如果线程优先级”不均“,在 cpu 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。

  • 解决方案
    • 方案一:保证资源充足。
    • 方案二:公平地分配资源。
    • 方案三:避免持有锁的线程长时间执行。

十、ReentrantLock

10.1 概述

  • 它与 synchronized 一样,都支持可重入。但是相对于 synchronized 它还具备如下特点:

    • 可中断
    • 可以设置超时时间
    • 可以设置为公平锁
    • 支持多个条件变量
  • 基本语法

// 获取锁
reentrantLock.lock();

try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

10.2 可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。

  • 代码示例
@Slf4j
public class ReentrantLockSample {

    static ReentrantLock lock = new ReentrantLock();

    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        method1();
        // execute method1
        // execute method2
        // execute method3
    }
}

10.3 可打断

  • 直接中断模式
@Slf4j
public class ReentrantLockSample {

    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {

            log.debug("start...");

            try {
                // 中断。
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("is interrupted!");
                return;
            }

            try {
                log.debug("get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("get lock");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            log.debug("run interrupt");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        
        
        // [main] get lock
        // [t1] start...
        // [main] run interrupt
        // [t1] is interrupted!
        // java.lang.InterruptedException
    }
}
  • 不可中断模式
@Slf4j
public class ReentrantLockSample {

    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            try {
                log.debug("start...");
                lock.lock();
                log.debug("get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("get lock");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            t1.interrupt();
            log.debug("run interrupt");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            log.debug("unlock");
            lock.unlock();
        }
        
        
        // [main] get lock
        // [t1] start...
        // [main] run interrupt
        // [main] unlock
        // [t1] get lock
    }
}

10.4 锁超时

  • 失败后,立刻返回
@Slf4j
public class ReentrantLockSample {

    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {

            log.debug("start...");

            if (!lock.tryLock()) {
                log.debug("get lock fail, return now!");
                return;
            }
            try {
                log.debug("get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("get lock");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        // [main] get lock
        // [t1] start...
        // [t1] get lock fail, return now!
    }
}
  • 超时失败
@Slf4j
public class ReentrantLockSample {

    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            log.debug("start...");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("wait 1s ,get fail return");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                log.debug("get lock");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("get lock");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

        
        // [main] get lock
        // [t1] start...
        // -- 1s --
        // [t1] wait 1s ,get fail return
    }
}

10.5 公平锁

ReentrantLock 默认是不公平的。

  • 规则开启
ReentrantLock lock = new ReentrantLock(true);
  • 注意:公平锁一般没有必要,会降低并发度

10.6 条件变量

  • synchronized 中也有条件变量(当条件不满足时进入 waitSet 等待)。

  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的。

  • 使用要点

    • await() 前需要获得锁。
    • await() 执行后,会释放锁,进入 conditionObject 等待。
    • await() 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁。
    • 竞争 lock 锁成功后,从 await() 后继续执行。
  • 代码示例

@Slf4j
public class ReentrantLockSample {

    private static final ReentrantLock LOCK = new ReentrantLock();
    private static Condition condition1 = LOCK.newCondition();
    private static Condition condition2 = LOCK.newCondition();
    private static volatile boolean task1Finish = false;
    private static volatile boolean task2Finish = false;
    
    private static void doTask1() {
        LOCK.lock();
        try {
            log.debug("do task1...");
            task1Finish = true;
            condition1.signal();
        } finally {
            LOCK.unlock();
        }
    }

    private static void doTask2() {
        LOCK.lock();
        try {
            log.debug("do task2...");
            task2Finish = true;
            condition2.signal();
        } finally {
            LOCK.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            try {
                LOCK.lock();
                // 任务1未完成则等待。
                while (!task1Finish) {
                    try {
                        log.debug("t1 wait...");
                        condition1.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("task1 finish!");
            } finally {
                LOCK.unlock();
            }
        }, "t1").start();

        new Thread(() -> {
            try {
                LOCK.lock();
                // 任务2未完成则等待。
                while (!task2Finish) {
                    try {
                        log.debug("t2 wait...");
                        condition2.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("task2 finish!");
            } finally {
                LOCK.unlock();
            }
        }, "t2").start();

        TimeUnit.SECONDS.sleep(1);
        doTask1();

        TimeUnit.SECONDS.sleep(1);
        doTask2();

        // [t1] t1 wait...
        // [t2] t2 wait...
        // [main] do task1...
        // [t1] task1 finish!
        // [main] do task2...
        // [t2] task2 finish!
    }
}

十一、同步模式之顺序控制

11.1 固定顺序

需求:必须先 2 后 1 打印。

  • 此处使用:使用 LockSupport 类的 park()unpark() 来进行实现。

  • park()unpark() 方法比较灵活,它俩谁先调用,谁后调用无所谓。并且是以线程为单位进行暂停恢复,不需要同步对象和运行标记。

  • 代码示例

@Slf4j
public class SequenceControlSample {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
            LockSupport.park();
            log.debug("1");
        }, "t1");

        Thread t2 = new Thread(() -> {
            log.debug("2");
            // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
            LockSupport.unpark(t1);
        }, "t2");

        t1.start();
        t2.start();
        // [t2] 2
        // [t1] 1
    }
}

11.2 交替输出

需求:线程 t1 输出 a 5 次,线程 t2 输出 b 5 次,线程 t3 输出 c 5 次,现在要求输出 abcabcabcabcabc 。

  • 代码示例(此处使用 wait - notify 实现):
@Slf4j
public class SyncWaitNotify {

    private int flag;
    private final int loopNumber;

    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }

    public void print(int waitFlag, int nextFlag, String str) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(() -> syncWaitNotify.print(1, 2, "a")).start();
        new Thread(() -> syncWaitNotify.print(2, 3, "b")).start();
        new Thread(() -> syncWaitNotify.print(3, 1, "c")).start();
        // abcabcabcabcabc
    }
}

十二、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

你可能感兴趣的:(JUC,java)