文章目录
前言
管程是什么?
管程(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)