JUC笔记-共享模型之管程 (Monitor)

JUC-共享模型之管程( Monitor)

    • 一、线程安全问题(重点)
      • 1.1 同步
      • 1.2 线程出现问题的根本原因分析
      • 1.3 synchronized 解决方案
        • 1.3.1 同步代码块
        • 1.3.2 synchronized原理
        • 1.3.3 同步方法
    • 二、线程八锁
      • 2.1 锁住同一个对象都是this(e1对象),结果为:1,2或者2,1
      • 2.2 锁住同一个对象都是this(e1对象),结果为:1s后1,2 || 2,1s后1
      • 2.3 a,b 锁住同一个对象都是this(e1对象), c没有上锁。结果为:3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1
      • 2.4 a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥; 结果为:2,1s后1
      • 2.5 a锁住的是EightLockTest.class对象,b锁住的是this(e1),不会互斥; 结果: 2,1s后1
      • 2.6 a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
      • 2.7 a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1
      • 2.8 a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
    • 三、变量的线程安全分析
      • 3.1 成员变量和静态变量的线程安全分析(重要)
      • 3.2 局部变量线程安全分析 (重要)
        • 3.2.1 线程安全的情况
        • 3.2.2 线程不安全的情况
          • 不安全原因分析
          • 解决方法
        • 3.2.3 思考 private 或 final的重要性
      • 3.3 常见的线程安全类
      • 3.4 示例分析—是否线程安全
        • 3.4.1 示例一
        • 3.4.2 示例二
        • 3.4.3 示例三
        • 3.4.4 示例四
        • 3.4.5 示例五
        • 3.4.6 示例六
      • 3.5 习题分析
        • 3.5.1 卖票练习
        • 3.5.2 转账练习
    • 四、Monitor 概念
      • 4.1 Java对象头
      • 4.2 Monitor 原理 (Synchronized底层实现-重量级锁)
      • 4.3 synchronized原理(字节码解释)
      • 4.4 锁升级
        • 4.4.1 偏向锁
        • 4.4.2 轻量级锁
        • 4.4.3 锁膨胀
        • 4.4.4 锁优化
          • 自旋锁 (优化重量级锁竞争)
          • 锁消除 (同步省略)
          • 锁粗化
    • 五、wait和notify(重点)
      • 5.1 基本使用
      • 5.2 API介绍
      • 5.3 warit() 对比 sleep() (重点)
      • 5.4 代码优化
      • 5.5 wait / notify 套路模板
    • 六、同步模式之保护性暂停 (join、Future的实现)
      • 6.1 单任务版
      • 6.2 多任务版
    • 七、异步模式之生产者/消费者 (重点)
    • 八、park & unpack (重要)
      • 8.1 基本使用
      • 8.2 park、unpark 原理
        • 先调用park再调用upark的过程
        • 先调用upark再调用park的过程
    • 九、多把锁
    • 十、活跃性
      • 10.1 死锁 (重点)
        • 10.1.1 发生死锁的必要条件
        • 10.1.2 定位死锁的方法
          • 1、JPS + JStack 进程ID
          • 2、jconsole检测死锁
        • 10.1.3 死锁举例 - 哲学家就餐问题
        • 10.1.4 避免死锁的方法
      • 10.2 活锁
      • 10.3 饥饿
    • 十一、ReentrantLock (重点)
      • 11.1 基本语法
      • 11.2 ReentrantLock特点
        • 11.2.1 支持锁重入
        • 11.2.2 可中断 (lockInterruptibly())
        • 11.2.3 锁超时 (lock.tryLock())
          • 通过lock.tryLock()来解决 哲学家就餐问题 (重点)
        • 11.2.4 公平锁 new ReentrantLock(true)
        • 11.2.5 条件变量 (可避免虚假唤醒)
    • 十二、同步模式之顺序控制 (案例)
      • 12.1 顺序输出
        • 12.1.1 Wait/Notify版本实现
        • 12.1.2 使用ReentrantLock的await/signal
        • 12.1.3 使用LockSupport中的park/unpart
      • 12.2 交替输出
        • 12.2.1 wait/notify版本
        • 12.2.2 await/signal版本
        • 12.2.3 LockSupport的park/unpark实现
    • 十三、本章小结


一、线程安全问题(重点)

多线程基础

1.1 同步

临界资源:一次仅允许一个进程使用的资源成为临界资源

临界区:访问临界资源的代码块

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

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

为了避免临界区的竞态条件发生(解决线程安全问题):

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

管程(monitor):由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)

synchronized对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

互斥和同步都可以采用 synchronized 关键字来完成,区别:

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

性能:

  • 线程安全,性能差
  • 线程不安全,性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类

1.2 线程出现问题的根本原因分析

线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,下面举一个例子

public class Test {
	static int count = 0;
	public static void main(String[] args) throws InterruptedException {
	    Thread t1 = new Thread(()->{
	        for (int i = 1; i < 5000; i++){
	            count++;
	        }
	    });
	    Thread t2 =new Thread(()->{
	        for (int i = 1; i < 5000; i++){
	            count--;
	        }
	    });
	    t1.start();
	    t2.start();
	    t1.join(); // 主线程等待t1线程执行完
	    t2.join(); // 主线程等待t2线程执行完
	    
	    // main线程只有等待t1, t2线程都执行完之后, 才能打印count, 否则main线程不会等待t1,t2
	    // 直接就打印count的值为0
	    log.debug("count的值是{}",count);
	}
}

// 打印: 并不是我们期望的0值, 为什么呢? 看下文分析
09:42:42.921 guizy.ThreadLocalDemo [main] - count的值是511 

我将从字节码的层面进行分析:

  • 因为在Java中对静态变量自增/自减并不是原子操作

    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    iadd // 自增
    putstatic i // 将修改后的值存入静态变量i
    
    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    isub // 自减
    putstatic i // 将修改后的值存入静态变量i
    

    可以看到count++ 和 count-- 操作实际都是需要这个4个指令完成的。

如果代码是正常按顺序运行的,那么count的值不会计算错
JUC笔记-共享模型之管程 (Monitor)_第1张图片
出现负数的情况:一个线程没有完成一次完整的自增/自减(多个指令) 的操作, 就被别的线程进行操作, 此时就会出现线程安全问题
JUC笔记-共享模型之管程 (Monitor)_第2张图片
出现正数的情况
JUC笔记-共享模型之管程 (Monitor)_第3张图片

1.3 synchronized 解决方案

1.3.1 同步代码块

使用synchronized来解决上述线程问题,即对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

synchronized语法

// 线程1获得锁, 那么线程2的状态是(blocked)
synchronized(对象) { 
 	临界区
}

解决代码:

static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
     Thread t1 = new Thread(() -> {
         for (int i = 0; i < 5000; i++) {
         	 // 对临界资源(共享资源的操作) 进行 加锁
             synchronized (room) {
             	counter++;
        	}
 		}
 	}, "t1");
     Thread t2 = new Thread(() -> {
         for (int i = 0; i < 5000; i++) {
             synchronized (room) {
             counter--;
         }
     }
     }, "t2");
     t1.start();
     t2.start();
     t1.join();
     t2.join();
     log.debug("{}",counter);
}

09:56:24.210 guizy.ThreadLocalDemo [main] - count的值是0
1.3.2 synchronized原理

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

原则上:

  • 锁对象建议使用共享资源

  • 在实例方法中使用this 作为锁对象,锁住的 this 正好是共享资源

  • 在静态方法中使用类名 .class 字节码作为锁对象,因为静态成员属于类,被所有实例对象共享,所以需要锁住类
    JUC笔记-共享模型之管程 (Monitor)_第4张图片
    思考:

  • 如果把synchronized(obj)放在for循环的外面, 如何理解?

    for 循环也是一个原子操作,表现出原子性

  • 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎么运行?

    因为 t1,t2 拿到不是同一把对象锁,会出现线程安全问题 – 必须要是同一把对象锁

  • 如果t1 synchronized(obj) 而 t2 没有加会怎么样 ?

    因为 t2 没有加锁,所以 t2,不需要获取t1的锁,直接就可以执行下面的代码,仍然会出现线程安全问题。

小总结:

