【Java进阶】多线程&高并发(二)<线程安全问题>

一、概述

  • 非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,但是值不同步的问题
  • 线程安全问题表现为三个方面:原子性、可见性和有序性

二、原子性

1. 介绍

  • 原子(Atomic)就是不可分割的意思
  • 原子操作的不可分割有两层的含义
    ① 访问(读/写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生,即其他线程是看不到当前操作的中间结果
    访问同一组共享变量的原子操作是不能够交叉的。比如:现实生活中从ATM机取款,对于用户来说,要么操作成功,用户拿到钱,余额减少,增加一条交易记录;要么没拿到钱,相当于取款操作没有发生
  • Java有两种方式实现原子性:一种是使用锁;另一种是利用处理器的CAS(Compare andSwap)指令
    ① 锁具有排它性,保证共享变量在某一时刻只能被一个线程访问
    ② CAS指令直接在硬件(处理器和内存)层次上实现,可以看做是硬件锁

2. 代码演示

package ThreadSafe;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author swaggyhang
 * @create 2023-06-17 9:36
 */
public class ThreadAtomicDemo {
    public static void main(String[] args) {
        MyInt myInt = new MyInt();

        // 启动两个线程,不断调用getNum()方法
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        System.out.println(Thread.currentThread().getName() + "->" + myInt.getNum());
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    }

    static class MyInt1 {
        int num;

        public int getNum() {
            /**
             * ++自增操作实现的步骤:
             * 1)读取num的值
             * 2)num自增
             * 3)把自增后的值再赋值给num变量
             */
            return num++;
        }
    }

    // 在Java中提供了一个线程安全的AtomicInteger类,保证了操作的原子性
    static class MyInt {
        AtomicInteger num = new AtomicInteger();

        public int getNum() {
            return num.getAndIncrement();
        }
    }
}

三、可见性

1. 介绍

  • 在多线程环境中,一个线程对某个共享变量进行更新之后,后续其他的线程可能无法立即读到这个更新的结果,这就是线程安全问题的另外一种形式:可见性(visibility)
  • 如果一个线程对共享变量更新后,后续访问改变了的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见;否则称这个线程对共享变量的更新对其他线程不可见
  • 多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据)

2. 代码演示

package ThreadSafe;

import java.util.Random;

/**
 * 线程的可见性
 *
 * @author swaggyhang
 * @create 2023-06-17 9:52
 */
public class ThreadVisibilityDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建并启动子线程
        MyTask myTask = new MyTask();
        new Thread(myTask).start();

        Thread.sleep(1000); // 主线程睡眠1秒

        // 主线程睡眠1秒后,取消子线程
        myTask.cancel();
        /**
         * 可能会出现以下情况:
         * 1、在main线程中调用了myTask.cancel()方法,把myTasK对象的toCancel变量修改为true,可能存在子线程看不到main线程对toCancel
         * 变量做的修改,在子线程中toCancel变量一直为false
         * 2、导致子线程看不到main线程对toCancel变量更新的原因
         * 1)JIT(即时编译器)可能会对run()方法中while循环进行优化为
         *    if (!toCancel) {
         *      while (!toCancel) {
         *          if (doSomething()) {
         *           }
         *       }
         *    }
         * 2)可能与计算机的存储系统有关,假设分别有两个cpu内核运行main线程和子线程,运行子线程的cpu无法立即读取运行main线程cpu中的数据
         */
    }


    static class MyTask implements Runnable {
        private boolean toCancel = false;

        @Override
        public void run() {
            while (!toCancel) {
                if (doSomething()) {
                }
            }
            if (toCancel) {
                System.out.println("任务被取消");
            } else {
                System.out.println("任务正常结束");
            }
        }

        private boolean doSomething() {
            System.out.println("执行某个任务...");
            try {
                Thread.sleep(new Random().nextInt() * 1000L); // 模拟执行任务的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return true;
        }

        public void cancel() {
            toCancel = true;
            System.out.println("收到取消线程的消息");
        }
    }
}

