Java多线程的原子性,可见性,有序性你都了解吗

目录
  • 1.原子性问题
  • 2.可见性问题
  • 3.有序性问题
  • 总结

问题:

1.什么是原子性、可见性、有序性?

1. 原子性问题

原子性、可见性、有序性是并发编程所面临的三大问题。

所谓原子操作,就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换。

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

getstatic i  // 获取静态变量i的值(内存取值)
iconst_1     // 准备常量1
iadd         // 自增 (寄存器增加1)
putstatic i  // 将修改后的值存入静态变量i(存值到内存)

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

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

出现负数的情况:

出现正数的情况:

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

因为这4个操作之间是可以发生线程切换的,或者说是可以被其他线程中断的。所以,++操作不是原子操作,在并行场景会发生原子性问题。

2. 可见性问题

一个线程对共享变量的修改,另一个线程能够立刻可见,我们称该共享变量具备内存可见性。

谈到内存可见性,要先引出Java内存模型的概念。JMM规定,将所有的变量都存放在公共主存中,当线程使用变量时会把主存中的变量复制到自己的工作内存(私有内存)中,线程对变量的读写操作,是自己工作内存中的变量副本。

如果两个线程同时操作一个共享变量,就可能发生可见性问题:

(1) 主存中有变量sum,初始值sum=0;

(2) 线程A计划将sum加1,先将sum=0复制到自己的私有内存中,然后更新sum的值,线程A操作完成之后其私有内存中sum=1,然而线程A将更新后的sum值回刷到主存的时间是不固定的;

(3) 在线程A没有回刷sum到主存前,刚好线程B同样从主存中读取sum,此时值为0,和线程A进行同样的操作,最后期盼的sum=2目标没有达成,最终sum=1;

线程A和线程B并发操作sum发生内存可见性问题:

要想解决多线程的内存可见性问题,所有线程都必须将共享变量刷新到主存,一种简单的方案是:使用Java提供的关键字volatile修饰共享变量。

为什么Java局部变量、方法参数不存在内存可见性问题?

在Java中,所有的局部变量、方法定义参数都不会在线程之间共享,所以也就不会有内存可见性问题。所有的Object实例、Class实例和数组元素都存储在JVM堆内存中,堆内存在线程之间共享,所以存在可见性问题。

3. 有序性问题

所谓程序的有序性,是指程序按照代码的先后顺序执行。如果程序执行的顺序与代码的先后顺序不同,并导致了错误的结果,即发生了有序性问题。

@Slf4j
public class Test3 {
    private static volatile int x=0,y=0;
    private static int a=0,b=0;

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;;i++){
            a=0;
            b=0;
            x=0;
            y=0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });

            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            // 假如t1线程先执行,t2线程后执行,则结果为a=1,x=0,b=1,y=1  (0,1)
            // 假如t2线程先执行,t1线程后执行,则结果为b=1,y=0,a=1,x=1  (1,0)
            // 假如t1线程和t2线程的指令是同时或交替执行的,则结果为a=1,b=1,x=1,y=1 (1,1)
            // 但是不可能出现(0,0)
            if(x==0 && y==0){
                log.debug("x:{}, y:{}",x,y);
            }
        }
    }
}

由于并发执行的无序性,赋值之后x、y的值可能为(1,0)、(0,1)或(1,1)。为什么呢?因为线程t1可能在线程t2开始之前就执行完了,也可能线程t2在线程t1开始之前就执行完了,甚至有可能二者的指令是同时或交替执行的。

然而,执行以上代码时,出乎意料的事情发生了:这段代码的执行结果也可能是(0,0),部分结果如下:

19:37:32.113 [main] DEBUG com.example.test.Test3 - x:0, y:0
19:37:33.041 [main] DEBUG com.example.test.Test3 - x:0, y:0
19:37:34.501 [main] DEBUG com.example.test.Test3 - x:0, y:0
19:37:41.825 [main] DEBUG com.example.test.Test3 - x:0, y:0

于以上程序来说,(0,0)结果是错误的,意味着已经发生了并发的有序性问题。为什么会出现(0,0)结果呢?可能在程序的执行过程中发生了指令重排序。对于线程t1来说,可能a=1和x=b这两个语句的赋值操作顺序被颠倒了,对于线程t2来说,可能b=1和y=a这两个语句的赋值操作顺序被颠倒了,从而出现了(x,y)值为(0,0)的错误结果。

什么是指令重排序?

一般来说,CPU为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行顺序同代码中的先后顺序一致,但是它会保证程序最终的执行结果和代码顺序执行的结果是一致的。

重排序也是单核时代非常优秀的优化手段,有足够多的措施保证其在单核下的正确性。在多核时代,如果工作线程之间不共享数据或仅共享不可变数据,重排序也是性能优化的利器。然而,如果工作线程之间共享了可变数据,由于两种重排序的结果都不是固定的,因此会导致工作线程似乎表现出了随机行为。指令重排序不会影响单个线程的执行,但是会影响多个线程并发执行的正确性。

事实上,输出了乱序的结果,并不代表一定发生了指令重排序,内存可见性问题也会导致这样的输出。但是,指令重排序也是导致乱序的原因之一。

总之,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有得到保证,就有可能会导致程序运行不正确。

总结

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

(0)

