并发编程:(三)共享模型之管程

一、共享问题

1、共享带来的问题

多线程的环境下,由于多个公共资源可能会被多个线程共享,也就是多个线程可能会操作(增、删、改等)同一资源。当多个线程操作同一资源时,很容易导致数据错乱,或发生数据安全问题(数据有可能丢失,有可能增加,有可能错乱)。

2、Java的体现

两个线程对初始值为 0 的静态变量一个做自增,一个自减,各做5000次

public class Test11 {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        },"t1");
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        },"t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count的结果:" + count);
    }
}

说明:

结果可能是正数、负数、零。

原因: Java 中对静态变量的自增、自减并不是原子操作。

3、临界区 Critical Section

一个程序运行多个线程本身没有问题的

问题出在多个线程访问 共享资源

  • 多个线程读 共享资源 其实也没有问题
  • 在多个线程对 共享资源 读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对 共享资源 的多线程读写操作,称这段代码块为 临界区

static int count = 0;
// 临界区
static void increment(){
  	count++;
}
// 临界区
static void decrement(){
  	count--;
}

4、竟态条件 Race Condition

多个线程在临界区内执行,由于代码的 执行序列不同 而导致结果无法预测,称之为发生了 竟态条件

二、synchronized 解决方案

1、synchronized介绍

synchronized 是 Java 中的关键字,是一种同步锁。在多线程开发中经常会使用到这个关键字,其主要作用是可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,同时保证一个线程操作的数据变化被其他线程所看到。

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

2、线程同步

在现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。如(礼品发放,每个人都想获得,天然的解决方案就是,在派发前,排队,前一个人领取完成后,后一个人再领取)。

3、线程同步两个条件

  1. 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的 等待池形成队列 ,等待前面的线程使用完毕后,下一个线程再使用。

  2. 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题。为了保证数据在方法中被访问时的正确性,在访问时加入 锁机制(synchronized) ,当一个线程获得对象的排它 ,独占资源,其他线程必须等待,使用后释放锁即可。

    存在问题:

    一个线程持有锁会导致其他所有需要此锁的线程挂起。

    在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

    如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。

4、解决问题

为了避免临界区的竟态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronizedLock
  • 非阻塞式的解决方案:原子变量

注意:

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

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

5、同步方法

只需在方法上加 synchronized 关键字

非静态的同步方法的锁对象是 this, 静态的同步方法的锁对象是该类的字节码(类名.class),如果锁对象是引用数据类型的成员变量,必须是静态的。

缺陷:**若将一个大的方法声明为 synchronized 将会大大影响效率。 **

/**
 * 线程安全,数据无负数、无相同的情况
 * @author admin
 *
 */
public class Test01 {
    public static void main(String[] args) {
        //一份资源
        Web12306 t = new Web12306();
        System.out.println(Thread.currentThread().getName());
        //多个代理
        new Thread(t,"黄牛1").start();
        new Thread(t,"黄牛2").start();
        new Thread(t,"黄牛3").start();
    }
}
class Web12306 implements Runnable{
    private int num = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while(true) {
            Ticket();
        }
    }
    //加入synchronized关键字
    public synchronized void Ticket() {
        if (num <= 0) {
            flag = false;
            return;
        }
        //模拟网络延迟,放大发生问题的可能性
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"抢到了,还有"+ --num +"张票");
    }
}

6、同步代码块

synchronized(obj){} :obj 称之为同步监视器

  • obj 可以是任何对象,但是推荐使用共享资源作为同步监视器。
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是 this 即该对象本身,或 类名.class
  • 一般来说是锁的要修改的那个对象。

同步监视器的执行过程:

  • 第一个线程访问,锁定同步监视器,执行其中代码。
  • 第二个线程访问,发现同步监视器被锁定,无法访问。
  • 第一个线程访问完毕,解锁同步监视器。
  • 第二个线程访问,发现同步监视器未上锁,就锁住并访问。
/**
 * 线程安全,数据无负数、无相同的情况
 * @author admin
 *
 */
