线程安全问题

1. 观察线程不安全

 接下来我们现象演示什么是线程不安全:在代码“没有问题”的情况下,但结果是错误的(无法100%得到预期的结果)

// 演示线程不安全的现象
public class Main {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;

    // 定义加减的次数
    static final int COUNT = 100000;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r++;
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                r--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Add add = new Add();
        add.start();

        Sub sub = new Sub();
        sub.start();
       //等待两个线程结束
        add.join();
        sub.join();

        // 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
        // 所以,结果应该是 0
        System.out.println(r);
    }
}

发现每次的结果不一样 且没有达到预期结果

原因分析:

(1)站在开发者角度

  •   多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据;
  •   至少有一个线程在修改这块共享数据。

 多个线程中至少有一个在对共享数据做写操作。

 即使在多线程的代码中,哪些情况下不需要考虑线程安全问题呢?

  • 几个线程之间互相没有任何数据共享的情况下,天生是线程安全的。
  • 几个线程之间即使有共享数据,但都是读操作,没有写操作时,也是天生线程安全的。

 (2)系统角度

前置知识:

  1. 高级语言(java代码)中的一条语句,很可能对应的时多条指令,以r++为例:r++的实质就是r = r + 1,变成指令动作:LOAD_A、ADD 1、STORE_A
  2. 线程调度是可能发生在任意时刻的,但不会切割指令(一条指令只有执行完/完全没有执行两种可能)。
  3. 单看 LOAD_A、ADD 1、STORE_A这三条指令,线程调度的发生可能会在四个时刻。

 


我们的预期是r ++或者r --是一个原子性的操作(全部完成或者全部没有完成),但实际执行起来,保证不了原子性,所以会出错。

在上面的例子中,我们可以发现,当设置的count值越大时,出错概率也是越大的,原因在于count越大,线程执行需要跨时间片的概率越大,导致中间出错的概率越大。

2. 线程安全的概念

 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

3. 线程不安全的原因

3.1  原子性

++的操作,本质上是三步操作,是一个"非原子"的操作。

可以通过加锁的方式,把这个操作变成原子性。

3.2 内存可见性

 前置知识:CPU为了提升数据获取速度,一般会在CPU中设置缓存(cache),因为指令的执行速度  >>  内存的读写速度。

而JVM规定了JVM内存模型来模拟这种现象:在这个规定中,把一个线程想象成一个CPU

  1. 主存储/主内存:是对真实内存的模拟;
  2. 工作存储/工作内存:是对CPU中缓存的模拟。

线程的所有数据操作(读或写)必须:

  1. 从主内存加载到工作内存中;
  2. 在工作内存中进行处理(允许在工作内存中处理很久);
  3. 完成最终的处理之后,再把数据同步回主内存。

一个线程对数据的操作,很可能其他线程是无法感知的,甚至某些情况下,会被编译器优化成完全看不到的结果!

可以采用synchronized关键字对线程加锁或者使用volatile关键字保证内存可见性!

3.3 代码重排序 

指令重排序导致线程不安全问题,这也是由于编译器的优化操作而导致的线程不安全问题!我们的代码先后执行顺序有时候并不会影响我们的结果,那么这时编译器在不改变代码逻辑的基础上就会改变一下顺序,提高运行效率,而这个操作在多线程往往会出现线程不安全问题!

这里也可以使用synchronized关键字避免指令重排列!

注:JVM也规定了一些重排序的基本原则:happend-before规则。可解释为:JVM要求,无论怎么优化,对于单线程来说结果不应该有改变。但并没有规定多线程环境的情况(不能规定),导致在多线程环境下可能出现问题。

4. 小结线程安全问题

程序的线程安全:运行结果100%符合预期。

Java语境下,经常说某个类、对象是线程安全的:这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是“简单”使用,可以不考虑线程安全的问题。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值