2021-08-15
上下文切换对非原子操作的影响
示例
我们都知道,两个线程 Thread0 与 Thread1,对同一个量 count 进行操作;一个让其自增,一个让其自减,次数相等;结果可能为 负数、正数、0(极少)
示例代码
package com.juc.ppy.synchronize;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.UnsafeCount")
public class UnsafeCount {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0; i < 50000; i++){
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int i = 0; i < 50000; i++){
count--;
}
}, "t2");
// 启动线程 t1 t2
t1.start();
t2.start();
// 主线程等待 t1 t2 执行完毕
t1.join();
t2.join();
log.debug("最终结果{}", count);
}
}
原因
上下文:存储器与程序计数器中线程执行的进度信息
上下文切换: ①Thread0 时间片用完,操作系统保存 Thread0 上下文到内存,Thread0 暂停运行,让出处理器使用权(切出)
②加载 Thread1 的上下文,Thread1 获得处理器使用权,开始或继续运行(切入)…
③Thread0 再次获取处理器使用权,继续执行
详细分析
count++ 看似只有一步操作,其实从字节码看,有四步操作
① getstatic i // 取出静态变量 i 的值
② iconst_1 // 准备常量 1
③ iadd // 自增
④ putstatic i // 将修改后的值存入静态变量 i
可能在 Thread0 中 count 自增之后的结果还没有重新存入 count 时,时间片用完了,轮到Thread1 使用处理器;Thread1 获取到的值还是旧值
原子性:一个或一系列操作不会被线程调度(上下文切换)所打断
综上:因为静态变量 count 自增自减操作是非原子性的 + 并发情况下,线程上下文切换;导致结果出现负数与正数
临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
count++ 与 count-- 既有读又有写
1. 线程运行多个线程是没有问题的
2. 多个线程读共享资源也没有问题
3. 当多个线程对共享资源进行读写操作时,线程切换(指令交错),就会出问题
竟态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
通过 synchronized 解决
package com.juc.ppy.synchronize;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.UnsafeCount")
public class UnsafeCount {
static int count = 0;
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0; i < 50000; i++){
synchronized(lock){
count++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int i = 0; i < 50000; i++){
synchronized(lock){
count--;
}
}
}, "t2");
// 启动线程 t1 t2
t1.start();
t2.start();
// 主线程等待 t1 t2 执行完毕
t1.join();
t2.join();
log.debug("最终结果{}", count);
}
}
为了避免临界区的竞态条件发生(避免在线程对共享资源进行读写时,发生线程切换对结果造成影响,即其他线程也可以读写);此处使用 synchronized 保证了临界区代码的原子性
synchronized:俗称对象锁,采用互斥方式;同一时刻,只能有一个线程拥有锁(Thread0);其他线程想获取该锁,就会被阻塞(Thread1);保证 Thread0 能正确执行完临界区代码(不代表不会发生上下文切换)
即使时间片用完,切换成 Thread1;由于 ①Thread0 还没有执行完临界区代码,②Thread1 无法获取锁,就无法执行临界区代码;③所以影响不到Thread0
2021-08-16
前几天因为家里人要做手术忙不开,学习计划就拖到了08.14才开始。