public class Test01 {
    public static void main(String[] args) {
        //一份资源
        Web12306 t = new Web12306();
        System.out.println(Thread.currentThread().getName());
        //多个代理
        new Thread(t,"黄牛1").start();
        new Thread(t,"黄牛2").start();
        new Thread(t,"黄牛3").start();
    }
}
class Web12306 implements Runnable{
    private int num = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while(true) {
            Ticket();
        }
    }
    //尽可能锁定合理的范围(不是指代码,是指数据的完成性)
    public void Ticket() {
        //考虑没有票的情况下
        if (num <= 0) {
            flag = false;
            return;
        }
        synchronized (this){
            //考虑最后一张票
            if (num <= 0) {
                flag = false;
                return;
            }
            //模拟网络延迟,放大发生问题的可能性
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"抢到了,还有"+ --num +"张票");
        }
    }
}

三、变量的线程安全分析

1、成员变量和静态变量是否线程安全

  1. 如果它们没有共享,则线程安全。

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

    如果只有读操作,则线程安全。

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

2、局部变量是否线程安全

  1. 局部变量是线程安全的。

  2. 但局部变量引用的对象则未必

    如果该对象没有逃离方法的作用访问,它是线程安全的。

    如果该对象逃离方法的作用范围,需要考虑线程安全。

3、局部变量线程安全问题

public class Test01 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            test1();
        },"t1");
        Thread t2 = new Thread(()->{
            test1();
        },"t2");
        t1.start();
        t2.start();
    }

    public static void test1(){
        int i = 10;
        i++;
        System.out.println(i);
    }
}

说明: 每个线程调用 test1() 方法时局部变量 i ,会在每个线程的栈帧内存中被创建多份,因此不存在共享。

并发编程:(三)共享模型之管程_第1张图片

4、成员变量示例

public class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    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);
    }
}

调用:

public class Test01 {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;

    public static void main(String[] args) {
        ThreadUnsafe t = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(()->{
                t.method1(LOOP_NUMBER);
            },"thread" + i).start();
        }
    }
}

可能会出现这么一种可能,如果线程1还没有add,线程0的remove就会报错

Exception in thread "thread0" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.remove(ArrayList.java:496)
	at com.day22.ThreadUnsafe.method3(ThreadUnsafe.java:20)
	at com.day22.ThreadUnsafe.method1(ThreadUnsafe.java:11)
	at com.day22.Test01.lambda$main$0(Test01.java:11)
	at java.lang.Thread.run(Thread.java:748)

分析:

  • 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
  • method3 与mothod2 分析相同

并发编程:(三)共享模型之管程_第2张图片

将 list 修改为局部变量,就不会出现上面的问题了

public class ThreadUnsafe {
    public 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);
    }
}

分析:

  • list 是局部变量,每个线程调用时会创建不同实例,没有共享。
  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象。
  • method3 的参数分析与 method2 相同。

并发编程:(三)共享模型之管程_第3张图片

5、常见线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • HashTable
  • java.util.concurrent 包下的类

这里线程安全是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:

  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的

6、Monitor原理

Monitor 被翻译为 监视器管程

1、理解java对象头

  • 实例对象:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

  • 对象头的主要结构是由 Mark Word 和 Class Metadata Address 组成

    Mark Word 字段存储着锁的状态,轻量级锁指针,重量级锁指针。

    重量级锁也就是通常说 synchronized 的对象锁,锁标识位为10,其中指针指向的 monitor 对象(也称为管程或监视器锁)的起始地址。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。但当一个 monitor 被某个线程持有后,它便处于锁定状态。

64位Mark Word结构:

并发编程:(三)共享模型之管程_第4张图片
Monitor 结构如下:

并发编程:(三)共享模型之管程_第5张图片

说明:

  1. 刚开始 MonitorOwnernull
  2. 当 Thread-2 执行 synchronizd(obj) 就会将 Monitor 的所有者 Owner 设置为 Thread-2,Monitor 中只能有一个Owner。
  3. 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj) ,就会进入 EntryList BLOCKED。
  4. Thread-2 执行完成同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的。
  5. 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程。

注意:

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

四、synchronized进阶锁优化

1、轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized

假设有两个方法同步块。利用同一个对象加锁

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。01 :无锁。00:轻量级锁。

并发编程:(三)共享模型之管程_第6张图片

  • 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。

并发编程:(三)共享模型之管程_第7张图片

  • 如果 CAS 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。

并发编程:(三)共享模型之管程_第8张图片

  • 如果 CAS 失败,有两种情况:
    1. 如果是其他线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程。
    2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。

并发编程:(三)共享模型之管程_第9张图片

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

并发编程:(三)共享模型之管程_第10张图片

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

2、锁膨胀

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

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

并发编程:(三)共享模型之管程_第11张图片

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

并发编程:(三)共享模型之管程_第12张图片

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