当多个线程对临界资源进行写操作的时候,此时会造成线程安全问题,如果使用synchronized关键字对象锁一定要是多个线程共有的,才能避免竞态条件的发生。

1.3.3 同步方法

把出现线程安全问题的核心方法锁起来,每次只能一个线程进入访问

synchronized修饰的方法的不具备继承性,所以子类是线程不安全的,如果子类的方法也被 synchronized 修饰两个锁对象其实是一把锁,而且是子类对象作为锁

用法:直接给方法加上一个修饰符 synchronized

// 同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}
// 同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

同步方法底层也是有锁对象的:

  • 如果方法是实例方法:同步方法默认用 this 作为的锁对象

    //在方法上加上synchronized关键字
     public synchronized void test() {
     	
     } 
     //等价于
    public void test() {
         synchronized(this) {}
     }
    
  • 如果方法是静态方法:同步方法默认用类名 .class 作为的锁对象

    class Test{
    	//在静态方法上加上synchronized关键字
    	public synchronized static void test() {}
    }
    //等价于
    class Test{
        public void test() {
            synchronized(Test.class) {}
    	}
    }
    

面向对象实例

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(room.get());
    }
}

class Room {
    int value = 0;
    private synchronized void increment() {
        value++;
    }
    private synchronized void decrement() {
        value--;
    }
    private synchronized int get() {
        return value;
    }
}

二、线程八锁

线程八锁就是考察 synchronized 锁住的是哪个对象

说明:主要关注锁住的对象是不是同一个

  • 锁住类对象所有类的实例的方法都是安全的类的所有实例都相当于同一把锁
  • 锁住this 对象只有在当前实例对象的线程内是安全的,如果有多个实例就不安全。

2.1 锁住同一个对象都是this(e1对象),结果为:1,2或者2,1

/**
 * Description: 不会出现安全问题, 打印结果顺序为: 1/2 或 2/1
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象就是this, 也就是e1
    public synchronized void a() {
        log.debug("1");
    }
    
    // 锁对象也是this, e1
    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

2.2 锁住同一个对象都是this(e1对象),结果为:1s后1,2 || 2,1s后1

/**
 * Description: 不会出现安全问题, 打印结果顺序为: 1s后1,2 || 2,1s后1
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象就是this, 也就是e1
    public synchronized void a(){
        Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象也是this, e1
    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

2.3 a,b 锁住同一个对象都是this(e1对象), c没有上锁。结果为:3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1

/**
 * Description: 会出现安全问题, 因为前两个线程, 执行run方法时, 都对相同的对象加锁;
 *              而第三个线程,调用的方法c, 并没有加锁, 所以它可以同前两个线程并行执行;
 *  打印结果顺序为: 分析: 因为线程3和线程1,2肯定是并行执行的, 所以有以下情况
 *               3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1
 *               至于 1,3,2的情况是不会发生的, 可以先调用到1,但需要sleep一秒.3肯定先执行了
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象就是this, 也就是e1
    public synchronized void a() throws InterruptedException {
        Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象也是this, e1
    public synchronized void b() {
        log.debug("2");
    }

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

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> {
            try {
                e1.a();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> e1.b()).start();
        new Thread(() -> e1.c()).start();
    }
}

2.4 a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥; 结果为:2,1s后1

/**
 * Description: 会出现安全问题, 线程1的锁对象为e1, 线程2的锁对象为e2. 所以他们会同一时刻执行1,2
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象是e1
    public synchronized void a() {
    	Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象是e2
    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        EightLockTest e2 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e2.b()).start();
    }
}

2.5 a锁住的是EightLockTest.class对象,b锁住的是this(e1),不会互斥; 结果: 2,1s后1

/**
 * Description: 会发生安全问题, 因为a锁住的是EightLockTest.class对象, b锁住的是this(e1),锁住的是不同对象,不会互斥
 *              结果: 2,1s后1
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象是e2
    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

2.6 a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2

/**
 * Description: 不会发生安全问题, 因为a,b锁住的是EightLockTest.class对象, 会发生互斥
 *              结果: 2,1s后1 || 1s后1,2
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象是EightLockTest.class类对象
    public static synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e1.b()).start();
    }
}

2.7 a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1

/**
 * Description: 会发生安全问题, 因为a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥
 *              结果: 2,1s后1
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象是this,e2对象
    public synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        EightLockTest e2 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e2.b()).start();
    }
}

2.8 a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2

/**
 * Description: 不会发生安全问题, 因为a,b锁住的是EightLockTest.class对象, 会发生互斥
 *              结果: 2,1s后1 || 1s后1,2
 *
 * @author guizy
 * @date 2020/12/19 11:24
 */
@Slf4j(topic = "guizy.EightLockTest")
public class EightLockTest {
    // 锁对象是EightLockTest.class类对象
    public static synchronized void a() {
        Thread.sleep(1000);
        log.debug("1");
    }

    // 锁对象是EightLockTest.class类对象
    public static synchronized void b() {
        log.debug("2");
    }

    public static void main(String[] args) {
        EightLockTest e1 = new EightLockTest();
        EightLockTest e2 = new EightLockTest();
        new Thread(() -> e1.a()).start();
        new Thread(() -> e2.b()).start();
    }
}

三、变量的线程安全分析

3.1 成员变量和静态变量的线程安全分析(重要)

  • 如果变量没有在线程间共享,那么变量是安全的
  • 如果变量在线程间共享
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区需要考虑线程安全

3.2 局部变量线程安全分析 (重要)

  • 局部变量【局部变量被初始化为基本数据类型】是安全的
  • 局部变量引用的对象则未必 (要看该对象是否被共享且被执行了读写操作)
    • 如果该对象没有逃离方法的作用范围,它是线程安全的
    • 如果该对象逃离方法的作用范围需要考虑线程安全
3.2.1 线程安全的情况
  • 局部变量表是存在于栈帧中,而虚拟机栈中又包括很多栈帧,虚拟机栈是线程私有的;
  • 局部变量【局部变量被初始化为基本数据类型】是安全的,示例如下
public static void test1() {
     int i = 10;
     i++;
}
  • 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
3.2.2 线程不安全的情况

如果局部变量引用的对象逃离方法的范围,那么要考虑线程安全问题,代码示例如下

循环创建了100个线程, 在线程体里面都调用了method1方法, 在method1方法中又循环调用了100次method2,method3方法。方法2,3都使用到了成员变量arrayList, 此时的问题就是: 1个线程它会循环调用100次方法2和3, 一共有100个线程, 此时100个线程操作的共享资源就是arrayList成员变量 , 而且还进行了读写操作. 必然会造成线程不安全的问题

public class Test15 {
    public static void main(String[] args) {
        UnsafeTest unsafeTest = new UnsafeTest();
        for (int i =0;i<100;i++){
            new Thread(()->{
                unsafeTest.method1();
            },"线程"+i).start();
        }
    }
}
class UnsafeTest{
    ArrayList<String> arrayList = new ArrayList<>();
    public void method1(){
        for (int i = 0; i < 100; i++) {
            method2();
            method3();
        }
    }
    private void method2() {
        arrayList.add("1");
    }
    private void method3() {
        arrayList.remove(0);
    }
}

