线程安全问题
? 作者:小胡_不糊涂
🌱 作者主页:小胡_不糊涂的个人主页
📀 收录专栏:JavaEE
💖 持续更文,关注博主少走弯路,谢谢大家支持 💖
1. 产生线程不安全的原因
线程的调度是随机的,随机调度会使一个程序在多线程环境下,执行顺序存在很多的变数,此时就需要我们保证代码在任意执行顺序下,都能正常工作。
1.1 修改共享数据
多个线程修改同一个变量。
下面的代码中,涉及到多个线程针对 count 变量进?修改。此时这个 count 是?个多个线程都能访问到的 “共享数据”。
public class TestDemo1 {
private static int count =0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
t1.start();
t2.start();
//保证t1,t2自增完后再打印
t1.join();
t2.join();
System.out.println("count="+count);
}
}
按照正常的思路来讲,t1自增5000,t2自增5000,count最终应该是10000,但为什么真正的输出跟我们想的不一样呢?
1.2 内存可见性问题
可?性指?个线程对共享变量值的修改,能够及时地被其他线程看到。
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型。
?的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到?致的并发效果。
- 线程之间的共享变量存在主内存 (Main Memory)
- 每?个线程都有??的 “?作内存” (Working Memory)
- 当线程要读取?个共享变量的时候,会先把变量从主内存拷?到?作内存,再从?作内存读取数据
- 当线程要修改?个共享变量的时候,也会先修改?作内存中的副本,再同步回主内存
由于每个线程有??的?作内存,这些?作内存中的内容相当于同?个共享变量的 “副本”。此时修改线程1的?作内存中的值,线程2的?作内存不?定会及时变化。
这里的"主内存"是真正硬件角度的"内存"。?所谓的 “工作内存”,则是指 CPU 的寄存器和高速缓存
例如:
1.初始情况下,两个线程的工作内容都是一样的
2.?旦线程1修改了 a 的值,此时主内存不?定能及时同步。对应的线程2的?作内存的 a 的值也不?定能及时同步
此时代码就容易出现问题。
1.3 原子性问题
什么是原?性?
我们把?段代码想象成?个房间,每个线程就是要进?这个房间的?。如果没有任何机制保证,A进?房间之后,还没有出来;B 是不是也可以进?房间,打断 A 在房间?的隐私。这个就是不具备原?性的。
那我们应该如何解决这个问题呢?是不是只要给房间加?把锁,A 进去就把?锁上,其他?是不是就进不来了。这样就保证了这段代码的原?性了。
有时也把这个现象叫做同步互斥,表?操作是互相排斥的。
如果不保证原子性会给多线程带来什么问题?
如果?个线程正在对?个变量操作,中途其他线程插?进来了,如果这个操作被打断了,结果就可能是错误的。
1.4 指令重排序问题
?段代码的逻辑是这样的:
- 去前台取下 U 盘
- 去教室写10分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进?优化,?如,按 1->3->2的?式执?,也是没问题,可以少跑?次前台。这种叫做指令重排序
编译器对于指令重排序的前提是 “保持逻辑不发?变化”。这?点在单线程环境下?较容易判断,但是在多线程环境下就没那么容易了,多线程的代码执?复杂程度更?,编译器很难在编译阶段对代码的执?效果进?预测,因此激进的重排序很容易导致优化后的逻辑和之前不等价。
2. 解决办法
2.1 加锁
synchronized 能够保证原?性,解决互斥问题。
public class TestDemo1 {
private static int count =0;
public static void main(String[] args) throws InterruptedException {
Object loker=new Object();
Thread t1=new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (loker) {
count++;
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (loker) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);//10000
}
}
当t1线程拿到锁(loker)后开始count自增,t2线程则进入阻塞等待,等t1解锁后,t2获得锁,count再进行自增,就避免了因看不见内存中的变化而产生的错误。
2.2 加volatile
volatile关键字的作用主要有如下两个:
- 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
- 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
注意: volatile 不能保证原子性
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!