线程不安全的原因
1.抢占式执行(罪魁祸首)
线程执行是无序的
2.多个线程修改同一个变量
例如(这些都是安全的)
一个线程修改一个变量
多个线程读取同一个变量
多个线程修改不同的变量
3.修改操作,不是原子操作
例如count++操作,可以分解成
1) load获取count的地址
2) add使count+1
3) 将修改的值保存到原来的的位置
4.内存可见性引起线程不安全
(后续代码解释)
5.指令重排序引起内存不安全
(后续举例解释)
线程安全的方案
1.synchronized
通过加锁操作来保证操作的原子性
当进入到synchronized代码块中就会自动触发加锁操作,当代码块运行结束的时候会自动解锁
其中()里的可以写作任意的object对象(int,float等基本数据类型不可以)
package homeWork;
public class Thread316_4 {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int i = 0;i < 10000;i++){
synchronized (Thread316_4.class){
count++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized (Thread316_4.class){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
如这段并发执行的求和,通过加锁,其中一个线程拿到了锁之后另一个线程就无法进入这个代码块中就是Thread316_4.class里面,就会等待另一个线程执行完之后才能拿到锁,并加锁
这样就不会出现抢占情况,从而不能++到20000
我当时的疑惑???
我当时觉得加锁还不如直接加join()让线程进入等待,没必要加锁这么麻烦,让我们分析一下
单个线程做的工作:
1) 创建i
2) 判定 i <10000
3) count++
4) 返回
5) i++
其中其实只有3)是串行的,如果用join()就是所有都是串行的
虽然加锁会影响到程序效率,但是并行也比join()串行更快一些
synchronized方法一些细节问题
比如这两种都是利用synchronized进行加锁,他们之间是有所不同的
add方法的加锁是创建一个对象,对他来进行加锁,来控制,但一个线程调用这个方法他会获得locker的锁来判断是否被获得决定是否阻塞等待
而sub方法的加锁是对this这个实例化对象进行加锁,比如Counter counter = new Counter();
此时调用sub()会对counter这个对象进行加锁,所以这两种都是加锁但是略有不同
2.volatile
为了解决线程不安全的4和5
1.保证内存可见性
先用代码来举个例子
import java.util.Scanner;
public class ThreadDemo9 {
public static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag == 0){
}
System.out.println('t');
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
flag = n;
});
t1.start();
t2.start();
}
}
我们想要的效果: 输入一个值,线程1停下来打印t
实际上:
输入了之后程序并没有停止
这就是由于内存可见性的问题,编译器自动优化
原因
1) while循环先 load从内存获取flag的值 然后cmp比较是否为0
2) load的开销太大,编译器给你自动优化了,只有第一次load其他只有cmp
加上volatile即可保证每次都能重新从内存读取
2.防止指令重排序
其实这也是编译器优化的策略,为了让程序更高效
这个场景比较难以复现只能叙述
例子
比如说你new一个student对象,一般的步骤是
1) 申请内存
2)调用构造方法
3)将内存的地址给变量名
但是如果是多线程执行的话
当t1执行了1,3两个步骤
而t2访问这个变量名时没初始化完成,就可能出现一些问题
所以加上volatile即可防止编译器自动优化,不会发生指令重排序