synchronized 保证可见性、原子性、有序性
一、概述
并发三大特性即 可见性、原子性、有序性
可见性: 一个线程修改了共享变量的值,另外一个线程应该立即得到共享变量的最新值
原子性: 一个或多个操作要么全部执行,并且在执行的过程中不会被其它因素打断,要么全部不执行
有序性: 为了提高程序运行效率,Java 在编译和运行时会对指令进行重排序,重排序后的指令可以保证单线程环境下程序的最终结果一致,但是多线程情况下可能会出现不符合预期的结果
二、测试
2.1、可见性
@Slf4j
public class Visibility {
// 定义共享变量
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
}, "t1").start();
// main 线程休眠 2s
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
flag = false;
log.info(Thread.currentThread().getName() + " 修改共享变量 flag 的值为 " + false);
}, "t2").start();
}
}
从上面程序的执行结果可以看出,t2 线程修改了 flag 的值为 false 之后,t1 线程并没有停止,一直在执行 while(true) 循环,也就是说线程 t2 修改了 flag 的值后,t1 线程并没有看到修改后的 flag 的新值
添加 synchronized 对上面的代码进行改造
@Slf4j
public class Visibility {
// 定义共享变量
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
synchronized (new Object()) {
log.info("flag 的值为: {}", flag);
}
}
}, "t1").start();
// main 线程休眠 2s
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
flag = false;
log.info(Thread.currentThread().getName() + " 修改共享变量 flag 的值为 " + false);
}, "t2").start();
}
}
上述代码添加 synchronized 之后,整个程序就会停下来,不会一直循环下去了
2.2、原子性
@Slf4j
public class Atomicity {
private static int count = 0;
private static final int THREAD_NUMBER = 50;
private static final int CIRCLE_NUMBER = 1000;
private static void increase() {
count++;
}
public static void main(String[] args) throws InterruptedException {
// 定义线程数组
Thread[] threadGroup = new Thread[THREAD_NUMBER];
for (int i = 0; i < threadGroup.length; i++) {
threadGroup[i] = new Thread(() -> {
for (int j = 0; j < CIRCLE_NUMBER; j++) {
increase();
}
});
threadGroup[i].start();
}
// main 线程等待线程数组中的所有线程执行完
for (int i = 0; i < threadGroup.length; i++) {
threadGroup[i].join();
}
log.info("count 的值为: {}", count);
}
}
50 个线程,每个线程循环执行 1000 次的 count++ 操作,我们预期值是 50000,但是多次执行程序得到的结果都是一个小于 50000 的值(极少数的情况下会出现 50000),上面的程序为什么不能得到我们的预期值 50000 呢
究其原因是 count++ 并不是一个原子性操作,其底层对应着 4 条 JVM 指令(甚至是对应机器指令层面的更多的微指令)
0 getstatic // 从主内存中获取 count 的值
3 iconst_1 // 准备常量 1
4 iadd // 将 count 与常量进行相加
5 putstatic // 将 count 的值写回主内存
8 return
例如在并发场景下两个线程进行 count++ 操作,可能会发生如下情况
时间节点 | 线程 1 | 线程 2 |
t1 | 从主内存中获取 count 的值,此时 count = 0 | |
t2 | 准备常量 1 | |
t3 | 从主内存中获取 count 的值,此时 count = 0 | |
t4 | 准备常量 1 | |
t5 | 将 count 的值与常量进行相加,此时 count = 1 | |
t6 | 将 count 的值写回主内存 | |
t7 | 将 count 的值与常量进行相加,此时 count = 1 | |
t8 | 将 count 的值写回主内存 |
两个线程都执行了一次 count++ 操作,期望结果是 2,但是最终得到的结果是 1,这也同时解释了上面的例子为什么得不到预期值 50000 的原因了
使用 synchronized 对代码进行改造(在 increase 方法上使用 synchronized)
@Slf4j
public class Atomicity {
private static int count = 0;
private static final int THREAD_NUMBER = 50;
private static final int CIRCLE_NUMBER = 1000;
// 使用 synchronized 修饰方法
private synchronized static void increase() {
count++;
}
public static void main(String[] args) throws InterruptedException {
// 定义线程数组
Thread[] threadGroup = new Thread[THREAD_NUMBER];
for (int i = 0; i < threadGroup.length; i++) {
threadGroup[i] = new Thread(() -> {
for (int j = 0; j < CIRCLE_NUMBER; j++) {
increase();
}
});
threadGroup[i].start();
}
// main 线程等待线程数组中的所有线程执行完
for (int i = 0; i < threadGroup.length; i++) {
threadGroup[i].join();
}
log.info("count 的值为: {}", count);
}
}
不论执行多少次,最终的结果都是预期值 50000
时间节点 | 线程 1 | 线程 2 |
t1 | 获取锁成功 | |
t2 | 从主内存中获取 count 的值,此时 count = 0 | |
t3 | 准备常量 1 | |
t4 | 将 count 的值与常量进行相加,此时 count = 1 | |
t5 | CPU 时间片到,上下文切换 | |
t6 | 尝试获取锁,获取锁失败,进入阻塞状态,让出 CPU 执行权 | |
t7 | 获得 CPU 时间片,将 count 的值写回主内存,唤醒线程 2 | |
t8 | 竞争锁,获取锁成功 | |
t9 | 从主内存中获取 count 的值,此时 count = 1 | |
t10 | 准备常量 1 | |
t11 | 将 count 的值与常量进行相加,此时 count = 2 | |
t12 | 将 count 的值写回主内存 |
加了 synchronized 关键字进行修饰后,两个线程执行两次 count++ 操作,最终的结果为 2,符合预期
2.3、有序性
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Orderly {
int num = 0;
boolean ready = false;
// 线程 1 执行的代码
@Actor
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程 2 执行的代码
@Actor
public void actor2(I_Result r) {
num = 2;
ready = true;
}
}
上面代码出现 1、4 这两个值是可以理解的,但是为什么会出现 0 这个值呢,唯一的解释就是线程 2 执行代码的顺序是这样的
// 线程 2 执行的代码
@Actor
public void actor2(I_Result r) {
ready = true;
num = 2;
}
也就是说,在程序执行的过程中是会出现指令重排现象的,注意: 指令重排不会影响单线程的执行结果,但是多线程环境下就有可能出现我们不想要的结果,这种情况要特别注意
为了保证线程安全,我们需要使用 synchronized 来解决有序性问题
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Orderly {
int num = 0;
boolean ready = false;
// 线程 1 执行的代码
@Actor
public void actor1(I_Result r) {
synchronized (this) {
if (ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
}
// 线程 2 执行的代码
@Actor
public void actor2(I_Result r) {
synchronized (this) {
num = 2;
ready = true;
}
}
}
使用 synchronized 之后就没有再出现 0 这个值了
synchronized 保证有序性的原理,我们加 synchronized 之后指令依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码,保证有序性