文章目录
- 前言
- 1. 共享带来的问题
- 1.1 问题演示
- 1.2 问题分析
- 1.3 结论
- 1.4 临界区
- 1.5 竞态条件
- 2. synchronized解决方案
- 2.1 synchronized
- 语法
- 解决
- 语法为什么需要传入对象?
- 思考
- 面向对象改进
- 3. 方法上的synchronized
- 3.1 写法
- 3.2 经典问题 “线程八锁”
- 情况1 :
- 情况2 :
- 情况3 :
- 情况4 :
- 情况5 :
- 情况6 :
- 情况7 :
- 情况8 :
前言
管程是什么?
管程(Monitor)是一种高级的同步机制,它将共享数据和对这些数据的操作封装在一起,保证在某一时刻只有一个线程能够执行被管程保护的代码。换句话说,管程是一种对对象的同步控制,确保对象的所有操作都是线程安全的。
在 Java 中,管程的实现依赖于 对象锁 和 条件变量。每个对象都有一个与之关联的锁(monitor
),以及与该锁相关联的 条件变量(如 wait()
、notify()
、notifyAll()
)。这些机制共同工作,保证线程间的互斥和协作。
1. 共享带来的问题
1.1 问题演示
我们用一个代码来体现,共享可能带来的问题。
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();
log.debug("count值为:{}", count);
上面的结果,理论上应该是0。但是最终结果却有可能是正数,有可能是负数,可能是0。
三次运行的结果:
1.2 问题分析
- 之所以出现这个问题,是为什么呢?那是因为Java中对于静态变量的自增和自减操作不是原子操作,因此需要从jvm字节码的角度来分析。
- 如果是单线程执行的话,不会有任何交错的问题产生,如图:
- 但是多线程情况下有可能出现交错运行,导致出错的情况:
1.3 结论
之所以出现结果不同的情况,根本原因是,线程之间上下文切换导致的指令交错,最终使得结果不同。
1.4 临界区
- 一个程序运行多个线程没有问题
- 问题出在多个线程访问共享资源
- 共同访问也没啥问题
- 但是多个线程对共享资源访问,产生指令交错,就会出现问题。
- 一段代码块如果存在对共享资源的多线程读写操作,就称这段代码块为临界区。
eg,下面代码中的临界区:
static int count = 0;static void increment() {// 临界区count ++;
}
static void decrement() {// 临界区count --;
}
1.5 竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
2. synchronized解决方案
- 阻塞式解决方案 : synchronized、Lock
- 非阻塞式解决方案:原子变量
2.1 synchronized
- synchronized : 即 “对象锁”。
它通过采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程想再获得这个【对象锁】时就会被阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文的切换。
语法
synchronized (对象) { // 线程1、线程2(blocked)//临界区
}
解决
static int count = 0;static Object object = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()-> {for(int i = 0; i < 5000; i ++ ) {synchronized (object) {count ++;}}}, "t1");Thread t2 = new Thread(()-> {for(int i = 0; i < 5000; i ++ ) {synchronized (object) {count --;}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("count值为:{}", count);}
运行结果:
语法为什么需要传入对象?
以下回答来自gpt4
同步锁机制的本质:
- Java 中的同步锁是基于对象的,具体来说,每个对象都有一个关联的锁。当一个线程需要执行一个同步块时,它必须先获得相应对象的锁。
synchronized
关键字后面跟随的对象(比如object
)就是用来控制进入该同步块的线程。只有获取到锁的线程才能进入该同步代码块,其他线程必须等待锁释放。
锁的粒度和作用域:
- 在这段代码中,
object
是一个静态对象,它是所有线程共享的。synchronized (object)
语句中的object
是指锁定这个特定的对象。当某个线程执行synchronized (object)
时,它会尝试获取object
的锁。如果另一个线程已经获得了object
的锁,那么该线程会被阻塞,直到锁被释放。
对象本身就是一个锁:
- 在 Java 中,每个对象都有一个关联的锁。锁的作用是防止多个线程同时访问某个共享资源或执行某段代码。在同步代码块中,我们可以指定任意对象作为锁,例如
object
。 object
作为锁的作用是保证在任何时刻,只有一个线程可以进入同步代码块。其他线程必须等待当前线程释放锁。
锁定静态对象:
- 在代码中,
object
是static
类型的,这意味着它是类级别的变量,而不是每个线程独立拥有的。这样所有线程都共享这个object
,因此它充当了同步锁,能够保证对count
变量的操作是互斥的。 - 静态变量
object
作为锁对象,能够在所有线程间共享锁,从而实现线程间对共享资源(count
)的同步访问。
思考
面向对象改进
public class Test13 {public static void main(String[] args) throws InterruptedException {Room room = new Room();Thread t1 = new Thread(() -> {room.increment();}, "t1");Thread t2 = new Thread(() -> {room.decrement();}, "t2");log.debug("count值为:{}", room.getCounter());}}class Room {private int counter = 0;public void increment() {synchronized (this) {counter ++;}}public void decrement() {synchronized (this) {counter --;}}public int getCounter() {synchronized (this) {return counter;}}}
将对共享资源的操作直接封装到room类中,Room类本身充当对象锁,这样每次操作就一定是原子的,不会出错。
3. 方法上的synchronized
3.1 写法
①、
public void increment() {synchronized (this) {counter ++;}
}
等价于 :
public synchronized void increment() {counter++;
}
②、
class Test{public static void test() {synchronized (Test.class) {}}
}
等价于 :
class Test{public synchronized static void test() {}
}
3.2 经典问题 “线程八锁”
该问题主要考察synchronized锁住哪一个对象。
情况1 :
运行结果 : 12、21
@Slf4j(topic = "c.Number")
class Number {public synchronized void a() {log.debug("1");}public synchronized void b() {log.debug("2");}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{n1.a();}).start();new Thread(()->{n1.b();}).start();}
}
运行结果:
情况2 :
运行结果 : 1秒后 12、或 2 1秒后 1
public synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}public synchronized void b() {log.debug("2");
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{n1.a();}).start();new Thread(()->{n1.b();}).start();
}
运行结果:
情况3 :
运行结果 : 3 1秒后 12 、 23 1秒后 1 、 32 1秒后 1
public synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}public synchronized void b() {log.debug("2");
}
public void c() {log.debug("3");
}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();
}
运行结果 :
情况4 :
运行结果 : 2 1秒后 1
public synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}
public synchronized void b() {log.debug("2");
}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{n1.a();}).start();new Thread(()->{n2.b();}).start();
}
结果 :
- 因为两个对象不互斥,因此,2一定比1先打印出来。
情况5 :
运行结果 : 2 1秒后 1
public static synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}
public synchronized void b() {log.debug("2");
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{n1.a();}).start();new Thread(()->{n1.b();}).start();
}
结果 :
执行流程
- 线程 1 执行
a()
,它会首先尝试获取类级别的锁(即Number.class
)。成功后,它开始执行a()
方法,然后进入Thread.sleep(1000)
,在锁内休眠 1 秒。 - 线程 2 执行
b()
,它会尝试获取实例锁(即n1
的锁)。因为a()
是static synchronized
,线程 2 不会被阻塞等待a()
,它直接获得n1
实例的锁。 - 线程 2 执行
b()
,并且成功打印"2"
。 - 线程 1 在休眠结束后,继续执行并打印
"1"
。
锁的竞争
- 由于
a()
是类级别的锁,它只与其他静态同步方法竞争锁。 b()
是实例级别的锁,它只与其他synchronized
实例方法竞争锁。
因此,这两个线程 不会因为锁的竞争而发生阻塞,并行执行,结果是线程 2 会先打印 "2"
,然后线程 1 打印 "1"
。
情况6 :
运行结果 : 1秒后 12 、2 1秒后 1
public static synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}
public static synchronized void b() {log.debug("2");
}public static void main(String[] args) {Number n1 = new Number();new Thread(()->{n1.a();}).start();new Thread(()->{n1.b();}).start();
}
结果 :
情况7 :
运行结果 : 1秒后 12 、2 1秒后 1
public static synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}
public synchronized void b() {log.debug("2");
}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锁住的是类静态锁,整个class的锁,那应该不会影响实例锁的对象。而a方法每次执行时,都会停顿1秒,那这样的话,b应该永远是最先执行的。
为了解决这个疑惑,我问了gpt4 ,回答如下:
虽然 b() 方法的锁不会被 a() 的类锁影响,但线程调度顺序以及线程竞争的情况仍然会导致你看到两种执行顺序。这种行为是因为 Java 的线程调度是非确定性的,并且线程的执行顺序可能会有很多变化。
如果你希望强制 b() 先执行,可以通过确保线程 2 先启动来控制线程的执行顺序,但如果没有控制,b() 和 a() 的执行顺序还是取决于调度的具体情况。
情况要归结于线程的调度情况,因此有可能会出现a执行完,b才执行的情况。
但是总体来说,这种情况更常见一些:
情况8 :
运行结果 : 1秒后 12 、2 1秒后 1
public static synchronized void a() {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}log.debug("1");
}
public static synchronized void b() {log.debug("2");
}public static void main(String[] args) {Number n1 = new Number();Number n2 = new Number();new Thread(()->{n1.a();}).start();new Thread(()->{n2.b();}).start();
}
结果 :