并发4️⃣管程①问题引入、synchronized

1、共享的问题

1.1、故事引入

老王(操作系统)有一个算盘(CPU)

  • 张三和李四是同一个客栈的伙计,张三负责收钱、李四负责采购。
  • 二人互不知道对方的资金情况。

张三(线程)要来借算盘,负责记客栈的收入

  1. 张三不可能无时不刻地使用算盘
  2. 会有不使用算盘的时候(阻塞),比如
    • 休息(sleep
    • 吃饭或上厕所(BIO
    • 等别的事情做完再继续(wait)。

李四(另一个线程)也要来借算盘(共享),负责记客栈的支出

  1. 张三和李四不能同时使用,因此需要轮流使用分时系统
  2. 由老王决定让谁使用算盘(CPU 调度,分配时间片
  3. 当张三阻塞时,老王让李四使用算盘,反之亦然(线程上下文切换

账本

  1. 张三和李四每次都先把运算结果记在脑子里(工作内存),再记到账本上(主内存)。
  2. 每次要看结果或者修改结果,就要用到账本。

并发问题:某天客栈现金 1000,收入 500,支出 200。

  1. 张三使用算盘

    • 看到账本上的结果是 1000,用算盘算出新结果是 1500。
    • 张三还没把新结果写到账本上,先跑去上厕所(BIO)。
  2. 老王看到张三去上厕所(线程阻塞),让李四使用算盘(线程上下文切换

    • 李四看到账本上的结果是 1000,用算盘算出新结果是 800。
    • 把新结果 800 写到账本上。
  3. 张三上完厕所,老王让张三使用算盘,张三把刚才算的 1500 写到账本上。

  4. 账本上记录的今日结余为 1500,而实际本应是 1300。

    image-20220325020037444

1.2、Java 共享问题

1.2.1、问题

从 Java 的角度,体现共享的问题

  • 两个线程对同一个静态变量进行写操作,一个做自增,一个做自减。

    image-20220325172947059

  • 正常情况下,正确结果是 0。但实际运行结果是不确定的。

  • 进行 10 次测试,结果有正、负、0 三种情况。

    image-20220325173103464

1.2.2、字节码

涉及 JVM 的字节码技术

字节码技术(一)字节码技术(二)

所有变量的操作,都是在栈帧的操作数栈上进行的

静态变量 i 的自增、自减操作,对应 4 行字节码

  1. 将静态变量 i 加载到操作数栈顶。

  2. 将常量池中的整数 1 加载到操作数栈顶。

  3. 操作数栈顶的两个数相加(相减)并出栈,将结果入栈

  4. 将结果赋值给静态变量 i

    // i++对应字节码
    getstatic i
    iconst_1
    iadd
    putstatic i
    // i-- 对应字节码
    getstatic i
    iconst_1
    isub
    putstatic i
    

1.2.3、主内存与工作内存

Java 内存模型(JMM)

  • 主内存所有变量都存储在主内存

  • 工作内存:每个线程有自己的工作内存,其中保存了该线程所用变量的主内存副本

    • 线程对变量的读写操作只在工作内存中进行,不能直接读写主内存中的数据。

    • 线程之间也无法直接访问对方工作内存的变量,需要通过主内存来传递。

      image-20220306150024740

1.2.4、分析

如果字节码指令的执行具有原子性,则不会出错。

  • i++i-- 对应的 4 行字节码能作为一个整体执行,在执行的过程中不会发生交错。

  • i++ 的字节码都执行完,再执行 i-- 的字节码(反之亦然)

    image-20220325142708272

多线程下,这些字节码指令不具有原子性

类似 1.1 故事中的并发问题,字节码指令发生交错。

  1. 线程 t1 读取变量值(假设是 0,计算出结果 1),执行 putstatic (保存结果)之前发生线程上下文切换。

  2. 线程 t2 读取变量值(0),计算出结果(-1)并保存到主内存。

  3. 线程 t1 执行 putstatic 将结果(1)保存到主内存。

  4. 此时结果为 1,而实际应该是 0。

    image-20220325143351628

1.3、临界区 & 竞态条件(❗)

临界区多线程下,对共享资源执行读写操作的代码块。

  • 单线程读写不会出错。
  • 多线程读不会出错。
  • 多线程读写会发生指令交错。

发生竞态条件多个线程执行临界区代码,由于代码的执行序列不同,无法预测结果。

避免发生竞态条件的 2 类解决方案

  • 阻塞式:synchronized、Lock
  • 非阻塞式:原子变量

2、synchronized(❗)

synchronized 也叫对象锁

互斥思想(悲观锁)

  • 同一时刻最多只有一个线程能持有对象锁,此时其它线程若想获取同一个对象锁就会阻塞
  • 持有对象锁的线程,可以安全的执行临界区内的代码。
  • 持有对象锁的线程也会发生线程上下文切换,只是即使发生了上下文切换,其它线程也无法拿到同一个对象的对象锁。

语法

  • 对象:必须非 null

    synchronized(对象) {
        // 临界区
    }
    
  • 方法

    权限修饰符 synchronized [static] 返回值 方法名() {
        // 内部有临界区代码
    }
    

2.1、synchronized 对象

2.1.1、解决共享问题

使用此方式解决 1.2.1 的共享问题。

  • 声明一个 Object 类型的共享对象(不能为 null),用于线程获取对象锁。

    image-20220325173326414

  • 进行 10 次测试,均为正确结果

    image-20220325173441598

2.1.2、理解

基于本文 1.1 的故事案例

为了避免并发问题,计算工作要在房间(对象)里完成

  1. 每次只能有一个人进房间(互斥),房间只有一把钥匙。
  2. 轮到张三(线程)使用算盘时(分配到时间片
    • 进入房间并且锁门拿走钥匙(持有对象锁
    • 在房间内算账(读写共享变量

此时李四也要使用算盘,但是房间被锁住了,只能在外面等(阻塞)。

  1. 张三阻塞、或老王决定让李四使用(时间片用完)的时候,张三仍拿着钥匙,从房间里出来
  2. 轮到李四使用(线程上下文切换),但是钥匙还在张三那里,只能等张三算完账再进去。
  3. 张三继续使用(线程上下文切换),把账算完并记录在账本上(将结果写回主内存),

张三开锁从房间出来(释放锁

  1. 张三叫了李四(唤醒阻塞线程),把钥匙给他,此时李四才可以进房间。
  2. 如果李四进去算账了,张三也只能等李四算完账写到账本后才能进去。

说明

  1. 如果张三出来的时候看到有 2 个人,那就看这两个人谁先抢到钥匙(竞争锁
  2. 如果王五没有老实等待(没有加对象锁),在张三上厕所(阻塞)的时候翻窗进去算账,也会造成并发问题。

原理图

  • synchronized 对象锁保证临界区代码的原子性

  • 即临界区内的代码(字节码)不可分割,执行结果不会被线程上下文切换影响。

    image-20220325180153883

2.1.3、思考

思考以下几种情况

Q1synchronized(obj) 放在 for 循环外面

  • 使 for 循环具有原子性,线程执行完该循环,其它线程才有机会进入该循环。
  • 相比把 synchornized(obj) 放在 for 里面,可以减少加锁和释放次数,降低资源浪费。

Q2t1 加了 synchronized(obj),但 t2 没有

  • t2 运行时不会尝试获取锁,而是直接执行。
  • 即使 t1 持有对象锁,t2 照常执行,不会阻塞。

Q3t1 加了 synchronized(obj1),但 t2 加了 synchronized(obj2)

  • 两个线程锁的是不同对象,持有的是两个不同的对象锁
  • 类似 Q2,线程加锁不会阻塞另一个线程。

2.1.4、代码改进

在实际开发中,操作的共享变量往往属于某个具体的类。

封装本案例中的 count 变量和操作。

class Counter {
    private int count;
    public void increment() {
        synchronized (this) {
            count++;
        }
    }
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
	// getter
}

线程只需调用 Counter 类提供的方法。

image-20220325181707543

2.2、synchronized 方法(❗)

方法声明时使用 synchronized 关键字

  • 非静态方法:相当于锁住当前对象(this)

  • 静态方法:相当于锁住当前类对象(xxx.class)

    权限修饰符 synchronized [static] 返回值 方法名() {
        // 内部有临界区代码
    }
    
    • 每个类实例独有一把对象锁,即类示例的对象锁有多把。
    • 类对象(class)的锁唯一

示例

class MyCounter {
    public synchronized void increment() {
        count++;
    }
    public synchronized static void decrement() {
        count--;
    } 
}
// 相当于
public void increment() {
    synchronized (this) {
        count++;
    }
}
public static void decrement() {
    synchronized (MyCounter.class) {
        count++;
    }
}

2.3、线程八锁(❗)

考察 synchronized 锁住的对象。

分析以下 8 道题的运行结果,需要分类讨论(哪个线程先执行)

2.3.1、非静态

同时开启 2 个线程,分别调用同一对象的 a() 和 b()

class Number {
    public synchronized void a() {
        LogUtils.debug("A");
    }
    public synchronized void b() {
        LogUtils.debug("B");
    }
}

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

a() 和 b() 都是 synchronized(this)

  • a() 先执行:AB
  • b() 先执行:BA

2.3.2、非静态+阻塞

同时开启 2 个线程,分别调用同一对象的 a() 和 b()

class Number {
    public synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public synchronized void b() {
        LogUtils.debug("B");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(() -> n1.a()).start();
    new Thread(() -> n1.b()).start();
}

a() 和 b() 都是 synchronized(this)

  • a() 先执行:1秒 → AB
  • b() 先执行:B → 1秒 → A

2.3.3、非静态+不同步

同时开启 3 个线程,分别调用同一对象的 a() 和 b() 和 c()

class Number {
    public synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public synchronized void b() {
        LogUtils.debug("B");
    }
    public void c() {
        LogUtils.debug("C");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    new Thread(()-> n1.a()).start();
    new Thread(()-> n1.b()).start();
    new Thread(()-> n1.c()).start();
}

a() 和 b() 是 synchronized(this),c 不同步、不会阻塞

  • a() 先执行:C → 1秒 → AB
  • b() 先执行:BC(CB)→ 1秒 → A

2.3.4、非静态+不同对象

同时开启 2 个线程,分别调用不同对象的 a() 和 b()

class Number {
    public synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public synchronized void b() {
        LogUtils.debug("B");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(()-> n1.a()).start();
    new Thread(()-> n2.b()).start();
}

a() 和 b() 持有不同的对象锁,不发生阻塞:B → 1秒 → A

2.3.5、非静态+静态

同时开启 2 个线程,分别调用同一对象的静态 a() 和 b()

class Number{
    public static synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public synchronized void b() {
        LogUtils.debug("B");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    // n1.a() 实际上是 Number.a()
    new Thread(() -> n1.a()).start();
    new Thread(() -> n1.b()).start();
}

a() 是 synchronized(Number.class),b() 是 synchronized(this)

属于不同的对象锁,不会阻塞:B → 1秒 → A

2.3.6、静态

同时开启 2 个线程,分别调用同一对象的 a() 和 b()

class Number{
    public static synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public static synchronized void b() {
        LogUtils.debug("B");
    }
}
@Test
public void test(){
    Number n1 = new Number();
    // 相当于Number.a()和Number.b()
    new Thread(() -> n1.a()).start();
    new Thread(() -> n1.b()).start();
}

a() 和 b() 都是 synchronized(Number.class)

  • a() 先执行:1秒 → AB
  • b() 先执行:B → 1秒 → A

2.3.7、非静态+静态+不同对象

同时开启 2 个线程,分别调用不同对象的 a() 和 b()

class Number{
    public static synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public synchronized void b() {
        LogUtils.debug("B");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(() -> n1.a()).start();
    new Thread(() -> n1.b()).start();
}

a() 和 b() 持有不同的对象锁,不发生阻塞:B → 1秒 → A

2.3.8、静态+不同对象(❗)

同时开启 2 个线程,分别调用不同对象的 a() 和 b()

class Number{
    public static synchronized void a() {
        SleepUtils.sleepSeconds(1);
        LogUtils.debug("A");
    }
    public static synchronized void b() {
        LogUtils.debug("B");
    }
}
public static void main(String[] args) {
    Number n1 = new Number();
    Number n2 = new Number();
    new Thread(() -> n1.a()).start();
    new Thread(() -> n1.b()).start();
}

a() 和 b() 都是 synchronized(Number.class),class 对象锁是唯一的

  • a() 先执行:1秒 → AB
  • b() 先执行:B → 1秒 → A
posted @ 2022-04-01 18:03  Jaywee  阅读(52)  评论(2编辑  收藏  举报

👇