四、有序性

1. 介绍

  • 有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order)
  • 乱序是指内存访问操作的顺序看起来发生了变化
  • 在多核处理器的环境下,编写的顺序代码结构的执行顺序可能是没有保障的
    ① 编译器可能会改变两个操作的先后顺序
    ② 处理器也可能不会按照目标代码的顺序执行
  • 这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序
  • 重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确性的情况下提升程序的性能。但是可能会对多线程城促的正确性产生影响,即可能导致线程安全问题
  • 重排序与可见性问题类似,不是必然出现的
  • 与内存操作顺序有关的几个概念
    ① 源代码顺序:源码中指定的内存访问顺序
    ② 程序顺序:处理器上运行的目标代码所指定的内存访问顺序
    ③ 执行顺序:内存访问操作在处理器上的实际执行顺序
    ④ 感知顺序:给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序
  • 可以把重排序分为指令重排序和存储子系统重排序两种
    ① 指令重排序:主要是由JIT编译器、处理器引起的,指程序顺序与执行顺序不一样
    ② 存储子系统重排序:是由高速缓存、写缓冲器引起的,感知顺序与执行顺序不一致

2. 指令重排序

  • 在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,就发生了指令重排序(Instruction Reorder)
  • 指令重排序是一种动作,确实对指令的顺序做了调整,重排序的对象指令
  • javac编译器一般不会执行指令重排序,而JIT编译器可能执行指令重排序
  • 处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致,哪条指令准备好了,处理器就执行哪条指令
  • 指令重排序不会对单线程程序的结果正确性产生影响,可能会导致多线程程序出现非预期的结果

3. 存储子系统重排序

  • 存储子系统是指写缓冲器与高速缓存
  • 高速缓存(Cache)是CPU中为了解决与主内存处理速度不匹配而设计的一个高速缓存
  • 写缓冲器(Store Buffer或者Write Buffer)用来提高写高速缓存操作的效率
  • 即使处理器严格按照顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的顺序看起来像是发生了变化。这种现象称为存储子系统重排序
  • 存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的现象
  • 存储子系统重排序对象时内存操作的结果
  • 从处理器角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作;写内存就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作。存储子系统(内存)重排序有以下四种可能:
    ① LoadLoad重排序:一个处理器先后执行两个操作L1和L2,其他处理器对两个内存操作的感知顺序可能是L2>L1
    ② StoreStore重排序:一个处理器先后执行两个写操作S1和S2,其他处理器对两个内存操作的感知顺序可能是S2>S1
    ③ LoadStore重排序:一个处理器先执行读内存操作L1,在执行写操作S1,其他处理器对两个内存操作的感知顺序可能是S1>L1
    ④ StoreLoad重排序:一个处理器先执行写内存操作S1,在执行读操作L1,其他处理器对两个内存操作的感知顺序可能是L1>S1
  • 内存重排序与具体的处理器微架构有关,不同的架构的处理器所允许的内存重排序不同
  • 内存重排序可能会导致线程安全问题。假设有两个共享变量int data = 0; boolean ready = false;
    在这里插入图片描述

4. 貌似串行语义

  • JIT编译器、处理器、存储子系统是按照一定的规则对指令、内存操作的结果进行重排序,给单线程程序造成一种假象:指令是按照源码的顺序执行的,这种假象称为貌似串行语义。但是这并不能保证多线程的正确性
  • 为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序。如果两个操作(指令)访问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data Dependency)
    在这里插入图片描述
    在这里插入图片描述
  • 存在控制依赖关系的语句允许重排序,一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关系(Control Dependency)。比如:if语句允许重排序,可能存在处理器先执行if代码块,再判断if条件是否成立

5. 保证内存访问的顺序性

  • 可以使用volatile关键字、synchronized关键字实现程序的有序性
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值