synchronized 保证可见性、原子性、有序性
一、概述
并发三大特性即 可见性、原子性、有序性
可见性: 一个线程修改了共享变量的值,另外一个线程应该立即得到共享变量的最新值
原子性: 一个或多个操作要么全部执行,并且在执行的过程中不会被其它因素打断,要么全部不执行
有序性: 为了提高程序运行效率,Java 在编译和运行时会对指令进行重排序,重排序后的指令可以保证单线程环境下程序的最终结果一致,但是多线程情况下可能会出现不符合预期的结果
二、测试
2.1、可见性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @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 对上面的代码进行改造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @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 指令(甚至是对应机器指令层面的更多的微指令)
1 2 3 4 5 | 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @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、有序性
测试参考 https://www.cnblogs.com/dalianpai/p/14175292.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @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 执行代码的顺序是这样的
1 2 3 4 5 6 | // 线程 2 执行的代码 @Actor public void actor2(I_Result r) { ready = true ; num = 2 ; } |
也就是说,在程序执行的过程中是会出现指令重排现象的,注意: 指令重排不会影响单线程的执行结果,但是多线程环境下就有可能出现我们不想要的结果,这种情况要特别注意
为了保证线程安全,我们需要使用 synchronized 来解决有序性问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @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 之后指令依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码,保证有序性
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?