1.线程不安全的原因
一般来说,线程不安全有以下5种原因:
- 抢占式执行:操作系统调度的随机性
- 多个线程修改同一个变量
- 修改操作不是原子的
- 内存可见性问题
- 指令重排序
对于前四种导致线程不安全的问题->可通过synchronized解决
对于后两种导致线程不安全的问题->可通过volatile解决
2.线程不安全示例及对应修改方法
package threading;
//演示线程安全问题:下面代码线程不安全
class Counter{
public int count =0;
public void increase(){
count++;
}
}
public class Demo14 {
private static Counter counter=new Counter();
public static void main(String[] args) throws InterruptedException {
//搞两个线程,每个线程针对counter进行5w次自增
// 预期结果:10w
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter:"+counter.count);
}
}
上述代码运行结果不是10w,线程不安全;线程不安全的原因是
- 抢占式执行:操作系统调度的随机性
- 多个线程修改同一个变量
- 修改操作不是原子的
下面来具体分析一下该代码:
进行的count++操作,底层是三条指令在CPU上完成的
- 把内存的数据读取到CPU寄存器中(load)
- 把CPU寄存器中的值进行++操作(add)
- 把寄存器中的值写回到内存中(save)
由于是两个线程修改同一个变量,每次修改三个步骤(不是原子的),由于线程之间的调度顺序不确定,故出现线程不安全
package threading;
//演示线程安全问题:下面代码线程安全
//加锁
class Counter1{
public int count =0;
//修饰方法
public synchronized void increase(){
count++;
}
}
public class Demo15 {
private static Counter1 counter= new Counter1();
public static void main(String[] args) throws InterruptedException {
//搞两个线程,每个线程针对counter进行5w次自增
// 预期结果:10w
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter:"+counter.count);
}
}
上述代码线程安全,运行结果10w;可以看出,代码只加了一个关键字synchronized(加锁)就解决上述问题;而synchronized有三种位置可添加:
- 直接修饰普通方法,锁对象相当于this(上述修改方法)
- 修饰代码块,锁对象在()指定
- 修饰静态方法,锁对象相当于类对象
1和3写起来类似,下面只演示2:
public class Demo {
public void method() {
synchronized (this) {
}
}
}
下面演示由后两种导致线程不安全的问题->可通过volatile解决
package threading;
//线程不安全:内存可见性问题
import java.util.Scanner;
public class Demo17 {
static class Counter{
public int count=0;
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1=new Thread(()->{
while (counter.count==0){
}
System.out.println("t1执行结束");
});
t1.start();
Thread t2=new Thread(()->{
System.out.println("请输入一个int:");
Scanner scanner=new Scanner(System.in);
counter.count=scanner.nextInt();
});
t2.start();
}
}
上述代码线程不安全,原因如下:
内存可见性问题
输入非0的数代码也不会结束,输入后内存已经被修改了,但是刚才的修改,对t1的读内存操作不会有影响;因为t1已经被优化为读一次就完了;t2把内存改了,t1没感知到--》内存可见性问题
package threading;
//线程不安全:内存可见性问题
import java.util.Scanner;
public class Demo17 {
static class Counter{
// public int count=0;
volatile public int count=0;
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1=new Thread(()->{
while (counter.count==0){
}
System.out.println("t1执行结束");
});
t1.start();
Thread t2=new Thread(()->{
System.out.println("请输入一个int:");
Scanner scanner=new Scanner(System.in);
counter.count=scanner.nextInt();
});
t2.start();
}
}
上述代码线程安全,解决方法是加了volatile关键字,该关键字只修饰变量,加入该关键字后,编译器不会做出”不读内存,只读寄存器“的优化,也就是说该关键字可以解决
- 内存可见性问题
- 指令重排序
这两种问题!
这里最后简单说一下什么是指令重排序:
一段代码是这样的:
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序