美文网首页
并发编程-解决原子性、可见性、有序性问题

并发编程-解决原子性、可见性、有序性问题

作者: 我可能是个假开发 | 来源:发表于2023-02-06 23:16 被阅读0次

上一篇中讲解了导致可见性的原因是缓存,导致有序性的原因是编译优化,解决可见性、有序性最直接的办法就是禁用缓存和编译优化。合理的方案应该是按需禁用缓存以及编译优化。

一、解决原子性、可见性、有序性问题

1.解决原子性问题

除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过synchronizedLock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

2.解决可见性问题

volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

3.解决有序性问题

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

二、Java内存模型

Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。

每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访
问其他线程的工作内存。Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

1.指令重排序

java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的
发挥机器性能。

2.as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

3.happens-before 原则

只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下:

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  • 传递性 A先于B ,B先于C 那么A必然先于C
  • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见
  • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 对象终结规则对象的构造函数执行,结束先于finalize()方法

四、volatile内存语义

volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用:

  • 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改
    了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
  • 禁止指令重排序优化

1.volatile的可见性

关于volatile的可见性作用,被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中

public class VolatileVisibilitySample {
    volatile boolean initFlag = false;
    public void save(){
        this.initFlag = true;
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }
    public void load(){
        String threadname = Thread.currentThread().getName();
        while (!initFlag){
            //线程在此处空跑,等待initFlag状态改变
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变");
    }
    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.save();
        },"threadA");
        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

线程A改变initFlag属性之后,线程B马上感知到

2.volatile无法保证原子性

在并发场景下,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。

声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。

// 以下代码来源于【参考1】
class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢?
    }
  }
}

示例代码,假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢?

  • 在低于 1.5 版本上运行,x 可能是 42,也有可能是 0;
  • 在 1.5 以上的版本上运行,x 就是等于 42。

3.volatile禁止重排优化

volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象.

五、volatile缓存可见性实现原理总结

底层通过汇编lock前缀指令,会锁定这块内存的缓存(缓存行锁定)并回写到主内存
IA-32和Intel64架构软件开发者手册对lock指令的解释:

  • 会将当前处理器缓存行的数据立即写回系统内存
  • 这个写回内存的操作会引起其他CPU里缓存了该内存地址的数据无效(MESI协议)
  • 提供内存屏障功能,使lock前后指令不能重排序

极客时间《Java并发编程实战》学习笔记Day02 - http://gk.link/a/11W9i

相关文章

  • Java多线程

    01 |可见性、原子性和有序性问题:并发编程Bug的源头 原子性:线程切换导致原子性。 可见性:CPU缓存导致可见...

  • Java并发——volatile、synchronized、lo

    在并发编程中有三个典型问题:原子性问题,可见性问题,有序性问题。 原子性问题 原子性:即一个操作或者多个操作 要么...

  • 一男子给对象转账5000元,居然又退还了!

    在并发编程中,所有问题的根源就是可见性、原子性和有序性问题,这篇文章我们就来聊聊原子性问题。 在介绍原子性问题之前...

  • 互斥锁,解决原子性问题

    并发编程有3个源头性问题:缓存导致的可见性问题,编译优化导致的有序性问题,以及线程切换导致的原子性问题。解决可见性...

  • 【漫画】JAVA并发编程 如何解决原子性问题

    在并发编程BUG源头文章中,我们初识了并发编程的三个bug源头:可见性、原子性、有序性。在如何解决可见性和原子性文...

  • Java并发编程

    并发编程的三个问题:原子性问题,可见性问题,有序性问题 原子性 概念简介 一个操作或者多个操作,要么全部执行,要么...

  • synchronized

    synchronized 并发编程的三个问题:并发性,原子性,有序性 多线程并发时候可能出现可见性问题:就是在多线...

  • volatile关键字

    在并发编程中,常遇到的三个问题:原子性,可见性,有序性。volatile可解决可见性和有序性的问题。 1、可见性 ...

  • Java中Volatile和synchronized

    JMM 内存模型:Java Memory Model问题:并发过程中如何处理可见性 原子性和有序性问题 并发编程中...

  • 原子性 可见性 有序性 以及 Volatile 关键字使用

    Java 并发编程问题 在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。这些问题发生...

网友评论

      本文标题:并发编程-解决原子性、可见性、有序性问题

      本文链接:https://www.haomeiwen.com/subject/kscxkdtx.html