Exception in thread "线程1" Exception in thread "线程2" java.lang.ArrayIndexOutOfBoundsException: -1
不安全原因分析
  • 无论哪个线程中的 method2 和 method3 引用的都是同一个对象中的 list 成员变量
  • 一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
    • 第一步: 在 arrayList[size]的位置存放此元素
    • 第二步: size++
  • 单线程运行的情况下,如果 size = 0,添加一个元素后,此元素在位置 0,而且 size=1;(没问题)
  • 多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 进行上下文切换 (线程A还没来得及size++),线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍等于0 (注意,我们假设的是添加一个元素是要两个步骤,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 size 的值
  • 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 size 却等于 2。这就是“线程不安全”了。
    JUC笔记-共享模型之管程 (Monitor)_第5张图片
解决方法
  • 可以将list修改成局部变量局部变量存放在栈帧中栈帧又存放在虚拟机栈中虚拟机栈是作为线程私有的
  • 因为method1方法,将arrayList传给method2,method3方法,此时他们三个方法共享这同一个arrayList,此时不会被其他线程访问到,所以不会出现线程安全问题,因为这三个方法使用的同一个线程
  • 在外部,创建了100个线程,每个线程都会调用method1方法,然后都会再重新创建一个新的arrayList对象这个新对象再传递给method2,method3方法
class UnsafeTest {
    public void method1() {
        ArrayList<String> arrayList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            method2(arrayList);
            method3(arrayList);
        }
    }

    private void method2(List<String> arrayList) {
        arrayList.add("1");
    }

    private void method3(List<String> arrayList) {
        arrayList.remove(0);
    }
}
3.2.3 思考 private 或 final的重要性
  • 方法访问修饰符带来的思考: 如果把method2和method3 的方法修改为public 会不会导致线程安全问题;分情况:
  • 情况1:有其它线程调用 method2 和 method3
    • 只修改为public修饰,此时不会出现线程安全的问题,即使线程2调用method2、3方法,给2、3方法传过来的list对象也是线程2调用method1方法时,传递给method2、3的list对象,不可能是线程1调用method1方法传的对象。 具体原因看上面:解决方法。
  • 情况2:在情况1 的基础上,为ThreadSafe 类添加子类,子类覆盖method2 或 method3方法,即如下所示: 从这个例子可以看出privatefinal提供【安全】的意义所在,请体会开闭原则中的【】。
  • 如果改为public, 此时子类可以重写父类的方法, 在子类中开线程来操作list对象, 此时就会出现线程安全问题: 子类和父类共享了list对象
  • 如果改为private, 子类就不能重写父类的私有方法, 也就不会出现线程安全问题; 所以所private修饰符是可以避免线程安全问题.
  • 所以如果不想子类重写父类的方法的时候, 我们可以将父类中的方法设置为private, final修饰的方法, 此时子类就无法影响父类中的方法了!
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

3.3 常见的线程安全类

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

重点:

  • 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 , 也可以理解为 它们的每个方法是原子的(方法都被加上了synchronized)。
  • 但注意它们多个方法的组合不是原子的,所以可能会出现线程安全问题
// 以下是线程安全的
Hashtable table = new Hashtable();

new Thread(()->{
	// put方法增加了synchronized
 	table.put("key", "value1");
}).start();

new Thread(()->{
 	table.put("key", "value2");
}).start();

线程安全类方法的组合代码示例

Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
 table.put("key", value);
}

这里只能是get方法内部是线程安全的put方法内部是线程安全的组合起来使用还是会受到上下文切换的影响
JUC笔记-共享模型之管程 (Monitor)_第6张图片
不可变类的线程安全

  • StringInteger类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的, 都被final修饰不能被继承
  • 肯定有些人他们知道String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象! (在字符串常量池中当修改了String的值,它不会再原有的基础上修改,而是会重新开辟一个空间来存储)。

3.4 示例分析—是否线程安全

3.4.1 示例一

Servlet运行在Tomcat环境下并只有一个实例,因此会被Tomcat的多个线程共享使用,因此存在成员变量的共享问题。

public class MyServlet extends HttpServlet {
	 // 是否安全?  否:HashMap不是线程安全的,HashTable是
	 Map<String,Object> map = new HashMap<>();
	 // 是否安全?  是:String 为不可变类,线程安全
	 String S1 = "...";
	 // 是否安全? 是
	 final String S2 = "...";
	 // 是否安全? 否:不是常见的线程安全类
	 Date D1 = new Date();
	 // 是否安全?  否:引用值D2不可变,但是日期里面的其它属性比如年月日可变。与字符串的最大区别是Date里面的属性可变。
	 final Date D2 = new Date();
 
	 public void doGet(HttpServletRequest request,HttpServletResponse response) {
	  // 使用上述变量
	 }
}
3.4.2 示例二
  • 分析线程是否安全,先对类的成员变量类变量(静态变量)局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的。下面对实例进行分析:此类不是线程安全的MyAspect切面类只有一个实例,成员变量start会被多个线程同时进行读写操作
  • Spring中的Bean都是单例的, 除非使用@Scope修改为多例
@Aspect
@Component 
public class MyAspect {
        // 是否安全?不安全, 因为MyAspect是单例的
        // 解决:使用@Aroud环绕通知,把start设置为局部变量
        private long start = 0L;

        @Before("execution(* *(..))")
        public void before() {
            start = System.nanoTime();
        }

        @After("execution(* *(..))")
        public void after() {
            long end = System.nanoTime();
            System.out.println("cost time:" + (end-start));
        }
    }
3.4.3 示例三
  • 此例是典型的三层模型调用MyServlet UserServiceImpl UserDaoImpl类都只有一个实例,UserDaoImpl类中没有成员变量update方法里的变量引用的对象不是线程共享的,所以是线程安全的;UserServiceImpl类中只有一个线程安全的UserDaoImpl类的实例,那么UserServiceImpl类也是线程安全的同理 MyServlet也是线程安全的
  • Servlet调用Service, Service调用Dao这三个方法使用的是同一个线程
public class MyServlet extends HttpServlet {
	 // 是否安全    是:UserService不可变,虽然有一个成员变量,
	 			// 但是是私有的, 没有地方修改它
	 private UserService userService = new UserServiceImpl();
	 
	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
	 	userService.update(...);
	 }
}

public class UserServiceImpl implements UserService {
	 // 是否安全     是:Dao不可变, 其没有成员变量
	 private UserDao userDao = new UserDaoImpl();
	 
	 public void update() {
	 	userDao.update();
	 }
}

public class UserDaoImpl implements UserDao { 
	 // 是否安全   是:没有成员变量,无法修改其状态和属性
	 public void update() {
	 	String sql = "update user set password = ? where username = ?";
	 	// 是否安全   是:不同线程创建的conn各不相同,都在各自的栈内存中
	 	try (Connection conn = DriverManager.getConnection("","","")){
	 	// ...
	 	} catch (Exception e) {
	 	// ...
	 	}
	 }
}
3.4.4 示例四

跟示例二大体相似,UserDaoImpl类中有成员变量,那么多个线程可以对成员变量conn 同时进行操作,故是不安全的

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}

public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    public void update() {
       userDao.update();
    }
}

public class UserDaoImpl implements UserDao {
    // 是否安全: 不安全; 当多个线程,共享conn, 一个线程拿到conn,刚创建一个连接赋值给conn, 此时另一个线程进来了, 直接将conn.close
    //另一个线程恢复了, 拿到conn干事情, 此时conn都被关闭了, 出现了问题
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}
3.4.5 示例五

跟示例三大体相似,UserServiceImpl类的update方法中UserDao是作为局部变量存在的,所以每个线程访问的时候都会新建有一个UserDao对象,新建的对象是线程独有的,所以是线程安全的

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    public void update() {
    	// 作为局部变量,每个线程都会新建
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}
3.4.6 示例六

私有变量sdf被暴露出去了, 发生了逃逸

public abstract class Test {
    public void bar() {
        // 是否安全
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }
    public abstract foo(SimpleDateFormat sdf);
    public static void main(String[] args) {
        new Test().bar();
    }
}

其中foo的行为是不确定的,可能导致不安全的发生,被称之为外星方法因为foo方法可以被重写,导致线程不安全
在String类中就考虑到了这一点,String类是final的,子类不能重写它的方法。

public void foo(SimpleDateFormat sdf) {
    String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
                sdf.parse(dateStr);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

3.5 习题分析

3.5.1 卖票练习

对类中的成员变量进行加锁synchronizedthis进行加锁。

package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        // 模拟多人买票
        TicketWindow window = new TicketWindow(1000);

        // 所有线程的集合(由于threadList在主线程中,不被共享,因此使用ArrayList不会出现线程安全问题)
        List<Thread> threadList = new ArrayList<>();
        // 卖出的票数统计(Vector为线程安全类)
        List<Integer> amountList = new Vector<>();
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                // 买票
                int amount = window.sell(random(5));
                // 统计买票数
                amountList.add(amount);
            });
            threadList.add(thread);
            thread.start();
        }

        for (Thread thread : threadList) {
            thread.join();
        }

        // 统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~5
    public static int random(int amount) {
        return random.nextInt(amount) + 1;
    }
}

