深入探究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线程不安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详谈异步log4j2中的location信息打印问题

    详谈异步log4j2中的location信息打印问题

    这篇文章主要介绍了详谈异步log4j2中的location信息打印问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • java对象转化成String类型的四种方法小结

    java对象转化成String类型的四种方法小结

    在java项目的实际开发和应用中,常常需要用到将对象转为String这一基本功能。本文就详细的介绍几种方法,感兴趣的可以了解一下
    2021-08-08
  • Spring与Struts整合之让Spring管理控制器操作示例

    Spring与Struts整合之让Spring管理控制器操作示例

    这篇文章主要介绍了Spring与Struts整合之让Spring管理控制器操作,结合实例形式详细分析了Spring管理控制器相关配置、接口实现与使用技巧,需要的朋友可以参考下
    2020-01-01
  • springmvc+spring+mybatis实现用户登录功能(下)

    springmvc+spring+mybatis实现用户登录功能(下)

    这篇文章主要为大家详细介绍了springmvc+spring+mybatis实现用户登录功能的第二篇,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-07-07
  • B/S与C/S架构的区别介绍

    B/S与C/S架构的区别介绍

    本文详细讲解了B/S与C/S架构的区别,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-12-12
  • Java基础精讲方法的使用

    Java基础精讲方法的使用

    方法,也称函数,如果想要重复一段或者多段代码块的使用,可以将这些代码封装成一个方法,方法具体表现为某种行为,使用方法可以提高代码的复用性
    2022-05-05
  • Java面试题冲刺第二十四天--并发编程

    Java面试题冲刺第二十四天--并发编程

    这篇文章主要为大家分享了最有价值的三道关于数据库的面试题,涵盖内容全面,包括数据结构和算法相关的题目、经典面试编程题等,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • SpringBoot集成RabbitMQ和概念介绍

    SpringBoot集成RabbitMQ和概念介绍

    这篇文章主要介绍了SpringBoot集成RabbitMQ和概念介绍,RabbitMQ即一个消息队列,主要是用来实现应用程序的异步和解耦,同时也能起到消息缓冲,消息分发的作用。更多相关内容需要的小伙伴可以参考一下下面文章内容
    2022-05-05
  • Java中CountDownLatch进行多线程同步详解及实例代码

    Java中CountDownLatch进行多线程同步详解及实例代码

    这篇文章主要介绍了Java中CountDownLatch进行多线程同步详解及实例代码的相关资料,需要的朋友可以参考下
    2017-03-03
  • spring data jpa使用详解(推荐)

    spring data jpa使用详解(推荐)

    这篇文章主要介绍了spring data jpa使用详解(推荐),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-04-04

最新评论