3、自旋优化

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

自旋重试成功的情况

线程1(core 1 上) 对象 Mark Word 线程2(core 2 上)
- 10(重量锁) -
访问同步代码块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步代码块 10(重量锁)重量锁指针 -
执行同步代码块 10(重量锁)重量锁指针 访问同步代码块,获取 monitor
执行同步代码块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步代码块
-

自旋重试失败的情况

线程1(core 1 上) 对象 Mark Word 线程2(core 2 上)
- 10(重量锁) -
访问同步代码块,获取 monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步代码块 10(重量锁)重量锁指针 -
执行同步代码块 10(重量锁)重量锁指针 访问同步代码块,获取 monitor
执行同步代码块 10(重量锁)重量锁指针 自旋重试
执行同步代码块 10(重量锁)重量锁指针 自旋重试
执行同步代码块 10(重量锁)重量锁指针 自旋重试
执行同步代码块 10(重量锁)重量锁指针 阻塞
-

说明:

  1. 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  2. 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就减少自旋甚至不自旋,总之,比较智能。
  3. Java 7 之后不能控制是否开启自旋功能。

4、偏向锁

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

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

static final Object obj = new Object();
public static void m1(){
    synchronized(obj){
          //同步块A
          m2();
    }
}
public static void m2(){
    synchronized(obj){
          //同步块B
          m3();
    }
}
public static void m3(){
    synchronized(obj){
          //同步块C
    }
}

并发编程:(三)共享模型之管程_第13张图片

并发编程:(三)共享模型之管程_第14张图片

1、偏向状态

并发编程:(三)共享模型之管程_第15张图片

一个对象创建时:

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

测试延迟


<dependency>
    <groupId>org.openjdk.jolgroupId>
    <artifactId>jol-coreartifactId>
    <version>0.10version>
dependency>
public class TestBiased {
    public static void main(String[] args)  {
        Dog dog = new Dog();
        // 无锁
        System.out.println("synchronized前");
        System.out.println(ClassLayout.parseInstance(dog).toPrintable());
        synchronized (dog){
            System.out.println("synchronized中");
            // 偏向锁
            System.out.println(ClassLayout.parseInstance(dog).toPrintable());
        }
        // 偏向锁
        System.out.println("synchronized后");
        System.out.println(ClassLayout.parseInstance(dog).toPrintable());
    }
}
class Dog{

}

并发编程:(三)共享模型之管程_第16张图片

注意:

处于偏向锁的对象解锁后,线程 id 仍存储于对象头中。

测试禁用

在运行时添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

并发编程:(三)共享模型之管程_第17张图片

2、撤销 - 调用对象 hashCode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。

  • 轻量级锁会在锁记录中记录 hashCode。
  • 重量级锁会在 Monitor 中记录 hashCode。
public class TestBiased {
    public static void main(String[] args)  {
        Dog dog = new Dog();
        //会禁用这个对象的偏向锁
        dog.hashCode();
        // 无锁
        System.out.println("synchronized前");
        System.out.println(ClassLayout.parseInstance(dog).toPrintable());
        synchronized (dog){
            System.out.println("synchronized中");
            // 偏向锁
            System.out.println(ClassLayout.parseInstance(dog).toPrintable());
        }
        // 偏向锁
        System.out.println("synchronized后");
        System.out.println(ClassLayout.parseInstance(dog).toPrintable());
    }
}
class Dog{

}

并发编程:(三)共享模型之管程_第18张图片

3、撤销 - 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