// 售票窗口
class TicketWindow {
	// 票总数
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    // 获取余票数量 join之后调用 无需加锁
    public int getCount() {
        return count;
    }

    // 售票
    public synchronized int sell(int amount) {
        if (this.count >= amount) {
            this.count -= amount;
            return amount;
        } else {
            return 0;
        }
    }
}
3.5.2 转账练习
  • 锁对象涉及到A的账户金额B的账户金额,所以需要对Account 类进行加锁。
  • 如果将synchronized加在实例方法transfer上,只会保证自己账户的金额(this)不发生线程安全问题
package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}", (a.getMoney() + b.getMoney()));
    }

    // Random 为线程安全
    static Random random = new Random();

    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) + 1;
    }
}

// 账户
class Account {
    private int money;

    public Account(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    // 转账
    public void transfer(Account target, int amount) {
    	//锁住Account类,因为涉及到A.money和B.money。
        synchronized(Account.class) {   
            if (this.money >= amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
}

// 没问题, 最终的结果仍然是 2000元

四、Monitor 概念

【对线面试官】synchronized


4.1 Java对象头

JVM-内存区域详解—2.3 对象的内存布局

对象头包含两部分运行时元数据(Mark Word)类型指针 (Klass Word)

  • 运行时元数据
    • 哈希值(HashCode),可以看作是堆中对象的地址
    • GC分代年龄(年龄计数器) (用于新生代from/to区晋升老年代的标准, 阈值为15)
    • 锁状态标志 (用于JDK1.6对synchronized的优化 -> 轻量级锁)
    • 线程持有的锁
    • 偏向线程ID (用于JDK1.6对synchronized的优化 -> 偏向锁)
    • 偏向时间戳
  • 类型指针
    • 指向类元数据InstanceKlass,确定该对象所属的类型。指向的其实是方法区中存放的类元信息

说明如果对象是数组,还需要记录数组的长度

  • 以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为类型指针指向方法区对应的Class对象
    JUC笔记-共享模型之管程 (Monitor)_第7张图片
  • 数组对象
    JUC笔记-共享模型之管程 (Monitor)_第8张图片
  • 其中 Mark Word结构为: 无锁(001)偏向锁(101)轻量级锁(00)重量级锁(10)
    JUC笔记-共享模型之管程 (Monitor)_第9张图片
  • 所以一个对象的结构如下:
    JUC笔记-共享模型之管程 (Monitor)_第10张图片

4.2 Monitor 原理 (Synchronized底层实现-重量级锁)

多线程同时访问临界区: 使用重量级锁

  • JDK6对Synchronized的优先状态偏向锁–>轻量级锁–>重量级锁

Monitor被翻译为监视器或者管程

每个Java对象都可以关联一个(操作系统的)Monitor,如果使用synchronized给对象上锁(重量级),该对象头的MarkWord中就被设置为指向Monitor对象的指针

工作流程

  • 开始时 MonitorOwnernull
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2Monitor 中只能有一个 Ownerobj 对象的 Mark Word 指向 Monitor把对象原有的 MarkWord 存入线程栈中的锁记录中(轻量级锁部分详解)
    JUC笔记-共享模型之管程 (Monitor)_第11张图片
  • 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)
  • Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找设置 Owner 为空把线程栈的锁记录中的对象头的值设置到 MarkWord
  • 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞
  • WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)

JUC笔记-共享模型之管程 (Monitor)_第12张图片
注意:

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

4.3 synchronized原理(字节码解释)

示例代码如下:

static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

反编译后的部分字节码:
JUC笔记-共享模型之管程 (Monitor)_第13张图片
说明:

  • 通过异常 try-catch 机制,确保一定会被解锁
  • 方法级别的 synchronized 不会在字节码指令中有所体现

4.4 锁升级

synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化
JUC笔记-共享模型之管程 (Monitor)_第14张图片

升级过程

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁随着竞争的增加,只能锁升级,不能降级
JUC笔记-共享模型之管程 (Monitor)_第15张图片
JUC笔记-共享模型之管程 (Monitor)_第16张图片

4.4.1 偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程这个线程之后重新获取该锁不再需要同步操作

  • 当锁对象第一次被线程获得的时候进入偏向状态,标记为101,同时使用 CAS 操作线程 ID记录到Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作
  • 当有另外一个线程去尝试获取这个锁对象时偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定轻量级锁状态

JUC笔记-共享模型之管程 (Monitor)_第17张图片
一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,thread、epoch、age 都为 0

  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟

    JDK 8 延迟 4s 开启偏向锁原因在刚开始执行代码时,会有好多线程来抢锁,如果开偏向锁效率反而降低

  • 如果禁用了偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值,添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁

撤销偏向锁的状态

  • 调用对象的 hashCode偏向锁的对象 MarkWord 中存储的是线程 id,调用 hashCode 导致偏向锁被撤销
    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor中记录 hashCode
  • 有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  • 调用 wait/notify,需要申请 Monitor,进入 WaitSet

批量撤销:如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

  • 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
  • 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
4.4.2 轻量级锁

一个对象有多个线程要加锁,但加锁的时间是错开的没有竞争),可以使用轻量级锁来优化轻量级锁对使用者是透明的(不可见)

可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁

轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作Java 6 才引入的偏向锁来优化

锁重入实例:

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
    JUC笔记-共享模型之管程 (Monitor)_第18张图片

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

  • 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁 JUC笔记-共享模型之管程 (Monitor)_第19张图片

  • 如果 CAS 失败,有两种情况:

    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是线程自己执行了 synchronized 锁重入,就添加一条 Lock Record 作为重入的计数

JUC笔记-共享模型之管程 (Monitor)_第20张图片

  • 当退出 synchronized 代码块(解锁时)

    • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
4.4.3 锁膨胀

尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
    JUC笔记-共享模型之管程 (Monitor)_第21张图片

  • Thread-1加轻量级锁失败,进入锁膨胀流程为 Object 对象申请 Monitor 锁,让Object 对象头指向重量级锁地址,Monitor 的 Owner 置为 Thread-0,然后自己进入 Monitor 的 EntryList BLOCKED
    JUC笔记-共享模型之管程 (Monitor)_第22张图片

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

4.4.4 锁优化
自旋锁 (优化重量级锁竞争)

重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁

注意:

  • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
  • 自旋失败的线程会进入阻塞状态

优点不会进入阻塞状态减少线程上下文切换的消耗

缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源

自旋锁情况:

  • 自旋成功的情况:
    JUC笔记-共享模型之管程 (Monitor)_第23张图片
  • 自旋失败的情况:
    JUC笔记-共享模型之管程 (Monitor)_第24张图片

自旋锁说明:

  • Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能
  • Java 7 之后不能控制是否开启自旋功能,由 JVM 控制

手写自旋锁

public class SpinLock {
    // 泛型装的是Thread,原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + " come in");

        //开始自旋,期望值为null,更新值是当前线程
        while (!atomicReference.compareAndSet(null, thread)) {
            Thread.sleep(1000);
            System.out.println(thread.getName() + " 正在自旋");
        }
        System.out.println(thread.getName() + " 自旋成功");
    }

    public void unlock() {
        Thread thread = Thread.currentThread();

        //线程使用完锁把引用变为null
		atomicReference.compareAndSet(thread, null);
        System.out.println(thread.getName() + " invoke unlock");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();
        new Thread(() -> {
            //占有锁
            lock.lock();
            Thread.sleep(10000); 

            //释放锁
            lock.unlock();
        },"t1").start();

        // 让main线程暂停1秒,使得t1线程,先执行
        Thread.sleep(1000);

        new Thread(() -> {
            lock.lock();
            lock.unlock();
        },"t2").start();
    }
}
锁消除 (同步省略)

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。

锁粗化

对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化

如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部

  • 一些看起来没有加锁的代码,其实隐式的加了很多锁:

    public static String concatString(String s1, String s2, String s3) {
        return s1 + s2 + s3;
    }
    
  • String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,转化为StringBuffer对象的连续 append() 操作,每个append() 方法中都有一个同步块

    public static String concatString(String s1, String s2, String s3) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        sb.append(s3);
        return sb.toString();
    }
    

扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,只需要加锁一次就可以

五、wait和notify(重点)

5.1 基本使用

需要获取对象锁后才可以调用 锁对象.wait()notify随机唤醒一个线程notifyAll唤醒所有线程去竞争 CPU

JUC笔记-共享模型之管程 (Monitor)_第25张图片线程0获得到了锁,成为Monitor的Owner,但是此时它发现自己想要执行synchroized代码块的条件不满足;此时它就调用obj.wait方法,进入到Monitor中的WaitSet集合

底层原理:

  • Owner 线程发现条件不满足调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKEDWAITING的线程都处于阻塞状态,不占用 CPU 时间片
  • 处于BLOCKED状态的线程会在锁被释放的时候被唤醒
  • 处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。然后它会进入到EntryList,重新竞争锁

5.2 API介绍

下面的四个方法都是Object中的方法; 通过锁对象来调用
Object 类 API:

public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待,直到另一个线程调用该对象的notify()方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒

说明:wait 是挂起线程需要唤醒的都是挂起操作阻塞线程可以自己去争抢锁挂起的线程需要唤醒后去争抢锁

wait 和 notify方法示例代码:

@Slf4j(topic = "guizy.WaitNotifyTest")
public class WaitNotifyTest {
    static final Object obj = new Object();

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

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行...");
                try {
                    // 只有获得锁对象之后, 才能调用wait/notify
                    obj.wait(); // 此时t1线程进入WaitSet等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...");
            }
        }, "t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行...");
                try {
                    obj.wait(); // 此时t2线程进入WaitSet等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...");
            }
        }, "t2").start();

        // 让主线程等两秒在执行,为了`唤醒`,不睡的话,那两个线程还没进入waitSet,主线程就开始唤醒了
        Thread.sleep(1000);
        log.debug("唤醒waitSet中的线程!");
        // 只有获得锁对象之后, 才能调用wait/notify
        synchronized (obj) {
            // obj.notify(); // 唤醒waitset中的一个线程
             obj.notifyAll(); // 唤醒waitset中的全部等待线程
        }
    }
}

13:01:36.176 guizy.WaitNotifyTest [t1] - 执行...
13:01:36.178 guizy.WaitNotifyTest [t2] - 执行...
13:01:37.175 guizy.WaitNotifyTest [main] - 唤醒waitSet中的线程!
13:01:37.175 guizy.WaitNotifyTest [t2] - 其它代码...
13:01:37.175 guizy.WaitNotifyTest [t1] - 其它代码...

5.3 warit() 对比 sleep() (重点)

不同点:

  • 原理不同sleep() 方法属于Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类用于线程间通信
  • 对锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
  • 使用区域不同wait() 方法必须放在同步控制方法和同步代码块先获取锁)中使用,sleep() 方法则可以放在任何地方使用

相同点:

  • 阻塞状态都为TIMED_WAITING (限时等待)

5.4 代码优化

  • 虚假唤醒notify只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程

    解决方法:采用notifyAll

  • notifyAll仅解决某个线程的唤醒问题使用 if + wait 判断仅有一次机会,一旦条件不成立,无法重新判断

    解决方法:用 while + wait当条件不成立,再次 wait

@Slf4j(topic = "c.demo")
public class demo {
    static final Object room = new Object();
    static boolean hasCigarette = false;    //有没有烟
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (room) {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {//while防止虚假唤醒
                    log.debug("没烟,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("有烟没?[{}]", hasCigarette);
                if (hasCigarette) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小南").start();

        new Thread(() -> {
            synchronized (room) {
                Thread thread = Thread.currentThread();
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        room.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖送到没?[{}]", hasTakeout);
                if (hasTakeout) {
                    log.debug("可以开始干活了");
                } else {
                    log.debug("没干成活...");
                }
            }
        }, "小女").start();


        Thread.sleep(1000);
        new Thread(() -> {
        // 这里能不能加 synchronized (room)? 
        // 是wait()方法的话可以,因为可以释放锁
        // 如果是slepp()方法不释放锁,就会等待持有锁的方法结束才能进入room
            synchronized (room) {
                hasTakeout = true;
				//log.debug("烟到了噢!");
                log.debug("外卖到了噢!");
                room.notifyAll();
            }
        }, "送外卖的").start();
    }
}

11:19:25.275 guizy.WaitNotifyTest [小南] - 有烟没?[false]
11:19:25.282 guizy.WaitNotifyTest [小南] - 没烟,先歇会!
11:19:25.282 guizy.WaitNotifyTest [小女] - 外卖送到没?[false]
11:19:25.283 guizy.WaitNotifyTest [小女] - 没外卖,先歇会!
11:19:26.287 guizy.WaitNotifyTest [送外卖的] - 外卖到了噢!
11:19:26.287 guizy.WaitNotifyTest [小女] - 外卖送到没?[true]
11:19:26.287 guizy.WaitNotifyTest [小女] - 可以开始干活了
11:19:26.288 guizy.WaitNotifyTest [小南] - 没烟,先歇会!

5.5 wait / notify 套路模板

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

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

六、同步模式之保护性暂停 (join、Future的实现)

6.1 单任务版

Guarded Suspension用在一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现Future 的实现采用的就是此模式
    JUC笔记-共享模型之管程 (Monitor)_第26张图片

一方等待另一方的执行结果举例 :

  • 线程1等待线程2的结果,并获取该结果,同时设置超时时间
@Slf4j(topic = "guizy.GuardeObjectTest")
public class GuardeObjectTest {
    public static void main(String[] args) {
        // 线程1等待线程2的结果
        GuardeObject guardeObject = new GuardeObject();
        new Thread(() -> {
            log.debug("begin");
            Object obj = guardeObject.get(2000);
            log.debug("结果是:{}", obj);
        }, "t1").start();

        new Thread(() -> {
            log.debug("begin");
            // Sleeper.sleep(1); // 在等待时间内
            Sleeper.sleep(3);	// 超出等待时间
            guardeObject.complete(new Object());
        }, "t2").start();
    }
}

class GuardeObject {
    // 结果
    private Object response;

    // 获取结果
    // timeout表示等待多久. 这里假如是2s
    public Object get(long timeout) {
        synchronized (this) {
            // 1) 记录最初时间 假如开始时间为 15:00:00
            long begin = System.currentTimeMillis();
            // 2) 已经经历的时间
            long passedTime = 0;
            
            while (response == null) {
                // 4) 计算这一轮循环应该等待的时间
                long waitTime = timeout - passedTime;
                // 经历的时间超过了最大等待时间, 无需等待,退出循环
                if (waitTime <= 0) {
                    break;
                }
                try {
                    // this.wait(timeout)的问题: 虚假唤醒在15:00:01的时候,此时response还null, 此时经历时间就为1s,
                    // 进入while循环的时候response还是空,此时判断1s<=timeout 2s,此时再次this.wait(2s)吗
                    // 此时已经经历了1s,所以只要再等1s就可以了. 所以等待的时间应该是 超时时间(timeout) - 经历的时间(passedTime)
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 3) 更新已经经历的时间
                passedTime = System.currentTimeMillis() - begin; // 15:00:02
            }
            return response;
        }
    }

    // 产生结果
    public void complete(Object response) {
        synchronized (this) {
            // 给结果变量赋值
            this.response = response;
            this.notifyAll();
    }
}
// 在等待时间内的情况
16:20:41.627 guizy.GuardeObjectTest [t1] - begin
16:20:41.627 guizy.GuardeObjectTest [t2] - begin
16:20:42.633 guizy.GuardeObjectTest [t1] - 结果是:java.lang.Object@1e1d0168

// 超时的情况
16:21:24.663 guizy.GuardeObjectTest [t2] - begin
16:21:24.663 guizy.GuardeObjectTest [t1] - begin
16:21:26.667 guizy.GuardeObjectTest [t1] - 结果是:null
  • 关于超时的增强,在join(long millis)的源码中得到了体现:
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 {
    // join一个指定的时间
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

6.2 多任务版

  • 多任务版GuardedObject图中 Futures 就好比外卖柜(每个外卖柜上有编号),左侧的 t0,t2,t4 就好比等待外卖的用户,右侧的 t1,t3,t5 就好比快递小哥。如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类
  • 不仅能够解耦结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。和生产者消费者模式的区别就是:这个产生结果的线程和使用结果的线程是一一对应的关系,但是生产者消费者模式并不是。
  • rpc框架的调用中就使用到了这种模式

JUC笔记-共享模型之管程 (Monitor)_第27张图片

@Slf4j(topic = "c.GuardedObjectTest")
public class GuardedObjectTest {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new People().start();
        }
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (Integer id : MiddleUtils.getIds()) {
            new FoodDelivery(id, "食物" + id).start();
        }
    }
}

@Slf4j(topic = "c.People")
// 用户类
class People extends Thread {
    @Override
    public void run() {
        // 点外卖
        GuardedObject guardedObject = MiddleUtils.createGuardedObject();
        log.debug("点外卖 订单编号id:{}", guardedObject.getId());
        Object food = guardedObject.get(5000);
        log.debug("收到外卖 订单编号id:{}, 食物:{}", guardedObject.getId(), food);
    }
}

@Slf4j(topic = "c.FoodDelivery")
// 外卖员类
class FoodDelivery  extends Thread {
    private final int id;
    private final String food;

    public FoodDelivery(int id, String mail) {
        this.id = id;
        this.food = mail;
    }

    @Override
    public void run() {
        GuardedObject guardedObject = MiddleUtils.getGuardedObject(id);
        log.debug("送外卖 订单编号id:{}, 食物:{}", id, food);
        guardedObject.complete(food);
    }
}

// 外卖柜(通用中间解耦类)
class MiddleUtils {
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();

    private static int id = 1;

    // 产生唯一 id
    private static synchronized int generateId() {
        return id++;
    }

    public static GuardedObject getGuardedObject(int id) {
        //根据id获取到box并删除对应的key和value,避免堆内存爆了
        return boxes.remove(id);
    }

    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    public static Set<Integer> getIds() {
        return boxes.keySet();
    }
}

// 用来传递信息的作用, 当多个类使用GuardedObject,就很不方便,此时需要一个设计一个解耦的中间类
class GuardedObject {
    // 标记GuardedObject
    private final int id;
    // 结果
    private Object response;

    public int getId() {
        return id;
    }

    public GuardedObject(int id) {
        this.id = id;
    }

    // 获取结果
    // timeout表示最大等待时间
    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();
        }
    }
}