相关推荐

  • Java并发之原子性 有序性 可见性及Happen Before原则

    1.原子性(Atomicity) 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响.由Java内存模型来直接保证的原子性变量操作包括read.load.assign.use.store和write这六个,我们大致可以认为,基本数据类型的访问.读写都是具备原子性的(例外就是long和double的非原子性协定).如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock

  • java并发编程之原子性、可见性、有序性

    目录 1原子性 1.1java中的原子性操作 2可见性 2.1可见性问题 2.2解决可见性问题 3有序性 3.1单个线程内程序的指令重排序 3.2多线程内程序的指令重排序 3.3保证有序性的解决方法 3.4volatile保证有序性的原理 4实例分析 4.1原理分析 4.2synchronized结合 4.3Lock结合 4.4使用AtomicInteger替换int 在java中,执行下面这个语句 int i =12; 执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写

  • 了解Java多线程的可见性与有序性

    多线程的可见性 一个线程对共享变量值的修改,能够及时的被其他线程看到. 共享变量 如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量. Java内存模型 JMM(Java Memory Model,简称JMM)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节.它遵循四个原则: 所有的变量都存储在主内存中 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝

  • Java多线程 原子性操作类的使用

    目录 1. 基本类型的使用 2. 数组类型的使用 3. 引用类型的使用 4.字段类型的使用 前言: 在java5以后,我们接触到了线程原子性操作,也就是在修改时我们只需要保证它的那个瞬间是安全的即可,经过相应的包装后可以再处理对象的并发修改,本文总结一下Atomic系列的类的使用方法,其中包含: 1. 基本类型的使用 public class AtomicTest { /** * 常见的方法列表 * * @see AtomicInteger#get() 直接返回值 * @see AtomicIn

  • Java 并发编程的可见性、有序性和原子性

    并发编程无论在哪门语言里,都属于高级篇,面试中也尝尝会被问到.想要深入理解并发编程机制确实不是一件容易的事,因为它涉及到计算机底层和操作系统的相关知识,如果对这部分知识不是很清楚可能会导致理解困难. 在这个专栏里,王子会尽量以白话和图片的方式剖析并发编程本质,希望可以让大家更容易理解. 今天我们就来谈一谈可见性.有序性和原子性都是什么东西. 并发编程的幕后 进入主题之前,我们先来了解一下并发编程的幕后. 随着CPU.内存和I/O设备的不断升级,它们之间一直存在着一个矛盾,就是速度不一致问题.CP

  • Java多线程的原子性,可见性,有序性你都了解吗

    目录 1.原子性问题 2.可见性问题 3.有序性问题 总结 问题: 1.什么是原子性.可见性.有序性? 1. 原子性问题 原子性.可见性.有序性是并发编程所面临的三大问题. 所谓原子操作,就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作.这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换. 例如对于 i++ 而言,实际会产生如下的 JVM 字节码指令: getstatic i // 获取静态变量i的值(内存取值) iconst_1 // 准备常量1 iadd //

  • java多线程Synchronized实现可见性原理解析

    Synchronized实现可见性原理 可见性 要实现共享变量的可见性,必须保证两点: 线程修改后的共享变量值能够及时从工作内存刷新到主内存中 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中 Java语言层面支持的可见性的实现方式 synchronized volatile synchronized实现可见性 synchronized能够实现: 原子性(同步) 可见性 JMM关于synchronized的两条规定: 1.线程解锁前,必须把贡献变量的最新值刷新到主内存中  2.线

  • java多线程从入门到精通看这篇就够了

    目录 一.认识线程及线程的创建 1.线程的概念 2.线程的特性 3.线程的创建方式 <1>继承Thread类 <2>实现Runnable接口 <3>实现Callable接口 二.线程的常用方法 1.构造方法和属性的获取方法 2.常用方法 <1>run()和start() <2>interrupt()方法 <3>join方法 <4>获取当前线程的引用currentThread();方法 <5>休眠当前线程slee

  • java 多线程Thread与runnable的区别

    java 多线程Thread与runnable的区别 java中实现多线程的方法有两种:继承Thread类和实现runnable接口 1,继承Thread类,重写父类run()方法 public class thread1 extends Thread { public void run() { for (int i = 0; i < 10000; i++) { System.out.println("我是线程"+this.getId()); } } public static

  • java多线程中的异常处理机制简析

    在java多线程程序中,所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉.这一点是通过java.lang.Runnable.run()方法声明(因为此方法声明上没有throw exception部分)进行了约束.但是线程依然有可能抛出unchecked exception,当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常

  • Java多线程并发编程 并发三大要素

    一.原子性 原子,一个不可再被分割的颗粒.原子性,指的是一个或多个不能再被分割的操作. int i = 1; // 原子操作 i++; // 非原子操作,从主内存读取 i 到线程工作内存,进行 +1,再把 i 写到朱内存. 虽然读取和写入都是原子操作,但合起来就不属于原子操作,我们又叫这种为"复合操作". 我们可以用synchronized 或 Lock 来把这个复合操作"变成"原子操作. 例子: private synchronized void increase

  • Java多线程并发编程 Volatile关键字

    volatile 关键字是一个神秘的关键字,也许在 J2EE 上的 JAVA 程序员会了解多一点,但在 Android 上的 JAVA 程序员大多不了解这个关键字.只要稍了解不当就好容易导致一些并发上的错误发生,例如好多人把 volatile 理解成变量的锁.(并不是) volatile 的特性: 具备可见性 保证不同线程对被 volatile 修饰的变量的可见性. 有一被 volatile 修饰的变量 i,在一个线程中修改了此变量 i,对于其他线程来说 i 的修改是立即可见的. 如: vola

随机推荐