线程安全问题的原因和解决方案

本文解释了线程安全的概念,分析了线程安全问题的五大原因,包括抢占式执行、多线程修改同一变量、非原子操作、内存可见性和指令重排序。并给出了使用synchronized和volatile关键字解决线程安全问题的方法实例。
摘要由CSDN通过智能技术生成

一,线程安全是什么

就是这个线程执行之后,要跟我们想的结果要一致,这就是线程安全的,如果不是,则是不安全,不安全的就是要进行处理的。

接下来观察一个经典代码,看是否安全:

public class demo7 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() ->{
           for (int i = 0; i<5000; i++){
               count++;
           }
        });
        Thread t1 = new Thread(() ->{
            for (int i = 0; i<5000; i++){
                count++;
            }
        });
        t1.start();
        t.start();
        t1.join();
        t.join();
        System.out.println(count);
    }
}

这个代码显然是线程不安全的,罪魁祸首就是多线程之间的抢占式执行,导致可能重复执行了count++,导致数据没有加上去,所以是不安全的,也就是没有达到预期的执行结果。 

二,线程安全问题

1.线程安全问题出现的五大原因*

1、抢占式执行,随机调度。

在多线程的线程安全问题中是罪魁祸首,因为抢占式执行有很多的随机性,导致线程之间的随机调度,数据录入不正确执行。

2、多线程同时修改一个变量。

比如上面的count,两个线程都在修改,就会导致count会有可能又来跟他一起执行,输入操作重复执行一个数据了。

就比如这个图,上述代码的i同时向前走,等于1,第一次count++后等于1,然后第二个线程进去调度,赋值还是1,就导致少赋值了一次。就是线程不安全的。 

3、修改操作不是原子的。

也就是保证一次只执行一个操作,不一次执行很多个操作,这样就能保证count是一个一个加的,其实上述count++是三个操作。

1)从内存把数据读到CPU

2)进⾏数据更新

3)把数据写回到CPU

解决方法就是加锁,让线程一个一个的执行,不发生同步互斥现象。

4、内存可见性

重要的一点就是要大家(其他线程)能够及时看见自己对共享变量值的变化,就是在线程中大家都有自己的主内存和工作内存,在自己工作内存修改的值,要及时同步到主内存,如果没有及时同步,那么其他线程执行时没有在主内存中看见修改,这就是可见性问题。

可能大家疑问为什么要弄这么多内存呢,其实这是一个抽象叫法,真实的是工作内存是在寄存器和高速缓存,主内存是真正的我们说的内存,为啥都不弄到寄存器呢,因为穷,寄存器太贵了。

5、指令重排序

一般指令就是分这三步:

1)申请内存空间

2)调用构造方法

3)把此时内存空间的地址,拿来使用,

这三步的后面两步是可以重新排序的,在单线程中没有一点问题,但在多线程中就有问题了。

就是两个线程先后访问一个共享变量,由于指令重排序的情况下,就可能前一个变量修改了,但延迟了对其他线程的可见,或者后面一个提前了,导致前一个还没有修改就已经被覆盖了,导致数据不一致的问题。也就是可能一个线程会修改一个线程的判断条件,结果先提前,拿来使用的地址判断后不符合条件,就直接跳过去了。

三,如何解决线程安全问题

一,加锁

使用synchronized关键字来给线程加锁,使线程执行顺序由我们掌控

代码如下:

public class demo8 {
    public static void main(String[] args) {
        Object object = new Object();
        Thread a = new Thread(() ->{
            synchronized (object){
            System.out.println("a");
            }
        });
        Thread b = new Thread(() ->{
            synchronized (object) {
                System.out.println("b");
            }
        });
        Thread c = new Thread(() ->{
            synchronized(object) {
                System.out.println("c");
            }
        });
        c.start();
        b.start();
        a.start();
    }
}

想创建锁就先创建一个锁对象,在java中锁对象没有要求,是个对象就行。进入synchronized就相当于上锁,走出它的“}”就相当于解锁,一旦上了锁,下个被上相同对象锁的线程就只能等锁释放也就是解锁之后才能上锁,运行。

二,volatile关键字

这个关键字可以保证内存可见性,还有指令重排序,他写上之后,编辑器在优化时会跳过一些必须走的程序的优化,从而保证每个步骤执行的到,也就是录入主内存每一步能被其他线程看见。

代码实现:

import java.util.Scanner;


    static class Counter {
        public volatile int flag = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
                // do nothing
            }
            System.out.println("循环结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输⼊⼀个整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }

给flag加上关键字,本来用户输入0不会结束,因为读的这一步操作会被优化掉,所以导致while循环会一直不满足条件。现在不会出现,就可以处理掉内存可见性

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值