七、异步模式之生产者/消费者 (重点)

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

JUC笔记-共享模型之管程 (Monitor)_第28张图片
我们下面写的小例子是线程间通信的消息队列,要注意区别,像RabbitMQ消息框架是进程间通信的

@Slf4j(topic = "giuzy.ProductConsumerTest")
public class ProductConsumerTest {
    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) {
                sleep(1000);
                Message message = queue.take();
            }
        }, "消费者").start();
    }

}

// 消息队列类,在线程之间通信
@Slf4j(topic = "guizy.MessageQueue")
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 {
                    log.debug("队列为空, 消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 从队列头部获取消息并返回
            Message message = list.removeFirst();
            log.debug("已消费消息 {}", message);
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message) {
        synchronized (list) {
            // 检查对象是否已满
            while (list.size() == capcity) {
                try {
                    log.debug("队列已满, 生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 将消息加入队列尾部
            list.addLast(message);
            log.debug("已生产消息 {}", message);
            list.notifyAll();
        }
    }
}

final class Message {
    private final int id;
    private final 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 +
                '}';
    }
}
18:52:53.440 guizy.MessageQueue [生产者1] - 已生产消息 Message{id=1, value=1}
18:52:53.443 guizy.MessageQueue [生产者0] - 已生产消息 Message{id=0, value=0}
18:52:53.444 guizy.MessageQueue [生产者2] - 队列已满, 生产者线程等待
18:52:54.439 guizy.MessageQueue [消费者] - 已消费消息 Message{id=1, value=1}
18:52:54.439 guizy.MessageQueue [生产者2] - 已生产消息 Message{id=2, value=2}
18:52:55.439 guizy.MessageQueue [消费者] - 已消费消息 Message{id=0, value=0}
18:52:56.440 guizy.MessageQueue [消费者] - 已消费消息 Message{id=2, value=2}
18:52:57.441 guizy.MessageQueue [消费者] - 队列为空, 消费者线程等待

八、park & unpack (重要)

8.1 基本使用

  • park/unpark都是LockSupport类中的的方法
  • 先调用unpark后,再调用park,此时park不会暂停线程
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(thread);

特点:

Objectwait & notify相比

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

8.2 park、unpark 原理

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

先调用park再调用upark的过程
  • 先调用park的情况
    • 当前线程调用Unsafe.park()方法
    • 检查 _counter,本情况为0,这时,获得_mutex 互斥锁(mutex对象有个等待队列 _cond)
    • 线程进入 _cond 条件变量阻塞
    • 设置_counter = 0

JUC笔记-共享模型之管程 (Monitor)_第29张图片

  • 调用unpark
    • 调用Unsafe.unpark(Thread_0)方法设置_counter 为 1
    • 唤醒 _cond 条件变量中的 Thread_0
    • Thread_0 恢复运行
    • 设置 _counter 为 0

JUC笔记-共享模型之管程 (Monitor)_第30张图片

先调用upark再调用park的过程
  • 调用 Unsafe.unpark(Thread_0)方法,设置 _counter 为 1
  • 当前线程调用 Unsafe.park()方法
  • 检查 _counter,本情况为 1这时线程 无需阻塞,继续运行
  • 设置 _counter 为 0

JUC笔记-共享模型之管程 (Monitor)_第31张图片

九、多把锁

小故事

  • 一间大屋子有两个功能睡觉、学习,互不相干
  • 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低
  • 并且,小南获得锁之后,学完习之后,小女才能进来睡觉。

解决方法是准备多个房间多个对象锁

@Slf4j(topic = "guizy.BigRoomTest")
public class BigRoomTest {
	private static final BigRoom sleepRoom = new BigRoom();
    private static final BigRoom studyRoom = new BigRoom();
    public static void main(String[] args) {
        // 不同对象调用
        new Thread(() -> sleepRoom .sleep(), "小南").start();
        new Thread(() -> studyRoom .study(), "小女").start();
    }
}

@Slf4j(topic = "guizy.BigRoom")
class BigRoom {
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}

将锁的粒度细分

  • 好处:可以增强并发度
  • 坏处:如果一个线程需要同时获得多把锁,就容易发生死锁

十、活跃性

因为某种原因,使得代码一直无法执行完毕,这样的现象叫做活跃性
活跃性相关的一系列问题都可以用 ReentrantLock 进行解决。

10.1 死锁 (重点)

一个线程需要 同时获取多把锁,这时就容易发生死锁

如:线程1获取A对象锁,线程2获取B对象锁;此时线程1又想获取B对象锁,线程2又想获取A对象锁;它们都等着对象释放锁,此时就称为死锁。

public static void main(String[] args) {
	final Object A = new Object();
	final Object B = new Object();
	
	new Thread(()->{
		synchronized (A) {
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (B) {

			}
		}
	}).start();

	new Thread(()->{
		synchronized (B) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (A) {

			}
		}
	}).start();
}
10.1.1 发生死锁的必要条件
  • 互斥条件
    在一段时间内,一种资源只能被一个进程所使用
  • 请求和保持条件
    进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
  • 不可抢占条件
    进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
  • 循环等待条件
    发生死锁时,必然存在一个进程——资源的循环链。

四个条件都成立的时候,便形成死锁。死锁情况下打破上述任何一个条件,便可让死锁消失

10.1.2 定位死锁的方法
1、JPS + JStack 进程ID
  • jps先找到JVM进程
  • jstack 进程ID

在Java控制台中的Terminal中输入 jps 指令可以查看正在运行中的进程ID,使用 jstack 进程ID 可以查看进程状态。

2、jconsole检测死锁

jconsole 工具,在 jdk\bin 目录下

10.1.3 死锁举例 - 哲学家就餐问题

JUC笔记-共享模型之管程 (Monitor)_第32张图片

/**
 * Description: 使用synchronized加锁, 导致哲学家就餐问题死锁
 * 核心原因是因为synchronized的锁是不可打断的, 进入阻塞队列,需要一直等待别的线程释放锁
 */
@Slf4j(topic = "guizy.PhilosopherEat")
public class PhilosopherEat {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

@Slf4j(topic = "guizy.Philosopher")
class Philosopher extends Thread {
    final Chopstick left;
    final Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            // 尝试获取左手筷子
            synchronized (left) {
                // 尝试获取右手筷子
                synchronized (right) {
                    eat();
                }
            }
        }
    }

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick{
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
10.1.4 避免死锁的方法
  • 在线程使用锁对象时, 采用固定加锁的顺序, 可以使用Hash值的大小来确定加锁的先后
  • 尽可能缩减加锁的范围, 等到操作共享变量的时候才加锁
  • 使用可释放的定时锁 (一段时间申请不到锁的权限了, 直接释放掉)

10.2 活锁

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

例子

class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                Thread.sleep(200);
                count--;
                System.out.println("线程一count:" + count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                Thread.sleep(200);
                count++;
                System.out.println("线程二count:"+ count);
            }
        }, "t2").start();
    }
}

