并发4️⃣管程①问题引入、synchronized
1、共享的问题
1.1、故事引入
老王(操作系统)有一个算盘(CPU)
- 张三和李四是同一个客栈的伙计,张三负责收钱、李四负责采购。
- 二人互不知道对方的资金情况。
张三(线程)要来借算盘,负责记客栈的收入
- 张三不可能无时不刻地使用算盘
- 会有不使用算盘的时候(阻塞),比如
- 休息(sleep)
- 吃饭或上厕所(BIO)
- 等别的事情做完再继续(wait)。
李四(另一个线程)也要来借算盘(共享),负责记客栈的支出
- 张三和李四不能同时使用,因此需要轮流使用(分时系统)
- 由老王决定让谁使用算盘(CPU 调度,分配时间片)
- 当张三阻塞时,老王让李四使用算盘,反之亦然(线程上下文切换)
账本
- 张三和李四每次都先把运算结果记在脑子里(工作内存),再记到账本上(主内存)。
- 每次要看结果或者修改结果,就要用到账本。
并发问题:某天客栈现金 1000,收入 500,支出 200。
-
张三使用算盘
- 看到账本上的结果是 1000,用算盘算出新结果是 1500。
- 张三还没把新结果写到账本上,先跑去上厕所(BIO)。
-
老王看到张三去上厕所(线程阻塞),让李四使用算盘(线程上下文切换)
- 李四看到账本上的结果是 1000,用算盘算出新结果是 800。
- 把新结果 800 写到账本上。
-
张三上完厕所,老王让张三使用算盘,张三把刚才算的 1500 写到账本上。
-
账本上记录的今日结余为 1500,而实际本应是 1300。
1.2、Java 共享问题
1.2.1、问题
从 Java 的角度,体现共享的问题
-
两个线程对同一个静态变量进行写操作,一个做自增,一个做自减。
-
正常情况下,正确结果是 0。但实际运行结果是不确定的。
-
进行 10 次测试,结果有正、负、0 三种情况。
1.2.2、字节码
涉及 JVM 的字节码技术
注:所有变量的操作,都是在栈帧的操作数栈上进行的。
静态变量 i 的自增、自减操作,对应 4 行字节码
-
将静态变量 i 加载到操作数栈顶。
-
将常量池中的整数 1 加载到操作数栈顶。
-
操作数栈顶的两个数相加(相减)并出栈,将结果入栈
-
将结果赋值给静态变量 i
// i++对应字节码 getstatic i iconst_1 iadd putstatic i // i-- 对应字节码 getstatic i iconst_1 isub putstatic i
1.2.3、主内存与工作内存
Java 内存模型(JMM)
-
主内存:所有变量都存储在主内存。
-
工作内存:每个线程有自己的工作内存,其中保存了该线程所用变量的主内存副本。
-
线程对变量的读写操作只在工作内存中进行,不能直接读写主内存中的数据。
-
线程之间也无法直接访问对方工作内存的变量,需要通过主内存来传递。
-
1.2.4、分析
如果字节码指令的执行具有原子性,则不会出错。
-
若
i++
和i--
对应的 4 行字节码能作为一个整体执行,在执行的过程中不会发生交错。 -
即
i++
的字节码都执行完,再执行i--
的字节码(反之亦然)
在多线程下,这些字节码指令不具有原子性。
类似 1.1 故事中的并发问题,字节码指令发生交错。
-
线程 t1 读取变量值(假设是 0,计算出结果 1),执行
putstatic
(保存结果)之前发生线程上下文切换。 -
线程 t2 读取变量值(0),计算出结果(-1)并保存到主内存。
-
线程 t1 执行
putstatic
将结果(1)保存到主内存。 -
此时结果为 1,而实际应该是 0。
1.3、临界区 & 竞态条件(❗)
临界区:多线程下,对共享资源执行读写操作的代码块。
- 单线程读写不会出错。
- 多线程读不会出错。
- 多线程读写会发生指令交错。
发生竞态条件:多个线程执行临界区代码,由于代码的执行序列不同,无法预测结果。
避免发生竞态条件的 2 类解决方案
- 阻塞式:synchronized、Lock
- 非阻塞式:原子变量
2、synchronized(❗)
synchronized 也叫对象锁
互斥思想(悲观锁)
- 同一时刻最多只有一个线程能持有对象锁,此时其它线程若想获取同一个对象锁就会阻塞。
- 持有对象锁的线程,可以安全的执行临界区内的代码。
- 持有对象锁的线程也会发生线程上下文切换,只是即使发生了上下文切换,其它线程也无法拿到同一个对象的对象锁。
语法
-
对象:必须非 null
synchronized(对象) { // 临界区 }
-
方法
权限修饰符 synchronized [static] 返回值 方法名() { // 内部有临界区代码 }
2.1、synchronized 对象
2.1.1、解决共享问题
使用此方式解决 1.2.1 的共享问题。
-
声明一个 Object 类型的共享对象(不能为 null),用于线程获取对象锁。
-
进行 10 次测试,均为正确结果
2.1.2、理解
基于本文 1.1 的故事案例
为了避免并发问题,计算工作要在房间(对象)里完成
- 每次只能有一个人进房间(互斥),房间只有一把钥匙。
- 轮到张三(线程)使用算盘时(分配到时间片)
- 进入房间并且锁门拿走钥匙(持有对象锁)
- 在房间内算账(读写共享变量)
此时李四也要使用算盘,但是房间被锁住了,只能在外面等(阻塞)。
- 张三阻塞、或老王决定让李四使用(时间片用完)的时候,张三仍拿着钥匙,从房间里出来
- 轮到李四使用(线程上下文切换),但是钥匙还在张三那里,只能等张三算完账再进去。
- 张三继续使用(线程上下文切换),把账算完并记录在账本上(将结果写回主内存),
张三开锁从房间出来(释放锁)
- 张三叫了李四(唤醒阻塞线程),把钥匙给他,此时李四才可以进房间。
- 如果李四进去算账了,张三也只能等李四算完账写到账本后才能进去。
说明
- 如果张三出来的时候看到有 2 个人,那就看这两个人谁先抢到钥匙(竞争锁)
- 如果王五没有老实等待(没有加对象锁),在张三上厕所(阻塞)的时候翻窗进去算账,也会造成并发问题。
原理图
-
synchronized 对象锁保证临界区代码的原子性。
-
即临界区内的代码(字节码)不可分割,执行结果不会被线程上下文切换影响。
2.1.3、思考
思考以下几种情况
Q1:把 synchronized(obj)
放在 for 循环外面
- 使 for 循环具有原子性,线程执行完该循环,其它线程才有机会进入该循环。
- 相比把
synchornized(obj)
放在 for 里面,可以减少加锁和释放次数,降低资源浪费。
Q2:t1 加了 synchronized(obj)
,但 t2 没有
- t2 运行时不会尝试获取锁,而是直接执行。
- 即使 t1 持有对象锁,t2 照常执行,不会阻塞。
Q3:t1 加了 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 类提供的方法。
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