JUC学习笔记——共享模型之管程
作者:互联网
实际代码体现
// 针对counter,我们一个线程++,一个线程--各运行5000次
static int counter = 0;
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("{}",counter);
}
// 但结果却不是0,经常为-5000~5000之间的数
我们可以从底层代码分析问题:
/*i++底层代码*/
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
/*i--底层代码*/
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
我们会发现他们的底层代码并不是一步实现,而是多步操作一同实现
但是如果是多线程,就会因为上下文切换的缘由导致部分步骤出现交杂(我们给出正数示例):
临界区和竞态条件
首先我们来简单介绍一下临界区:
- 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
- 例如我们上述共享问题中的i就是共享资源,而对i操作的i++和i--操作都可以被称为临界区
针对临界区我们需要注意以下内容:
- 一个程序运行多个线程本身是没有问题的
- 多个线程读共享资源其实也没有问题
- 但是在多个线程对共享资源读写操作时发生指令交错,就会出现问题
- 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
共享问题解决方案
这小节我们将会介绍共享问题解决方案
共享问题解决方案总述
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized简述
- 俗称的【对象锁】,采用互斥的方式使目前至多只有一个线程能持有【对象锁】其它线程再想获取这个【对象锁】时就会阻塞住。
- 这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
我们先来介绍synchronized的语法:
// 线程1, 线程2 都使用同一对象作为锁,这样一个运行,另一个处于blocked阻塞
synchronized(对象)
{
临界区
}
我们再给出相关代码示例:
// 我们创建一个room对象来作为锁,注意处理共享问题的线程需要绑定同一个锁
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
我们做简单解释:
- synchronized相当于一个方法用来设置一个房间
- room对象相当于一个锁,这个锁控制着房间,而房间中放着所有对应的synchronized里面的代码
- 多个线程谁先进入room就可以获得钥匙,然后如果想要进入这个房间操作,只有有钥匙的线程才可以
- 当该线程操作结束后,就会主动将钥匙让出来,其他线程就可以进行抢夺钥匙,哪个线程获得钥匙就可以继续操作
- 同时我们需要注意时间片结束并不意味着解开锁,就算轮到其他线程的时间片,他们也不能进入到房间里去执行他们的代码