线程的安全问题【重点】

目录

前言

什么是线程安全?【经典面试题】

count++的详细过程

线程不安全的原因【经典面试题】

1.线程之间是抢占式执行的(根本原因,万恶之源)

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

3.原子性

4.内存可见性

5.指令重排序(也是和编译器的优化有关)


前言

线程安全是多线程里面最核心的话题,也是和实际工作最相关的话题,不管是工作还是学习,都应该重点掌握。

什么是线程安全?【经典面试题】

为了更好的理解多线程安全问题,我们先看一个例子

public class ThreadDemo15 {
    static class Counter{
        public int count=0;
        public void increase(){
            count++;
        }
    }
    static Counter counter=new Counter();
    public static void main(String[] args) throws InterruptedException {
        //此处创建两个线程,分别对count自增5w次
        Thread t1=new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t1.start();
        Thread t2=new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

运行结果每次都不一样

 

 

 而我们真正想要的结果其实是10w

那么哪里出了问题呢?

我们可以判断这大概率和多线程并发执行相关,而且通过验证,确实是这样

验证过程

public class ThreadDemo15 {
    static class Counter{
        public int count=0;
        public void increase(){
            count++;
        }
    }
    static Counter counter=new Counter();
    public static void main(String[] args) throws InterruptedException {
        //此处创建两个线程,分别对count自增5w次
        Thread t1=new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    counter.increase();
                }
            }
        };
        t1.start();
        /*Thread t2=new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };*/
        //t2.start();
        t1.join();
        //t2.join();
        System.out.println(counter.count);
    }
}

 这种情况,我们称之为“线程不安全”

为了解决这个问题,我们需要分析程序执行的过程

首先我们需要知道count++的详细过程

count++的详细过程

共分为三个步骤:

1.把内存中的值读取到CPU中(load)

2.执行++操作(add)

3.把CPU的值写回到内存中(save)

刚刚是一个线程执行count++操作,但如果是两个线程并发执行count++操作,那么就容易出问题

我们需要知道的是操作系统调度线程的时候是“抢占式执行”的方式,也就是竞争的关系,某个线程什么时候上CPU执行,什么时候切换出CPU,是完全不确定的。而且另一方面,两个线程在两个不同的CPU可以并发执行。因此,两个线程的执行顺序是完全不可预测的。

我们先假设两个线程针对一个变量进行++操作,我们需要知道count++的操作并不是完全三步都要执行完,也可能是线程1只执行完load,线程2把三步都执行完,线程1再执行后面的两步,此时,算出的结果就会出问题,假设内存中初始值为0,线程2执行完count++后内存中的值变为了1,线程1把后面两步都执行完之后,内存中的值还是1,这是出现了问题,因为我们的两个线程执行了两次count++操作,内存中的结果按理来说应该是2。而且这只是出现问题的其中一种情况。

只有串行执行的时候才不会出现这样的问题。

线程不安全的原因【经典面试题】

1.线程之间是抢占式执行的(根本原因,万恶之源)

因为是抢占式执行,所以导致两个线程里面操作的先后顺序不确定,这样的随机性,导致了线程的不安全。

这是操作系统内核实现的,我们无力改变。

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

一个线程修改一个变量,没有线程安全问题,结果确定;

多个线程读取同一变量,也没有线程安全问题,读只是单纯的把内存中的数据放到CPU中;

多个线程修改不同的变量,举例来说就是十个线程修改十个变量,一对一的关系,也没有线程安全问题,这个类似于第一种情况,所以为了规避线程安全问题,可以变换代码的组织形式,达到一个线程只修改一个变量的条件,但是有些场景下可以这么变换,有些却不可以。

3.原子性

像++这样的操作,本质上是三个步骤,是一个“非原子”的操作

像=这样的操作,本质上是一个步骤,是一个“原子”操作

++操作本身不是原子操作,可以通过加锁的方式,把这个操作变成原子操作。

4.内存可见性

具体就是一个线程修改,一个线程读取,由于编译器的优化,可能把一些中间环节的LOAD和SAVE省略掉了,此时读的线程可能读到的是未修改过的结果。

这里我们需要知道:++操作每次执行都要LOAD和SAVE,由于ADD比LOAD和SAVE要快一万倍,因此在执行很多次++操作的时候,很多LOAD和SAVE操作就被省略掉了,这样做是为了提高程序的整体效率。这个省略操作是编译器和JVM综合配合达成的效果。

这种优化在单线程下具有很高的效率,但是在多线程的时候,另一个线程也尝试读取/修改这个数据,这时候就会出问题。

volatile关键字可以解决内存可见性的问题。(原理是加上这个关键字就把编译器的优化给关闭了)

5.指令重排序(也是和编译器的优化有关)

编译器会自动调整代码执行的顺序,起到提高执行效率的效果。

调整的前提是不改变最终的结果,如果在单线程的情况下,编译器判断顺序是否影响结果很容易,可是在多线程的情况下编译器判定顺序是否影响结果,就容易出错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值