java线程安全问题的原因与大致解决思路

  • Post author:
  • Post category:java


在我们编写多线程程序的时候,经常会因为线程安全问题导致出现各种各样的bug,这里我们总结一些线程安全的原因和大致的解决思路


目录


线程安全的问题


1.操作系统的抢占执行


2.多个线程修改同一个变量


3.操作不是原子性的


4.内存可见性


5.指令重排序


线程安全的解决方法


3.操作不是原子性的


4.内存可见性


5.指令重排序问题


线程安全的问题

1.操作系统的抢占执行

这个问题时我们编写多线程代码时,出现线程安全问题的罪魁祸首,当然,这个问题也是操作系统方面的,我们并没有办法去解决这个问题

2.多个线程修改同一个变量

在多线程中,我们多个线程修改同一个变量的时候,也很容易触发线程安全问题,我们可以适当调整代码来解决这个问题,但是并不是每次都可以调节代码的,所以这个也不是重点

3.操作不是原子性的

我们改变不了上面的两个问题,但是我们可以改变这个问题,我们可以吧操作变成原子操作,通过加锁操作来实现,这样我们就可以一定程度上保护线程安全

4.内存可见性

所谓的内存可见性,就是指一个线程修改了一个公共的值,另一个线程也能看到

5.指令重排序

指令重排序就是指,在单线程情况下,很多代码的执行顺序会影响执行速度,写编译器的大佬就把这代码进行重排序优化

线程安全的解决方法

前面的两个我们没有办法改变,我们从第三个开始讲解决思路

3.操作不是原子性的

在java中操作不是原子性的很容易出现问题,就比如我们这一段代码

public class Main {
    public 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.join();
        t2.join();
        System.out.println(count);
    }
}

这段代码中我们使用两个线程对count进行累加操作,但是得到的结果好像并不是我们想要的,这就出现了线程安全问题,因为++操作其实有三个子操作,分别是load,add,save,当着三个子操作不是原子操作的时候,可能就会出现两个线程执行++只进行了一次自增的情况,所以导致结果不是我们想要的,

那我们应该怎么解决呢?

加锁!!!

我们可以通过加锁操作,让操作变成原子性的

经过改进,我们给两个线程加上锁,使其成为原子操作(这里的代码编写并不是很好,只是为了表达意思)

public class Main {
    public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            synchronized (Main.class){
                for (int i = 0; i < 5000; i++) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() ->{
            synchronized (Main.class){
                for (int i = 0; i < 5000; i++) {
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

可以看到我们的结果就得到了10000,也就解决了这种类型的问题

4.内存可见性

直接看这个问题先

class A extends Thread {
    public int i = 0;
    public void run() {
        while(i == 0){

        }
    }
}


public class Main {
    public static void main(String[] args) throws InterruptedException {
        A t1 = new A();
        Scanner sc = new Scanner(System.in);
        t1.start();

        t1.i = sc.nextInt();
        t1.join();
    }
}

这里我们执行的结果是

可以看到当我们输入5之后,他并没有结束线程,这是为什么呢?

因为我们的


编译器进行了自动的优化,让我们的while循环中的判断只执行一次,


如果在单线程的情况下,这样当然是没问题的了,但是在多线程的情况下,就会出问题,闹我们如何解决呢?

前面提到了volatile关键字,我们对代码进行修改

class A extends Thread {
    public volatile int i = 0;
    public void run() {
        while(i == 0){

        }
    }
}


public class Main {
    public static void main(String[] args) throws InterruptedException {
        A t1 = new A();
        Scanner sc = new Scanner(System.in);
        t1.start();

        t1.i = sc.nextInt();
        t1.join();
    }
}

可以看到,当我们输入5之后,线程直接就停止了,volatile到底干了什么呢?

其实很简单,他就是跟编译器说,这个变量,会有别的线程来读,就老老实实的去读数据,就那么简单,这样我们就解决了内存可见性的问题.

5.指令重排序问题

指令重排序也是编译器优化的一种,就是把一些执行的执行顺序给他改变了,这在单线程的情况下肯定任何问题,但是在多线程的情况下,就不一定了,多线程的情况下可能会导致wait()这种关键字得到错误的信息,那我们怎么办呢?

还是加volatile关键字,给我们可能发生指令重排序的实例或者变量加上这个关键字的时候,就不会发生指令重排序的问题了,比如我们的线程安全的”懒汉”单例模式,就是通过volatile关键字

经典的面试问题:

简述volatile关键字的作用:

1.保证内存的可见性,用屏障指令实现

2.禁止指令重排序,编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。



版权声明:本文为qq_55546526原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。