public class TestBiased {
    public static void main(String[] args)  {
        Dog dog = new Dog();
        new Thread(()->{
            // 无锁
            System.out.println("synchronized前");
            System.out.println(ClassLayout.parseInstance(dog).toPrintable());
            synchronized (dog){
                System.out.println("synchronized中");
                // 偏向锁
                System.out.println(ClassLayout.parseInstance(dog).toPrintable());
            }
            // 偏向锁
            System.out.println("synchronized后");
            System.out.println(ClassLayout.parseInstance(dog).toPrintable());
            synchronized (TestBiased.class){
                TestBiased.class.notify();//唤醒
            }
        },"t1").start();


        new Thread(()->{
            synchronized (TestBiased.class){
                try {
                    TestBiased.class.wait();//等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 无锁
            System.out.println("synchronized前");
            System.out.println(ClassLayout.parseInstance(dog).toPrintable());
            synchronized (dog){
                System.out.println("synchronized中");
                // 偏向锁
                System.out.println(ClassLayout.parseInstance(dog).toPrintable());
            }
            // 偏向锁
            System.out.println("synchronized后");
            System.out.println(ClassLayout.parseInstance(dog).toPrintable());
        },"t2").start();
    }
}
class Dog{

}

并发编程:(三)共享模型之管程_第19张图片

4、批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 t1 的对象仍有机会重新偏向 t2,重偏向会重置对象的 Thread ID。

当撤销偏向锁阈值超过 20 次后,jvm会觉得,是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程。

public class TestBiased {
    public static void main(String[] args)  {
        Vector<Dog> list = new Vector<>();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 30; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    System.out.println(ClassLayout.parseInstance(d).toPrintable());
                }
            }
            synchronized (list) {
                list.notify();
            }
        },"t1");
        t1.start();


        Thread t2 = new Thread(()->{
            synchronized (list) {
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("===================================================>");
            for (int i = 0; i < 30; i++) {
                Dog d = list.get(i);
                System.out.println(ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    System.out.println(ClassLayout.parseInstance(d).toPrintable());
                }
            }
        },"t2");
        t2.start();
    }
}
class Dog{

}
5、批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会觉得,确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

public class TestBiased {
    static Thread t1,t2,t3;
    public static void main(String[] args) throws InterruptedException {
        Vector<Dog> list = new Vector<>();
        int loopNumber = 39;
        t1 = new Thread(()->{
            for (int i = 0; i < loopNumber; i++) {
                Dog d = new Dog();
                list.add(d);
                synchronized (d) {
                    System.out.println(ClassLayout.parseInstance(d).toPrintable());
                }
            }
            LockSupport.unpark(t2);
        },"t1");
        t1.start();


        t2 = new Thread(()->{
           LockSupport.park();
            System.out.println("=============================================>");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                System.out.println(ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    System.out.println(ClassLayout.parseInstance(d).toPrintable());
                }
            }
            LockSupport.unpark(t3);
        },"t2");
        t2.start();


        t3 = new Thread(()->{
            LockSupport.park();
            System.out.println("=============================================>");
            for (int i = 0; i < loopNumber; i++) {
                Dog d = list.get(i);
                System.out.println(ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    System.out.println(ClassLayout.parseInstance(d).toPrintable());
                }
            }
        },"t3");
        t3.start();
        t3.join();
      	//发现不会再偏向了
        System.out.println(ClassLayout.parseInstance(new Dog()).toPrintable());
    }
}
class Dog{

}

五、原理之synchronized进阶

1、wait & notify

1、原理:

并发编程:(三)共享模型之管程_第20张图片

说明:

  1. Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。
  2. BLOCKED 和 WAITING 的线程都处于阻塞状态,不占 CPU 时间片。
  3. BLOCKER 线程会在 Owner 线程释放锁时唤醒。
  4. WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后不意味着立刻获得锁,仍需进入 EntryList 重新竞争。

2、API介绍

obj.wait() :让进入 object 监视器的线程到 waitSet 等待。

obj.notify() :在 object 上正在 waitSet 等待的线程中挑一个唤醒。

obj.notifyAll() :让 object 上正在 waitSet 等待的线程全部唤醒。

总结: 它们都是线程间进行协作的手段,都属于 object 对象的方法,**必须获得此对象的锁,**才能调用这几个方法

public class TestBenchmark {
    final static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (obj) {
                System.out.println("执行...");
                try {
                    obj.wait();//让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("其他代码1...");
            }
        }).start();


        new Thread(()->{
            synchronized (obj) {
                System.out.println("执行...");
                try {
                    obj.wait();//让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("其他代码2....");
            }
        }).start();

        //主线程两秒后执行
        sleep(2000);
        System.out.println("唤醒 obj 上其他线程");
        synchronized (obj) {
            obj.notify();//唤醒obj上一个线程
//            obj.notifyAll();//唤醒obj上所有等待线程
        }
    }
}

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就有机会获取对象的锁。无限制等待,直到notify 为止。

wait(long n) 有时限的等待,等待 n 毫秒后结束等待,或者被 notify。

3、sleep 和 wait 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object的方法。
  2. sleep 不需要强制和 synchronized 配合使用。但 wait 需要和 synchronized 一起使用。
  3. sleep 在睡眠的时候不会释放对象锁,抱着锁睡觉,wait 在等待的时候会释放对象锁。
  4. 它们状态都是 TIMED_WAITING。

