线程安全问题以及解决

一、 观察线程不安全现象

/**
 * 演示线程不安全现象
 * @author Kobayashi
 * @date 2022/05/10 20:10
 **/
public class ThreadInsecurity {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;

    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 100;

    // 定义两个线程,分别对 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();

        System.out.println(r); // 0
    }
} 

理论上 r 被加了1000000次,也被减了1000000次,所以应该结果为0。
但实际上却是
在这里插入图片描述
在线程不安全时候,代码无法百分百得到我们想要的预期结果,所以线程安全就可以保证代码100%符合预期。

二、线程安全

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

三、 线程不安全的原因

1、站在开发者角度:

多个线程之间操作同一块数据(共享data)
并且至少有一个线程在修改这块数据

**即使在多线程代码中,也有情况不需要考虑线程安全问题。

  1. 几个线程之间互相没有任何数据共享,天生是线程安全的
  2. 几个线程之间有数据共享,但是都做读操作,没有写操作,也是线程安全的。

2、站在系统角度:

原子性

原子性我们可以理解为,一个操作或者多个操作,要么全部执行并且执行的过程中不被任何因素打断,要么就都不执行。和数据库里边的事务是一样的道理。

首先我们得知道:
对r++或者r–
1.从内存中(r代表的内存区域)把数据加载到寄存器中 LOAD_r
2.对数据加1 ADD1
3.把寄存器中的值写回内存中 STORE_r

所以可能会存在一种情况:有一个线程对 r 进行了r++操作,但是还没有写回内存的时候,另一个线程跑进来又对 r 进行了r–操作,最终写回到内存的r的值就错了!

为什么COUNT越大,出错的概率就越大?

COUNT越大,线程执行需要跨时间片的概率就越大,碰到线程调度的概率就越大,就会导致中间出错的概率提升。

可见性

可见性指的是,一个线程对共享变量值的修改,能够及时的被其他线程所看到。

java内存模型(JMM):java虚拟机规范中定义了内存模型
目的是为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台都能达到一致的并发效果。
在这里插入图片描述

  • 线程之间的共享变量存在主内存
  • 每一个线程都有自己的“工作内存”
  • 当线程要读取一个共享变量时,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
  • 当线程要修改一个共享变量时,会先修改工作内存中的副本,在同步回主内存中

在这里插入图片描述
这时候代码中就容易出现问题!

代码顺序性

程序执行的顺序按照代码的先后顺序执行

举个栗子:
一段代码,

  1. 去厕所刷个牙
  2. 回屋里喝一杯牛奶
  3. 去厕所照照镜子

如果单线程情况下,JVM,CPU指令集会对其进行优化,比如,会按照 1 -> 3 -> 2 的顺序执行,从结果上来看也没有什么问题,还少跑一次撤硕。这种叫做指令重排序。
重排序,就是执行指令的顺序和书写指令的顺序不一致。

JVM规定了一些重排序基本原则: happend-before原则
简单解释就是:
JVM要求无论怎么优化,对于单线程的视角,结果不会有改变。但是并没有规定多线程环境的情况,就导致在多线程情况下可能出现问题。

那么如何解决线程安全呢?

加锁

/**
 * @author Kobayashi
 * @date 2022/05/10 21:08
 **/
public class Demo1 {
    // 定义一个共享的数据 —— 静态属性的方式来体现
    static int r = 0;

    // 定义加减的次数
    // COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
    static final int COUNT = 1000000;

    // 定义两个线程,分别对 r 进行 加法 + 减法操作
    // r++ 和 r-- 互斥
    static class Add extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                synchronized (Demo1.class) {
                    r++;    // r++ 是原子的
                }
            }
        }
    }

    static class Sub extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < COUNT; i++) {
                synchronized (Demo1.class) {
                    r--;    // 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); // 0
    }

}

synchronized加锁的用法还有很多种,这里只使用了其中一种。

关于线程方面的代码总结:
https://gitee.com/Z-Y-Hhhh/zyh_classcode/tree/master/src/thread

谢谢浏览
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值