Java多线程之线程安全问题详解

 更新时间:2022年03月02日 16:56:35   作者:小小茶花女  
这篇文章主要为大家详细介绍了Java多线程之线程安全问题,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助

面试题:

  • 什么是线程安全和线程不安全?
  • 自增运算是不是线程安全的?如何保证多线程下 i++ 结果正确?

1. 什么是线程安全和线程不安全?

什么是线程安全呢?当多个线程并发访问某个Java对象时,无论系统如何调度这些线程,也无论这些线程将如何交替操作,这个对象都能表现出一致的、正确的行为,那么对这个对象的操作是线程安全的。

如果这个对象表现出不一致的、错误的行为,那么对这个对象的操作不是线程安全的,发生了线程的安全问题。

2. 自增运算为什么不是线程安全的?

线程安全实验:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?具体的代码如下

public class ThreadDemo {
    private static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        // 线程1对变量i做5000次自增运算
         Thread t1 = new Thread(()->{
             for(int j=0;j<5000;j++){
                 i++;
             }
         });
         Thread t2 = new Thread(()->{
             for(int j=0;j<5000;j++){
                 i--;
             }
         });
         t1.start();
         t2.start();
         // 主线程等待t1线程和t2线程执行结束再继续执行
         t1.join();
         t2.join();
        System.out.println(i);// 581 / -1830 / 0
    }
}

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。

例如对于 i++ 而言,实际会产生如下的 JVM 字节码指令:

getstatic i  // 获取静态变量i的值
iconst_1     // 准备常量1
iadd         // 自增
putstatic i  // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i  // 获取静态变量i的值
iconst_1     // 准备常量1
isub         // 自减
putstatic i  // 将修改后的值存入静态变量

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

在这里插入图片描述

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

在这里插入图片描述

但多线程下这 8 行代码可能交错运行:

出现负数的情况:

在这里插入图片描述

出现正数的情况:

在这里插入图片描述

因此,一个自增运算符是一个复合操作,至少包括三个JVM指令:“内存取值”“寄存器增加1”和“存值到内存”。这三个指令在JVM内部是独立进行的,中间完全可能会出现多个线程并发进行。“内存取值”“寄存器增加1”和“存值到内存”这三个JVM指令本身是不可再分的,它们都具备原子性,是线程安全的,也叫原子操作。但是,两个或者两个以上的原子操作合在一起进行操作就不再具备原子性了。比如先读后写,就有可能在读之后,其实这个变量被修改了,出现读和写数据不一致的情况。

3. 临界区资源和竞态条件

在多个线程操作相同资源(如变量、数组或者对象)时就可能出现线程安全问题。一般来说,只在多个线程对这个资源进行写操作的时候才会出现问题,如果是简单的读操作,不改变资源的话,显然是不会出现问题的。

临界区资源表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程则必须等待。在并发情况下,临界区资源是受保护的对象。

临界区代码段是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后执行临界区代码段,执行完成之后释放资源。临界区代码段的进入和退出如图所示:

在这里插入图片描述

竞态条件可能是由于在访问临界区代码段时没有互斥地访问而导致的特殊情况。如果多个线程在临界区代码段的并发执行结果可能因为代码的执行顺序不同而不同,我们就说这时在临界区出现了竞态条件问题。

比如下面代码中的临界区资源和临界区代码段:

public class SafeDemo {
    // 临界区资源
    private static int i = 0;
    // 临界区代码段
    public void selfIncrement(){
        for(int j=0;j<5000;j++){
            i++;
        }
    }
    // 临界区代码段
    public void selfDecrement(){
        for(int j=0;j<5000;j++){
            i--;
        }
    }
	// 这个不是临界区代码,因为虽然使用了共享资源,但是这个方法并没有被多个线程同时访问
    public int getI(){
        return i;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeDemo safeDemo = new SafeDemo();
        Thread t1 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        Thread t2 = new Thread(()->{
            safeDemo.selfDecrement();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(safeDemo.getI());
    }
}

当多个线程访问临界区的selfIncrement()方法时,就会出现竞态条件的问题。更标准地说,当两个或多个线程竞争同一个资源时,对资源的访问顺序就变得非常关键。为了避免竞态条件的问题,我们必须保证临界区代码段操作具备排他性。这就意味着当一个线程进入临界区代码段执行时,其他线程不能进入临界区代码段执行。

总结:

(1) 一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,而在多个线程对共享资源读写操作时发生指令交错,就会出现问题 ;

(2) 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区代码块;

(3) 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件;

在Java中,可以使用synchronized关键字,使用Lock显式锁实例,或者使用原子变量(AtomicVariables)对临界区代码段进行排他性保护。

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!      

相关文章

  • java validation 后台参数验证的使用详解

    java validation 后台参数验证的使用详解

    本篇文章主要介绍了java validation 后台参数验证的使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • Java 基础之修饰符关键词整理

    Java 基础之修饰符关键词整理

    这篇文章主要介绍了Java 基础之修饰符关键词整理的相关资料,需要的朋友可以参考下
    2017-02-02
  • 详解springboot中mybatis注解形式

    详解springboot中mybatis注解形式

    在本文中小编给大家分享了关于springboot中mybatis注解形式的介绍,有兴趣的可以跟着学习下。
    2018-10-10
  • springboot集成swagger3与knife4j的详细代码

    springboot集成swagger3与knife4j的详细代码

    这篇文章主要介绍了springboot集成swagger3与knife4j,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • SpringBoot-JPA删除不成功,只执行了查询语句问题

    SpringBoot-JPA删除不成功,只执行了查询语句问题

    这篇文章主要介绍了SpringBoot-JPA删除不成功,只执行了查询语句问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 利用Java编写一个出敬业福的小程序

    利用Java编写一个出敬业福的小程序

    新年将至,又开始扫福活动,每年的敬业福成了大家难过的坎。所以本文将介绍一个通过Java编写的一款福字生成器,感兴趣的小伙伴可以试一试
    2022-01-01
  • Java 多线程并发编程_动力节点Java学院整理

    Java 多线程并发编程_动力节点Java学院整理

    这篇文章主要介绍了Java 多线程并发编程的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2017-05-05
  • Java FileDescriptor总结_动力节点Java学院整理

    Java FileDescriptor总结_动力节点Java学院整理

    FileDescriptor 是“文件描述符”。可以被用来表示开放文件、开放套接字等。接下来通过本文给大家分享Java FileDescriptor总结,感兴趣的朋友一起学习吧
    2017-05-05
  • JAVA生成pdf文件的实操教程

    JAVA生成pdf文件的实操教程

    PDF是可移植文档格式,是一种电子文件格式,具有许多其他电子文档格式无法相比的优点,下面这篇文章主要给大家介绍了关于JAVA生成pdf文件的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2022-11-11
  • Mybatis的Mapper中的方法为什么不能重载

    Mybatis的Mapper中的方法为什么不能重载

    这篇文章主要介绍了Mybatis的Mapper中的方法为什么不能重载,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09

最新评论