栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

线程安全问题以及解决

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

线程安全问题以及解决

文章目录
  • 一、 观察线程不安全现象
  • 二、线程安全
  • 三、 线程不安全的原因
    • 1、站在开发者角度:
    • 2、站在系统角度:
      • 原子性
        • 为什么COUNT越大,出错的概率就越大?
      • 可见性
      • 代码顺序性
  • 那么如何解决线程安全呢?

一、 观察线程不安全现象
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要求无论怎么优化,对于单线程的视角,结果不会有改变。但是并没有规定多线程环境的情况,就导致在多线程情况下可能出现问题。

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

加锁

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

谢谢浏览

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/884658.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号