线程安全问题
目录
1.不安全原因
1.1线程调度无序(抢占式)
1.2多个线程修改同一个变量
1.3修改操作不是原子的
1.4内存可见性
1.5指令重排序
2.解决问题
2.1synchronized 关键字
2.1.1 特点
2.1.2 synchronized的过程
2.2使用
2.2volatile 关键字
1.不安全原因
1.1线程调度无序(抢占式)
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
//两个线程,分别对count自增5w次
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.get());
}
实际结果和预期结果不相符==>线程不安全
#
原因:与线程调度随机性相关
count++:由3个cpu指令构成
1.load,把内存中的数据读取到cpu寄存器中
2.add,寄存器中值,进行+1运算
3.save,把寄存器中的值写回到内存中
由于多线程调度顺序不确定,实际执行过程中,++操作的指令顺序有很多可能
例如:
第一种:
t1.load t1.add t1.save t2.load t2.add t2.save
执行过程:t1和t2可能在同一个cpu核心,可能在不同的cpu核心
t1=0 t1中++ —>t1=1 内存中count=1
t2=1 t2中++ —>t2=2 内存中count=2
第二种:
执行过程:
t1.load t2.load t2.add t2.save t1.add t1.save
t1=0 t2=0 t2中++ t2=1
内存中count=1 t1中++ t1=1 内存中count=1
两个结果不同
当前调度顺序无序,不知道有多少次是顺序执行,多少次是交错执行,所以每次结果都不一样
1.2多个线程修改同一个变量
一个线程修改同一个变量==>安全
多个线程读取同一个变量==>安全
多个线程修改不同变量==>安全
1.3修改操作不是原子的
不可分割的最小单位是原子
例如:
- 像++操作不是原子的,可以拆分成3个操作(load,add,save)
如果操作对应多个cpu指令,大概率不是原子
- 如果是直接使用=赋值,就是原子操作
因为不是原子导致两个线程的指令排列存在变数
不是原子导致两个线程的指令排列存在变数,导致执行结果与预期结果不同
1.4内存可见性
内存可见性:多线程环境下,编译器对代码优化,产生了误判,引发了bug
例如:
while(flag==0){
}
while循环,flag==0
load 从内存读取数据到cpu寄存器,cmp比较寄存器值是否为0
load时间开销高于cmp,因为load开销大,load结果一样,所以编译器就把load优化掉了,只有第一次执行load真正执行了(编译器优化的手段)
编译器优化,在程序结果变前提下,通过加减语句,变换语句,列操作,让整个程序执行的效率大大提升在程序结果不变前提下:单线程下判定是非常准确,多线程就不一定了
1.5指令重排序
指令重排序,是编译器优化的策略
调整代码执行顺序,让程序更高效
调整代码执行顺序,单线程不会有影响,但是多线程就不一定了
#
2.解决问题
2.1synchronized 关键字
2.1.1 特点
某个线程执行到某个对象的 synchronized 中时 , 其他线程如果也执行到
同一个对象 synchronized 就会 阻塞等待
进入 synchronized 修饰的代码块 , 相当于 加锁
退出 synchronized 修饰的代码块 , 相当于 解锁
例如:
public void add(){
synchronized (this){
count++;
}
}
这里的this是锁对象
如果两个线程,针对同一个对象加锁,就会出现锁竞争(一个先拿到锁,另一个阻塞等待)
如果两个线程,针对不同对象加锁,不会有影响
()里的锁对象,可以写作任意一个Object对象(基本数据类型不行),此处的this,相当于counter
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
这两个线程在竞争同一个锁对象,就会产生锁竞争,(t1拿到锁,t2阻塞)
运行结果:100000
此时,线程就安全了
就count++为例:
两个线程t1,t2原本有3个指令,加上synchronized后,加上lock和unlock.
如果t1先执行,保证,t2的load一定在t1的save之后,此时就线程安全
加锁:本质是吧并发的变成串行的
join和加锁的区别:
join:两个线程完整的进行串行
加锁:两个线程的某个小部分串行,大部分并行
上述代码,线程的步骤:
1.创建i
2.判定i<5000
3.调用add
4.count++
5.add返回
6.i++
只有4是串行,12356是并发,在保证线程安全的前提,同时代价更快,更好利用多核cpu
因为t1还没有执行完count++,此时t2也执行到count++,t2就会阻塞等待,此时t2暂时还不能执行5 6,但是1 2 3都是并发执行.t1,t2都执行完4,5 6 t1 t2依然可以并发执行
加锁可能会阻塞,对效率有影响
比不加锁准确,比串行快
2.1.2 synchronized的过程
获得互斥锁
从主内存拷贝变量的最新副本到工作的内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
2.2使用
- 直接修饰普通方法:
锁的 SynchronizedDemo 对象
public synchronized void methond() {
}
2) 修饰静态方法:
锁的 SynchronizedDemo 类的对象
public synchronized static void method() {
}
3) 修饰代码块: 明确指定锁哪个对象
锁当前对象
public void method() {
synchronized (this) {
}
锁类对象
public void method() {
synchronized (SynchronizedDemo.class) {
}
2.2volatile 关键字
volatile 修饰的变量, 能够保证 “内存可见性”
volatile public static int flag=0;
每次都从内存读取flag变量的值,编译器不进行优化
volatile 不保证原子性
volatile使用的场景:一个线程读,一个线程写
还没有评论,来说两句吧...