4、wait 和 notifyAll 正确使用格式

synchronized(lock) {
  	while(条件不成立){
    	lock.wait();
  	}
  	//干活
}
//另一个线程
synchronized(lock) {
    lock.notifyAll();
}

2、同步模式之保护性暂停

1、定义

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待。

  1. 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject。
  2. 如果有结果不断从一个线程到另一个线程那么可以使用消息队列。
  3. JDK 中,join 的实现,Future 的实现,采用的就是此模式。
  4. 因为要等待另一方的结果,因此归类到同步模式。

并发编程:(三)共享模型之管程_第21张图片

2、实现

class GuardedObject{
    //结果
    private Object response;

    //获取结果
    public Object get(){
        synchronized (this) {
            //条件不满足就等待
            while (response == null) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }
    }

    //产生结果
    public void complete(Object response) {
        synchronized (this) {
            //条件满足,唤醒等待的线程
            this.response = response;
            this.notifyAll();
        }
    }
}

3、应用

一个线程等待另一个线程的结果

public class Test03 {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            //等待结束
            System.out.println("开始...");
            List<String> list = (List<String>) guardedObject.get();
            System.out.println("结果大小:" + list.size());
        },"t1").start();

        new Thread(()->{
            System.out.println("执行...");
            List<String> list = new ArrayList<>();
            list.add("张三");
            list.add("李四");
            list.add("王五");
            //模拟延时等待3秒
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            guardedObject.complete(list);
        },"t2").start();
    }
}

执行结果:

开始...
执行...
结果大小:3

4、带超时版GuardedObject

public class Test03 {
    public static void main(String[] args) {
        GuardedObject guardedObject = new GuardedObject();
        new Thread(()->{
            //等待结束
            System.out.println("开始...");
            Object obj = guardedObject.get(2000);
            System.out.println("结果:" + obj);
        },"t1").start();

        new Thread(()->{
            System.out.println("执行...");
            //等待3秒
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            List<String> list = new ArrayList<>();
            list.add("张三");
            list.add("李四");
            list.add("王五");
            guardedObject.complete(list);
        },"t2").start();
    }
}
class GuardedObject{
    //结果
    private Object response;

    //获取结果
    public Object get(long timeout){
        synchronized (this) {
            //记录开始时间
            long begin = System.currentTimeMillis();
            //经历时间
            long passedTime = 0;
            //条件不满足就等待
            while (response == null) {
                //这一轮循环应该等待的时间
                long waitTime = timeout - passedTime;
                //经历时间超过最大等待时间时,退出循环
                if (waitTime <=0 ) {
                    break;
                }
                try {
                    this.wait(waitTime);//避免虚假唤醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //计算经历时间
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    //产生结果
    public void complete(Object response) {
        synchronized (this) {
            //条件满足,唤醒等待的线程
            this.response = response;
            this.notifyAll();
        }
    }
}

执行结果:

开始...
执行...
结果:null

5、join原理

是调用者轮询检查线程 alive 状态,join 体现的是保护性暂停模式

t1.join();

等价于下面代码

synchronized(t1) {
	//调用者线程进入 t1 的 waitSet 等待,直到 t1 运行结束
    while(t1.isAlive()){
    	t1.wait();
    }
}

join底层源码

public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();//记录当前时间
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
              	//本次循环需要等待的时间
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
              	//计算经历时间
                now = System.currentTimeMillis() - base;
            }
        }
    }

3、异步模式之生产者/消费者

1、定义要点

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

并发编程:(三)共享模型之管程_第22张图片

2、实现与应用

public class Test04 {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);
        //创建生产者
        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(()->{
                queue.put(new Message(id,"生产消息" + id));
            },"生产者" + i).start();
        }
        new Thread(()->{
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //消费消息
                Message take = queue.take();
            }
        },"消费者").start();
    }
}
class MessageQueue{
    //消息的队列集合
    private LinkedList<Message> list = new LinkedList<>();
    //队列容量
    private int capcity;

    public MessageQueue(int capcity) {
        this.capcity = capcity;
    }

