多线程编程的安全问题和解决措施

线程不安全的概念

由于多线程并发执行,导致结果出错,我们称这种线程是不安全的。

多线程编程出错的原因

一:线程之间并发执行的随机性导致线程不安全

二:多个线程对同一个对象进行修改

三:线程的操作不是原子性的

四:内存可见性问题

五:指令重排序

下面举一个线程不安全的例子:

假设有两个线程A、B它们里面都有一个for循环,它们都要执行让变量count自增的操作

int count = 0;
for(int i = 0;i<10000;i++){
    count++;
}

按照我们的预期,count最终的结果不出意外的话应该20000,但是最终的结果总是在10000到20000之间。出现这种情况的原因就是因为count++这个动作它不是原子性的。

原子性就表明这个动作不能再细分,而count++这个动作就可以分为1.从内存中取出count数据,2.让count加一,3.把count放回到内存中。

不是原子性的操作在多线程编程中就有可能出问题,比如同一时刻线程A和线程B同时将内存中的count假设为0读入CPU中,然后又同一时间将count加一然后放回到内存中,。count有两次动作本应该为2,但是结果却为1,而从这里开始就出问题了。

上面这个列子就可以说明线程不安全所有原因。

上面已经分析了线程的操作不是原子的,再来分析其他两个。

引起线程不安全的根本原因就是因为多线程之间的并发执行,这个是无法从根本上上改变的。并发执行就导致同一时间段内多个线程之间抢占式的执行,就如上面的操作一样,线程A和线程B之间是不知道对方存在的,它们才没有什么礼让,谁先到就先执行。因为执行的顺序都是随机的,因此每次得到的结果肯定也不一样,最极端要么结果为10000要么为20000,结果为10000的情况那就是每时每刻两个线程的动作都是一样的,而20000的情况则两个线程是顺序执行,线程A先执行,线程B后执行。

两个线程操作同一对象就跟不用说了,count作为被操作的对象它是没有意识的,他不能决定这一时刻让A执行,下一时刻让B执行。如果两个线程操作的是不同的对象,比如让线程A操作count,线程B同样的逻辑操作另一个变量ret,则结果都是10000。

通过一个例子来看内存可见性问题:

现在有两个线程A、B,线程A中有一个while循环,判断条件为flag==0。线程B里面执行让用户输入整形数字falg的逻辑。

public class Thread18 {
    static class  Counter{
        private static int flag = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread thread = new Thread(() -> {
            while(counter.flag==0){

            }
            System.out.println("线程结束");
        });
        thread.start();

        Thread thread1 = new Thread(() -> {
            Scanner scan = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flag = scan.nextInt();
        });
        thread1.start();
    }
}

结果并没有输出“线程结束”。这就是多线程中内存可见性带来的线程不安全问题。

首先明确是什么可见性,可见性指的是一个线程对内存中的一个共享变量进行修改操作,这个修改操作对其他线程是可见的,例如一个读一个写,但是当写操作的线程进行的修改的时候,读线程可能读到修改前的数据。

出现这样的原因主要和底层编译器的优化相关,在Java内存模型中每个线程都有自己的工作内存(CPU缓存),同时这些线程还有一个共享变量的主内存。当线程要读取一个共享变量时,会先把变量从主内存中拷贝到工作内存,再从工作内存读取数据。当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,然后将再将副本放回到主内存中。

上面的这些情况,对于单核CPU,同一时刻只能有一个线程在执行,所以从主内存中获取到共享变量后,对其进行修改后存放在CPU的缓存中,然后再刷回主内存中, 这整个过程只有一个线程操作,因为为每个线程分配了时间片,所以看起来是多个线程同时执行,但实际上只有一个线程执行。

再回到上面的那个问题,没有输出“线程结束”的原因是thread线程拿取数据时,它是先在自己的工作内存中拿,他自己工作内存中保存的flag就是0,而由于thread1修改的数据没有及时更新到主内存中 ,所以就导致thread线程获取的flag没有改变,所以就不会输出"线程结束"。

指令重排序指的是编译器为了提高程序的执行效率,而改变代码的执行顺序,这样做在多线程下运行就可能出错。举个例子,比如现在要求你按着顺序来做一套试卷,而经历题海战术的你知道什么题简单,因此你会先做简单的题目,最后再做困难的,这就是类似指令重排序。

指令重排序在多线程中可能会出现问题:

clsaa test{
    private static Integer value = 1;
    boolean flag;
    public static void init(){
        this.value = 8;
        this.flag = true;
    }
    public static void getValue(){
        if(flag){
            System.out.println(value);
        }
    }
}

在单线程中输出的value值为8,但是在多线程中输出的值就可能不为8了,原因如下:

假设现在有两条线程Thread1和Thread2,Thread1执行该程序的时候,可能是这样执行的this.flag = true。而在Thread1执行之后,Thread2恰好执行getValue方法,线程2输出的value值就为0。

本来Thread1应该先执行this.value = 8的,但是就是因为指令重排序让它先执行this.flag = true,于是就出现了问题。

为了解决多线程问题,常用的就是给线程加锁或者是在变量前面加上volatile或者是使用线程安全类、线程安全的集合等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咸鱼吐泡泡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值