10.3 饥饿

  • 一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
  • 在使用顺序加锁时,可能会出现饥饿现象

十一、ReentrantLock (重点)

ReentrantLock 的特点(synchronized不具备的)

  • 支持锁重入
    可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 可中断
    lock.lockInterruptibly() : 可以被其他线程打断的中断锁
  • 可以设置超时时间
    lock.tryLock(时间): 尝试获取锁对象,如果超过了设置的时间,还没有获取到锁,此时就退出阻塞队列,并释放掉自己拥有的锁。
  • 可以设置为公平锁
    (先到先得) 默认是非公平,true为公平 new ReentrantLock(true)
  • 支持多个条件变量( 有多个waitset)
    (可避免虚假唤醒) - lock.newCondition()创建条件变量对象,通过条件变量对象调用 await/signal方法, 等待/唤醒

11.1 基本语法

//获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//加锁
lock.lock();
try {
	//临界区
	//需要执行的代码
}finally {
	//释放锁
	lock.unlock();
}

11.2 ReentrantLock特点

11.2.1 支持锁重入
  • 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
/**
 * Description: ReentrantLock 可重入锁, 同一个线程可以多次获得锁对象
 */
@Slf4j(topic = "guizy.ReentrantTest")
public class ReentrantTest {

    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
    	// 如果有竞争就进入`阻塞队列`, 一直等待着,不能被打断
        lock.lock();
        try {
            log.debug("entry main...");
            m1();
        } finally {
            lock.unlock();
        }
    }

    private static void m1() {
        lock.lock();
        try {
            log.debug("entry m1...");
            m2();
        } finally {
            lock.unlock();
        }
    }

    private static void m2() {
        log.debug("entry m2....");
    }
}
13:54:29.324 guizy.ReentrantTest [main] - entry main...
13:54:29.326 guizy.ReentrantTest [main] - entry m1...
13:54:29.326 guizy.ReentrantTest [main] - entry m2....
11.2.2 可中断 (lockInterruptibly())
  • synchronizedreentrantlock.lock() 的锁, 是不可被打断的;也就是说别的线程已经获得了锁,我的线程就需要一直等待下去,不能中断

  • 可被中断的锁,通过lock.lockInterruptibly()获取的锁对象,可以通过调用阻塞线程的interrupt()方法来中断,让其停止阻塞,获得锁失败

  • 可中断的锁,在一定程度上可以被动的减少死锁的概率,之所以被动,是因为我们需要手动调用阻塞线程的interrupt方法;

/**
 * Description: 演示RenntrantLock中的可打断锁方法 lock.lockInterruptibly();
 */
@Slf4j(topic = "guizy.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            log.debug("t1线程启动...");
            try {
                // lockInterruptibly()是一个可打断的锁, 如果有锁竞争在进入阻塞队列后,可以通过interrupt进行打断
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等锁的过程中被打断"); //没有获得锁就被打断跑出的异常
                return;
            }
            try {
                log.debug("t1线程获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        // 主线程获得锁(此锁不可打断)
        lock.lock();
        log.debug("main线程获得了锁");
        // 启动t1线程
        t1.start();
        try {
            Sleeper.sleep(1);
            log.debug("执行打断");
            t1.interrupt();            //打断t1线程
        } finally {
            lock.unlock();
        }
    }
}
14:18:09.145 guizy.ReentrantTest [main] - main线程获得了锁
14:18:09.148 guizy.ReentrantTest [t1] - t1线程启动...
14:18:10.149 guizy.ReentrantTest [main] - 执行打断
14:18:10.149 guizy.ReentrantTest [t1] - 等锁的过程中被打断
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.guizy.reentrantlock.ReentrantTest.lambda$main$0(ReentrantTest.java:25)
	at java.lang.Thread.run(Thread.java:748)
11.2.3 锁超时 (lock.tryLock())

防止无限制等待,减少死锁

  • 使用 lock.tryLock() 方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。
  • 并且tryLock方法可以设置指定等待时间,参数为:tryLock(long timeout, TimeUnit unit) , 其中timeout为最长等待时间,TimeUnit为时间单位

获取锁的过程中,如果超过等待时间,或者被打断就直接从阻塞队列移除,此时获取锁就失败了,不会一直阻塞着 ! (可以用来实现死锁问题)

  • 不设置等待时间,立即失败
  • 设置等待时间,超过等待时间还没有获得锁,失败,从阻塞队列移除该线程
/**
 * Description: ReentrantLock, 演示RenntrantLock中的tryLock(long mills), 超过锁设置的等待时间,就从阻塞队列移除
 *
 * @author guizy1
 * @date 2020/12/23 13:50
 */
@Slf4j(topic = "guizy.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获得锁");
            try {
                // 设置等待时间, 超过等待时间 / 被打断, 都会获取锁失败; 退出阻塞队列
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("获取锁超时,返回");
                    return;
                }
            } catch (InterruptedException e) {
                log.debug("被打断了, 获取锁失败, 返回");
                e.printStackTrace();
                return;
            }
            try {
                log.debug("获得到锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("获得到锁");
        t1.start();
//        t1.interrupt();
        // 主线程2s之后才释放锁
        Sleeper.sleep(2);
        log.debug("main线程释放了锁");
        lock.unlock();
    }
}

// 超时的打印
14:55:56.647 guizy.WaitNotifyTest [main] - 获得到锁
14:55:56.651 guizy.WaitNotifyTest [t1] - 尝试获得锁
14:55:57.652 guizy.WaitNotifyTest [t1] - 获取锁超时,返回
14:55:58.652 guizy.WaitNotifyTest [main] - main线程释放了锁

// 中断的打印
23:04:13.062 [main] DEBUG guizy.ReentrantTest - 获得到锁
23:04:13.066 [t1] DEBUG guizy.ReentrantTest - 尝试获得锁
23:04:13.067 [t1] DEBUG guizy.ReentrantTest - 被打断了, 获取锁失败, 返回
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireNanos(AbstractQueuedSynchronizer.java:1245)
	at java.util.concurrent.locks.ReentrantLock.tryLock(ReentrantLock.java:442)
	at com.fucking.algorithm.ReentrantTest.lambda$main$0(ReentrantTest.java:20)
	at java.lang.Thread.run(Thread.java:748)
23:04:15.076 [main] DEBUG guizy.ReentrantTest - main线程释放了锁
java.lang.InterruptedException
通过lock.tryLock()来解决 哲学家就餐问题 (重点)
/**
 * Description: 使用了ReentrantLock锁, 该类中有一个tryLock()方法, 在指定时间内获取不到锁对象, 就从阻塞队列移除,不用一直等待。
 *              当获取了左手边的筷子之后, 尝试获取右手边的筷子, 如果该筷子被其他哲学家占用, 获取失败, 此时就先把自己左手边的筷子, 给释放掉. 这样就避免了死锁问题
 */