    //获取消息
    public Message take(){
        synchronized (list) {
            //检查队列是否为空
            while (list.isEmpty()) {
                try {
                    System.out.println("【" + Thread.currentThread().getName() + "】 - 队列为空,消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //从队列头部获取消息并返回
            Message message = list.removeFirst();
            System.out.println("【" + Thread.currentThread().getName() + "】 - 已消费消息:" + message);
            list.notifyAll();//唤醒等待,每消费一个唤醒生产
            return message;
        }
    }

    //存入消息
    public void put(Message message) {
        synchronized (list) {
            //检查队列是否已满
            while (list.size() == capcity) {
                try {
                    System.out.println("【" + Thread.currentThread().getName() + "】 - 队列已满,生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将新消息加入队列尾部
            list.addLast(message);
            System.out.println("【" + Thread.currentThread().getName() + "】 - 已生产消息:" + message);
            list.notifyAll();
        }
    }
}

final class Message{
    private int id;
    private Object value;

    public Message(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

执行结果:

【生产者0】 - 已生产消息:Message{id=0, value=生产消息0}
【生产者2】 - 已生产消息:Message{id=2, value=生产消息2}
【生产者1】 - 队列已满,生产者线程等待
【消费者】 - 已消费消息:Message{id=0, value=生产消息0}
【生产者1】 - 已生产消息:Message{id=1, value=生产消息1}
【消费者】 - 已消费消息:Message{id=2, value=生产消息2}
【消费者】 - 已消费消息:Message{id=1, value=生产消息1}
【消费者】 - 队列为空,消费者线程等待

4、Park & Unpark

1、基本使用

它们是 LockSupport 类中的方法

//暂停当前线程
LockSupport.park();

//恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

2、实例

public class Test05 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("【" + Thread.currentThread().getName() + "】 - start...");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("【" + Thread.currentThread().getName() + "】 - park...");
            //上锁
            LockSupport.park();
            System.out.println("【" + Thread.currentThread().getName() + "】 - end...");
        },"t1");
        t1.start();
        sleep(2000);
        System.out.println("【" + Thread.currentThread().getName() + "】 - unpark...");
        //解锁
        LockSupport.unpark(t1);
    }
}

执行效果:

【t1】 - start...
【t1】 - park...
【main】 - unpark...
【t1】 - end...

3、特点

与 Object 的 wait & notify 相比:

  1. wait、notify 和 notifyAll 必须配合 Object Monitor(锁) 一起使用,而 park、unpark 不需要。
  2. park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】。
  3. park & unpark 可以先 unpark,而 wait & notify 不能先 notify。

4、原理

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

举例说明:

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0耗尽,1充足)。
  • 调用 park 就是要看需不需要停下来歇息
    1. 如果干粮耗尽,那么钻进帐篷休息。
    2. 如果干粮充足,那么不需要停留,继续前进。
  • 调用 unpark ,就好比干粮充足
    1. 如果这时线程还在帐篷,就唤醒让他继续前进。
    2. 如果这时线程还在运行,那么下次他调用 park 时,仅仅消耗干粮,不需要停留,继续前进。因为背包空间有限,每次调用 unpark 仅会补充一份干粮。

并发编程:(三)共享模型之管程_第23张图片

1.当前线程调用 Unsafe.park() 方法。

2.检查 _counter,若为0,这时,获得 _mutex 互斥锁。

3.线程进入 _cond 条件变量阻塞。

4.设置 _counter = 0。

并发编程:(三)共享模型之管程_第24张图片

1.调用 Unsafe.unpark(Thread_0) 方法,设置 _counter = 1。

2.唤醒 _cond 条件变量中的 Thread_0。

3.Thread_0 恢复运行。

4.设置 _counter = 0。

并发编程:(三)共享模型之管程_第25张图片

1.调用 Unsafe.unpark(Thread_0) 方法,设置 _counter = 1。

2.当前线程调用 Unsafe.park() 方法。

3.检查 _counter,若为1,这时线程无需阻塞,继续运行。

4.设置 _counter = 0。

5、线程状态装换

并发编程:(三)共享模型之管程_第26张图片

状态名称 说明
NEW 初始状态,线程被创建,但是还没有调用start()方法
RUNNABLE 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统称为“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的动作(通知或者中断)
TIMED_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

假设有线程 Thread t

1、NEW --> RUNNABLE 状态

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

2、RUNNABLE <--> WAITING 状态

t 线程synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程RUNNABLE --> WAITING
  • 调用 obj.notify() , obj.notifyAll() , 线程对象.interrupt()
    • 竞争锁成功,t 线程WAITING --> RUNNABLE
    • 竞争锁失败,t 线程WAITING --> BLOCKED

3、RUNNABLE <--> WAITING 状态

  • 当前线程调用 t.join() 方法,当前线程从 RUNNABLE --> WAITING
  • t 线程 运行结束,或调用了当前线程的 interropt() 时,当前线程从 WAITING --> RUNNABLE

4、RUNNABLE <--> WAITING 状态

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

5、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

6、RUNNABLE <--> TIMED_WAITING 状态

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过 n 毫秒,或 t 线程 运行结束,或调用当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

7、RUNNABLE <--> TIMED_WAITING 状态

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

8、RUNNABLE <--> TIMED_WAITING 状态

  • 当前线程调用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

9、RUNNABLE <--> BLOCKED 状态

  • t 线程synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程 竞争 成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

10、RUNNABLE <--> TERMINATED 状态

  • 当前线程所有代码运行完毕,进入 TERMINATED

6、多把锁

多把不相干的锁

public class Test06 {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(()->{
            try {
                bigRoom.sleep();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小红").start();
        new Thread(()->{
            try {
                bigRoom.study();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"小兰").start();
    }
}
class BigRoom{
    private final Object studyRoom = new Object();
    private final Object sleepRoom = new Object();
    public void sleep() throws InterruptedException {
        synchronized (sleepRoom) {
            System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 休息2小时");
            Thread.sleep(2000);
        }
    }
    public void study() throws InterruptedException {
        synchronized (studyRoom) {
            System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 学习1小时");
            Thread.sleep(1000);
        }
    }
}

执行结果:

Wed Jul 08 17:02:32 CST 2020【小兰】 - 学习1小时
Wed Jul 08 17:02:32 CST 2020【小红】 - 休息2小时

优点和缺点:

优点 :可以增强并发度。

缺点 :如果一个线程需要同时获得多把锁,就容易造成死锁现象。

7、线程活跃性(死锁)

1、死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。相互不释放资源从而相互等待

public class Test07 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
                }
            }
        },"t1");

        Thread t2 = new Thread(()->{
            synchronized (B) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
                }
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

执行结果:

Wed Jul 08 17:31:06 CST 2020【t2】 - Lock B
Wed Jul 08 17:31:06 CST 2020【t1】 - Lock A

解决死锁:

public class Test07 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(()->{
            synchronized (A) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (B) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
            }
        },"t1");

        Thread t2 = new Thread(()->{
            synchronized (B) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (A) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
            }
        },"t2");
        t1.start();
        t2.start();
    }
}

