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 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值