@Slf4j(topic = "guizy.PhilosopherEat")
public class PhilosopherEat {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

@Slf4j(topic = "guizy.Philosopher")
class Philosopher extends Thread {
    final Chopstick left;
    final Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            // 获得了左手边筷子 (针对五个哲学家, 它们刚开始肯定都可获得左筷子)
            if (left.tryLock()) {
                try {
                	// 此时发现它的right筷子被占用了, 使用tryLock(), 
                	// 尝试获取失败, 此时它就会将自己左筷子也释放掉
                    // 临界区代码
                    if (right.tryLock()) { //尝试获取右手边筷子, 如果获取失败, 则会释放左边的筷子
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

// 继承ReentrantLock, 让筷子类称为锁
class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
11.2.4 公平锁 new ReentrantLock(true)
  • ReentrantLock默认是非公平锁,可以指定为公平锁。

  • 在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。一般不设置ReentrantLock为公平的,,会降低并发度

  • Synchronized底层的Monitor锁就是不公平的,和谁先进入阻塞队列是没有关系的。

//默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);
11.2.5 条件变量 (可避免虚假唤醒)
  • Synchronized 中也有条件变量,就是Monitor监视器中的 waitSet等待集合,当条件不满足时进入waitSet 等待
  • ReentrantLock的条件变量比 synchronized 强大之处在于,它是 支持多个条件变量
  • 这就好比synchronized 是那些不满足条件的线程都在一间休息室等通知;(此时会造成虚假唤醒),而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒;(可以避免虚假唤醒)

使用要点:

  • await前需要 获得锁
  • await 执行后,会释放锁进入 conditionObject (条件变量) 中等待
  • await 的线程被唤醒(或打断、或超时)去重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行
  • signal方法用来唤醒条件变量(等待室)汇总的某一个等待的线程
  • signalAll方法,唤醒条件变量(休息室)中的所有线程
/**
 * Description: ReentrantLock可以设置多个条件变量(多个休息室), 相对于synchronized底层monitor锁中waitSet
 */
@Slf4j(topic = "guizy.ConditionVariable")
public class ConditionVariable {
    private static boolean hasCigarette = false;
    private static boolean hasTakeout = false;
    private static final ReentrantLock lock = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = lock.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = lock.newCondition();

    public static void main(String[] args) {

        new Thread(() -> {
        	// 1 加锁
            lock.lock();
            // 2 临界区
            try {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        // 此时小南进入到 等烟的休息室,等待送烟的唤醒
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("烟来咯, 可以开始干活了");
            } finally {
            // 3 解锁 
                lock.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        // 此时小女进入到 等外卖的休息室,等待送外卖的唤醒
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外卖来咯, 可以开始干活了");
            } finally {
                lock.unlock();
            }
        }, "小女").start();

        Sleeper.sleep(1);
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("送外卖的来咯~");
                hasTakeout = true;
                // 唤醒等外卖的小女线程
                waitTakeoutSet.signal();
            } finally {
                lock.unlock();
            }
        }, "送外卖的").start();

        Sleeper.sleep(1);
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("送烟的来咯~");
                hasCigarette = true;
                // 唤醒等烟的小南线程
                waitCigaretteSet.signal();
            } finally {
                lock.unlock();
            }
        }, "送烟的").start();
    }
}

十二、同步模式之顺序控制 (案例)

12.1 顺序输出

假如有两个线程,线程A打印1,线程B打印2。
要求:程序先打印2,再打印1

12.1.1 Wait/Notify版本实现
/**
 * Description: 使用wait/notify来实现顺序打印 2, 1
 */
@Slf4j(topic = "guizy.SyncPrintWaitTest")
public class SyncPrintWaitTest {

    public static final Object lock = new Object();
    // t2线程释放执行过
    public static boolean t2Runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!t2Runned) {
                    try {
                    	// 进入等待(waitset), 会释放锁
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                t2Runned = true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
12.1.2 使用ReentrantLock的await/signal
/**
 * Description: 使用ReentrantLock的await/sinal 来实现顺序打印 2, 1
 */
@Slf4j(topic = "guizy.SyncPrintWaitTest")
public class SyncPrintWaitTest {

    public static final ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();
    // t2线程释放执行过
    public static boolean t2Runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                // 临界区
                while (!t2Runned) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            } finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("2");
                t2Runned = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}
12.1.3 使用LockSupport中的park/unpart
/**
 * Description: 使用LockSupport中的park,unpark来实现, 顺序打印 2, 1
 */
@Slf4j(topic = "guizy.SyncPrintWaitTest")
public class SyncPrintWaitTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");
        t1.start();

        new Thread(() -> {
            log.debug("2");
            LockSupport.unpark(t1);
        }, "t2").start();
    }
}
21:46:02.138 [t2] DEBUG com.fucking.algorithm.SyncPrintWaitTest - 2
21:46:02.148 [t1] DEBUG com.fucking.algorithm.SyncPrintWaitTest - 1

12.2 交替输出

需求

  • 线程1 输出 a 5次, 线程2 输出 b 5次, 线程3 输出 c 5次。现在要求输出格式abcabcabcabcabcabc
12.2.1 wait/notify版本
/**
 * Description: 使用wait/notify来实现三个线程交替打印abcabcabcabcabc
 */
public class TestWaitNotify {
    public static void main(String[] args) {
        WaitNotify waitNotify = new WaitNotify(1, 5);

        new Thread(() -> {
            waitNotify.print("a", 1, 2);

        }, "a线程").start();

        new Thread(() -> {
            waitNotify.print("b", 2, 3);

        }, "b线程").start();

        new Thread(() -> {
            waitNotify.print("c", 3, 1);

        }, "c线程").start();
    }
}

class WaitNotify {

    private int flag;
    
    // 循环次数
    private int loopNumber;
    
   	public WaitNotify (int flag, int loopNum) {
          this.flag = flag;
          this.loopNum = loopNum;
    }
    /*
        输出内容    等待标记    下一个标记
        a           1          2
        b           2          3
        c           3          1
     */
    public void print(String str, int waitFlag, int nextFlag) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (waitFlag != this.flag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                this.flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}
12.2.2 await/signal版本
/**
 * Description: 使用await/signal来实现三个线程交替打印abcabcabcabcabc
 */
@Slf4j(topic = "guizy.TestWaitNotify")
public class TestAwaitSignal {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a_condition = awaitSignal.newCondition();
        Condition b_condition = awaitSignal.newCondition();
        Condition c_condition = awaitSignal.newCondition();

        new Thread(() -> {
            awaitSignal.print("a", a_condition, b_condition);
        }, "a").start();

        new Thread(() -> {
            awaitSignal.print("b", b_condition, c_condition);
        }, "b").start();

        new Thread(() -> {
            awaitSignal.print("c", c_condition, a_condition);
        }, "c").start();

        Thread.sleep(1000);
        System.out.println("==========开始=========");
        awaitSignal.lock();
        try {
            a_condition.signal();  //首先唤醒a线程
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {
    private final int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Condition condition, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            lock();
            try {
                try {
                    condition.await();
                    //System.out.print("i:==="+i);
                    System.out.print(str);
                    next.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                unlock();
            }
        }
    }
}
12.2.3 LockSupport的park/unpark实现
/**
 * Description: 使用park/unpark来实现三个线程交替打印abcabcabcabcabc
 */
@Slf4j(topic = "guizy.TestWaitNotify")
public class TestParkUnpark {
    static Thread a;
    static Thread b;
    static Thread c;

    public static void main(String[] args) {
        ParkUnpark parkUnpark = new ParkUnpark(5);

        a = new Thread(() -> {
            parkUnpark.print("a", b);
        }, "a");

        b = new Thread(() -> {
            parkUnpark.print("b", c);
        }, "b");

        c = new Thread(() -> {
            parkUnpark.print("c", a);
        }, "c");

        a.start();
        b.start();
        c.start();

        LockSupport.unpark(a);

    }
}

class ParkUnpark {
    private final int loopNumber;

    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Thread nextThread) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(nextThread);
        }
    }
}

十三、本章小结

JUC笔记-共享模型之管程 (Monitor)_第33张图片JUC笔记-共享模型之管程 (Monitor)_第34张图片


参考:

  • 学习《Java并发编程》目录索引—共享模型之管程 (Monitor)
  • JavaNotes—JUC

你可能感兴趣的:(#,Java并发编程,java,后端,juc)