总结:不要在同一个代码块中,同时持有多个对象的锁就能避免死锁发生。

使用顺序加锁的方式解决死锁问题:

并发编程:(三)共享模型之管程_第27张图片

线程1 已经获得 对象A 的锁,线程2 获得 对象B 的锁,线程1 尝试获取 对象B 的锁,线程2 尝试获取 对象A 的锁,这样就导致死锁的发生

顺序加锁的解决方法:

并发编程:(三)共享模型之管程_第28张图片

线程1 获取 对象A 的锁,线程2 再想获取 对象A 的锁已经获取不到,就会进入 对象A 的 entryList 里阻塞,线程1 再尝试获取 对象B 的锁。

2、定位死锁

  1. 使用 jps 定位进程 id。
  2. 再用 jstack 定位死锁。

3、活锁

活锁出现在两个线程互相改变对方的结束条件,最后造成都无法结束的情况。解决方法:交错执行或设置随机睡眠时间。

public class Test08 {
    static volatile int count = 10;
    static final Object obj = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            while (count > 0) {
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println("count:" + count);
            }
        },"t1").start();
        new Thread(()->{
            while (count < 20) {
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
                System.out.println("count:" + count);
            }
        },"t2").start();
    }
}

4、饥饿

在java中,下面三个常见的原因会导致线程饥饿:

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  3. 线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

8、ReentrantLock(可重入)

1、特点

相对于 synchronized 它具备如下特点:

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁(防止线程饥饿)
  4. 支持多个条件变量
  5. 与 synchronized 一样,都支持可重入

2、基本语法

//获取锁,获取不到就会等待
reentrantLock.lock();//放try里面外面都是一样的
try {
  	//临界区
} finally {
  	//释放锁
  	reentrantLock.unLock();
}

3、可重入

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

