线程安全问题的原因及其解决方法。
1、(根本原因)多线程调度采取随机的方式,操作系统采用随机、抢占式调度的方式,导致出现多线程安全问题。
当前主流操作系统本身都是采用随机、抢占式的方式进行多线程调度,我们只能采取措施来避免线程安全问题。
2、多线程同时修改同一变量,容易出现线程安全问题。
举个例子:
t1和t2两个线程个循环1w次,总的count值应为2w,但是运行下段代码每次的值都是不相同的,这就出现了线程安全问题。
class Count{
public static int count=0;
public static void increase(){
count++;
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Count counter=new Count();
Thread t1=new Thread(()->{
for (int i=0;i<10000;i++){
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i=0;i<10000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
将increase方法进行打包,也就是上一个锁,就可以使count++具有原子性,成为一个整体而不被打断 。
更新代码如下:
class Count{
public static int count=0;
synchronized public static void increase(){
count++;
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Count counter=new Count();
Thread t1=new Thread(()->{
for (int i=0;i<10000;i++){
counter.increase();
}
});
Thread t2=new Thread(()->{
for (int i=0;i<10000;i++){
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
3、修改操作不是原子的
如2所示的例子,count++不是原子性的,故对其加锁,使其成为一个“原子”。
4、内存可见性,引起的多线程安全问题。
在程序编译的过程中,编译器会对代码进行优化,这也就导致了内存可见性问题的出现,也就是好心办了坏事。
举个例子:
让t1一直循环,用户通过控制t2来控制t1循环是否结束。
import java.util.Scanner;
public class Demo3 {
public static int isQuit=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while(isQuit==0){
;
}
System.out.println("t1执行结束");
});
Thread t2=new Thread(()->{
System.out.println("请输入isQuit的值:");
Scanner scanner=new Scanner(System.in);
isQuit=scanner.nextInt();
});
t1.start();
t2.start();
}
}
运行结果:
我们发现,当t2输入一个非零的数时,t1并没有停止循环。这就是由于idea编译器将我们所写的代码进行了优化, 使程序中原有的逻辑出现了差错。遇见这种问题,我们采用volatile关键字来避免这种问题的出现。
优化代码:
import java.util.Scanner;
public class Demo3 {
volatile public static int isQuit=0;
public static void main(String[] args) {
Thread t1=new Thread(()->{
while(isQuit==0){
;
}
System.out.println("t1执行结束");
});
Thread t2=new Thread(()->{
System.out.print("请输入isQuit的值:");
Scanner scanner=new Scanner(System.in);
isQuit=scanner.nextInt();
});
t1.start();
t2.start();
}
}
使用volatile关键字来修饰成员变量isQuit,禁止该变量的读操作优化到存储器,避免了内存可视化问题的出现。
5、指令重排,引起的多线程安全问题。
指令重排也是编译器的优化的一种手段。编译器在保证原有指令逻辑不变的情况下,对代码指令的顺序进行调整,使调整后代码的执行效率提高。
举个例子:
单例模式下的懒汉模式
//懒汉模式
class SingletonLazy{
private static volatile SingletonLazy instance=null;
public static SingletonLazy getInstance(){
//3.判断是不是第一次进行if判断,如果是第一次进行加锁,避免后续加锁而降低效率
if(instance==null){
//2.加锁,使if判断和创建新对象成为一个整体,具有原子性
synchronized (SingletonLazy.class){
//1.基础代码
if (instance==null){
instance=new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){}
}
public class Demo5 {
public static void main(String[] args) {
SingletonLazy s1=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
//报错
//SingletonLazy s3=new SingletonLazy();
System.out.println(s1==s2);
}
}
其中,将instance前加上volatile就是为了防止指令重排而带来的多线程安全问题。