深入探究Java线程不安全的原因与解决

 更新时间:2022年04月26日 09:18:26   作者:淡沫初夏Zz  
线程不安全这个问题,一般在学Java时,我们老师会让我们背诵一段长长的话。"当不同线程同时能访问同一个变量时,可能会导致线程不安全"。实际上,这句话重点想突出的只有原子性。而我们往往考虑线程不安全的原因,会从三方面进行考虑:就是原子性,可见性,有序性

一、什么是线程安全

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:

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

二、线程不安全的原因

1、修改共享数据

static class Counter {
    public int count = 0;
    void increase() {
        count++;
    }
}
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
}

上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.此时这个 counter.count 是一个多个线程都能访问到的 “共享数据”

2、原子性

原子性就是 提供互斥访问,同一时刻只能有一个线程对数据进行操作,有时也把这个现象叫做同步互斥,表示操作是互相排斥的

不保证原子性会给多线程带来什么问题 如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大

3、内存可见性

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

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

   private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
            }
            System.out.println(Thread.currentThread().getName() +
                    "执⾏完成");
        });
        t1.start();
        Scanner scanner = new Scanner(System.in);
        System.out.print("->");
        count = scanner.nextInt();
    }

4、指令重排序

一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。

编译器对于指令重排序的前提

“保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价

三、解决线程安全方案

  • volatile解决内存可见性和指令重排序

代码在写入 volatile 修饰的变量的时候:

改变线程⼯作内存中volatile变量副本的值,将改变后的副本的值从⼯作内存刷新到主内存

  • 直接访问工作内存,速度快,但是可能出现数据不⼀致的情况
  • 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了

代码示例:

/**
 * 内存可见性
 * 线程1没感受到flag的变化,实际线程2已经改变了flag的值
 * 使用volatile,解决内存可见性和指令重排序
 */
public class ThreadSeeVolatile {
    //全局变量
    private volatile static boolean flag = true;
    public static void main(String[] args) {
        //创建子线程
        Thread t1 = new Thread(() ->{
            System.out.println("1开始执行:" + LocalDateTime.now());
            while(flag){
            }
            System.out.println("2结束执行" + LocalDateTime.now());
        });
        t1.start();
        Thread t2 = new Thread(() ->{
            //休眠1s
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("修改flag=false"+ LocalDateTime.now());
            flag = false;
        });
        t2.start();
    }
}

volatile的缺点

volatile 虽然可以解决内存可见性和指令重排序的问题,但是解决不了原子性问题,因此对于 ++ 和 --操作的线程非安全问题依然解决不了

  • 通过synchronized锁实现原子性操作

JDK提供锁分两种:

①一种是synchronized,依赖JVM实现锁,因此在这个关键字作用对象的作用范围内是同一时刻只能有一个线程进行操作;

②另一种是LOCK,是JDK提供的代码层面的锁,依赖CPU指令,代表性的是ReentrantLock。

  • synchronized 会起到互斥效果, 某个线程执行到某个对象的synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized修饰的对象有四种:

(1)修饰代码块,作用于调用的对象

(2)修饰方法,作用于调用的对象

(3)修饰静态方法,作用于所有对象

(4)修饰类,作用于所有对象

   // 修饰一个代码块: 明确指定锁哪个对象
    public void test1(int j) {
        synchronized (this) {
        }
    }
    // 修饰一个方法
    public synchronized void test2(int j) {
    }
    // 修饰一个类
    public static void test1(int j) {
        synchronized (SynchronizedExample2.class) {
        }
    }
    // 修饰一个静态方法
    public static synchronized void test2(int j) {
    }

到此这篇关于深入探究Java线程不安全的原因与解决的文章就介绍到这了,更多相关Java线程不安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java中SynchronousQueue的底层实现原理剖析

    Java中SynchronousQueue的底层实现原理剖析

    BlockingQueue的实现类中,有一种阻塞队列比较特殊,就是SynchronousQueue(同步移交队列),队列长度为0。本文就来剖析一下SynchronousQueue的底层实现原理,感兴趣的可以了解一下
    2022-11-11
  • java实战案例之用户注册并发送邮件激活/发送邮件验证码

    java实战案例之用户注册并发送邮件激活/发送邮件验证码

    现在很多的网站都提供有用户注册功能,当我们注册成功之后就会收到封注册网站的邮件,邮件里包含了我们的注册的用户名和密码及激活账户的超链接等信息,这篇文章主要给大家介绍了关于java实战案例之用户注册并发送邮件激活/发送邮件验证码的相关资料,需要的朋友可以参考下
    2021-09-09
  • Java实现对视频进行截图的方法【附ffmpeg下载】

    Java实现对视频进行截图的方法【附ffmpeg下载】

    这篇文章主要介绍了Java实现对视频进行截图的方法,结合实例形式分析了Java使用ffmpeg针对视频进行截图的相关操作技巧,并附带ffmpeg.exe文件供读者下载使用,需要的朋友可以参考下
    2018-01-01
  • Springcloud+Mybatis使用多数据源的四种方式(小结)

    Springcloud+Mybatis使用多数据源的四种方式(小结)

    这篇文章主要介绍了Springcloud+Mybatis使用多数据源的四种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Java基于二分搜索树、链表的实现的集合Set复杂度分析实例详解

    Java基于二分搜索树、链表的实现的集合Set复杂度分析实例详解

    这篇文章主要介绍了Java基于二分搜索树、链表的实现的集合Set复杂度分析,结合实例形式详细分析了Java基于二分搜索树、链表的实现的集合Set复杂度分析相关操作技巧与注意事项,需要的朋友可以参考下
    2020-03-03
  • Java的深拷贝和浅拷贝深入了解

    Java的深拷贝和浅拷贝深入了解

    这篇文章主要为大家介绍了Java的深拷贝和浅拷贝,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • 详解Elasticsearch如何把一个索引变为只读

    详解Elasticsearch如何把一个索引变为只读

    这篇文章主要为大家介绍了详解Elasticsearch如何把一个索引变为只读示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • SpringBoot中的404错误:原因、影响及解决策略

    SpringBoot中的404错误:原因、影响及解决策略

    本文详细介绍了SpringBoot中404错误的出现原因、影响以及处理策略,404错误常见于URL路径错误、控制器配置问题、静态资源配置错误、依赖缺失或版本不兼容、配置错误和服务器配置问题,解决方法包括检查URL路径、审查控制器配置、配置静态资源
    2025-02-02
  • 老生常谈Java中instanceof关键字的理解

    老生常谈Java中instanceof关键字的理解

    java 中的instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例。这篇文章主要介绍了老生常谈Java中instanceof关键字的理解,需要的朋友可以参考下
    2018-10-10
  • Java 处理树形结构数据的过程

    Java 处理树形结构数据的过程

    这篇文章主要介绍了Java 处理树形结构数据的过程,本文给大家分析具体实现过程,结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08

最新评论