public class Test01 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) {
        //上锁
        reentrantLock.lock();
        try {
            System.out.println("main");
            m1();
        } finally {
            //释放锁
            reentrantLock.unlock();
        }
    }

    public static void m1(){
        //上锁
        reentrantLock.lock();
        try {
            System.out.println("m1");
            m2();
        } finally {
            //释放锁
            reentrantLock.unlock();
        }
    }

    public static void m2(){
        //上锁
        reentrantLock.lock();
        try {
            System.out.println("m2");
        } finally {
            //释放锁
            reentrantLock.unlock();
        }
    }
}

4、可打断

lockInterruptibly() 方法返回的为获取锁的方法,但是当线程调用了 interrupt() 方法后,此方法将会返回一个异常,导致线程的中断。即线程中断。

public class Test02 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                //如果没有竞争那么此方法就会获取 reentrantLock 对象锁
                //如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法打断
                System.out.println("尝试获得锁");
                reentrantLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("没有获得锁,返回");
                return;
            }
            try {
                System.out.println("获取到锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");
        reentrantLock.lock();
        t1.start();

        sleep(1000);
        System.out.println("打断t1");
        t1.interrupt();
    }
}

执行结果:

并发编程:(三)共享模型之管程_第29张图片

注意: 如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

public class Test02 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("尝试获得锁");
            reentrantLock.lock();
            try {
                System.out.println("获取到锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");
        reentrantLock.lock();
        System.out.println("获得到锁");
        t1.start();

        sleep(1000);
        System.out.println("打断t1");
        t1.interrupt();
    }
}

执行结果:

并发编程:(三)共享模型之管程_第30张图片

5、锁超时

tryLock() 方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回 true ,如果获取失败(即锁已被其他线程获取),则返回 false ,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

获取不到锁,立刻结束等待:

public class Test03 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 尝试获得锁");
            if (!reentrantLock.tryLock()) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取不到锁");
                return;
            }
            try {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取到锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        reentrantLock.lock();
        System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 主线程先于对象上锁");
        t1.start();
        sleep(1000);
        reentrantLock.unlock();
    }
}

获取不到锁,等待时间结束等待:

public class Test03 {
    private static ReentrantLock reentrantLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 尝试获得锁");
            try {
                if (!reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取不到锁,等待 1s 结束");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 获取到锁");
            } finally {
                reentrantLock.unlock();
            }
        }, "t1");

        reentrantLock.lock();
        System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 主线程先于对象上锁");
        t1.start();
        sleep(2000);
        reentrantLock.unlock();
    }
}

执行结果:

Thu Jul 09 14:22:00 CST 2020【main】 - 主线程先于对象上锁
Thu Jul 09 14:22:00 CST 2020【t1】 - 尝试获得锁
Thu Jul 09 14:22:01 CST 2020【t1】 - 获取不到锁,等待 1s 结束

6、公平锁

ReentrantLock 默认是不公平的。公平锁一般没有必要,会降低并发度。

7、条件变量

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

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

  1. synchronized 是那些不满足条件的线程都在一个休息室等待消息。
  2. ReentrantLock 支持多间休息室,每个休息室都等待一种情况(等早餐的、等水的),唤醒也是按休息室来唤醒。
  3. await()进入等待,signal() 唤醒某一个等待,signalAll() 唤醒当前休息室所有的等待。

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)需重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
public class Test04 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    //等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    //等待外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            ROOM.lock();
            try {
                System.out.println("小红:有烟没?" + hasCigarette);
                while (!hasCigarette){
                    System.out.println("小红:没烟,先歇会!");
                    try {
                        waitCigaretteSet.await();//进入等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("小红:可以干活了!");
            } finally {
                ROOM.unlock();
            }
        },"小红").start();
        new Thread(()->{
            ROOM.lock();
            try {
                System.out.println("小兰:外卖送到了吗?" + hasTakeout);
                while (!hasTakeout){
                    System.out.println("小兰:没外卖,先歇会!");
                    try {
                        waitTakeoutSet.await();//进入等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("小兰:可以干活了");
            } finally {
                ROOM.unlock();
            }
        },"小兰").start();

        sleep(1000);
        new Thread(()->{
            ROOM.lock();
            try {
                hasTakeout = true;
                waitTakeoutSet.signal();//唤醒等待
            } finally {
                ROOM.unlock();
            }
        },"送外卖的").start();

        sleep(1000);
        new Thread(()->{
            ROOM.lock();
            try {
                hasCigarette = true;
                waitCigaretteSet.signal();//唤醒等待
            } finally {
                ROOM.unlock();
            }
        },"送烟的").start();
    }
}

你可能感兴趣的:(多线程与并发,多线程)