Java并发编程——管程(Monitor)

管程(Monitor)

​ 管程(Monitor),Java 中被翻译为监视器,管程是它在操作系统层面的名称,所谓管程,即管理共享变量及对其操作过程,让它支持并发访问,在 Java 中可以理解为管理类的成员变量和方法,从而达到线程安全的目的

共享带来的问题

​ 假设此时有两个线程同时对初始值为0的静态变量做相同次数的自增和自减:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; ++i) counter++;
}, "t1");

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; ++i) counter--;
}, "t2");

t1.start();
t2.start();
t1.join();
t2.join();

log.debug("is {}", counter);
}

​ 按常理,这个加一次减一次,最后的结果应该是0吧,让我们来看看运行结果:

1
2
3
16:01:50 [main] c.Test1 - is -1768

Process finished with exit code 0

​ 显然,这个程序的运行结果不是0,为什么会出现这个问题呢?我们需要从字节码来进行分析

​ 对于i++操作来说,实际上会产生四行字节码指令getstatic iiconst_1iaddputstatic,而在 Java 内存模型中,完成静态变量的自增自减,需要在主存和工作内存中进行数据交换:

1
2
3
4
 					《---》 线程 1 i++
主内存
static int i = 0
《---》 线程 2 i--

​ 由于线程占用 cpu 资源的顺序是随机的,所以i++i--这两行代码所对应的8行字节码指令,可能会出现交错运行的情况:线程 1 从主存中获取了 i的值为0,还没进行++操作时,线程 2 也取到了i = 0,这时线程 1 和线程 2 分别进行++--操作后,线程 1 先将i = 1返回给主存,随后线程 2 又将i = -1返回给主存,这时主存中的i值即为-1,在这种情况多次重复运行后,i的值根本无法预测

​ 如何解决在并发情况下的这种问题呢?这时要引入临界区(Critical Section)的概念

临界区(Critical Section)

​ 上述程序中,一个程序运行多个线程本身是没有问题的,问题出现在多个线程同时访问共享资源,而多个线程访问共享资源本身也是没有问题的,问题的根本在于:在多个线程对共享资源进行读写操作时发生了指令交错,而当一段代码块内如果存在对共享资源的多线程读写操作,我们就称这段代码块为临界区

​ 很明显,上一个例子中的临界区即为counter++counter--这两行代码,多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件,为了避免临界区的竞态条件发生,可以使用:

  • 阻塞时解决方案:synchronized、Lock
  • 非阻塞式解决方案:原子变量

## synchronized 解决方案

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

synchronized 用法

​ 【对象锁】在使用时,只需要将临界区代码放在synchronized代码块内即可,即:

1
2
3
4
5
6
7
8
9
static Object room = new Object();
...
synchronized(room) {
conuter++;
}
...
synchronized(room) {
conuter--;
}

​ 这种写法就保证了

-------------本文结束感谢您的阅读-------------