Java并发编程之三大特性

25 阅读8分钟

Java并发编程之三大特性

1、老生长谈,先来说一说并行与并发是什么

  • 并行:指在同一时刻,有多条指令在多个处理器上同时执行。无论从微观还是从宏观来看,二者都是一起执行的。

  • 并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执 行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

1679300754570.png - 就像上面这张图,只有一个打饭阿姨的时候,一个阿姨其实是在并发被使用的。而有多个打饭阿姨的时候,多个阿姨之间才是并行被使用的

2、再来谈一下并发编程的几个特性---并发编程Bug的源头:可见性、原子性和有序性问题

2.1、可见性

  • 概念:当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介

    的方法来实现可见性的。

  • 在进一步解释可见性之前,让我们想一想我们该如何保证可见性或者保证可见性有哪些方式? 我所知的可以通过以下的几种方式来保证可见性问题:

    • 通过volatile关键字保证可见性
    • 通过内存屏障保证可见性
    • 通过synchronized关键字保证可见性
    • 通过Lock保证可见性
    • 通过final关键字保证可见性
  • 原因

    • 通过volatile关键字保证可见性-实际上使用的内存屏障

      • 当JVM将.class文件编译为具体的CPU执行指令后,观察这些指令可以发现,在加了volatile修饰的共享变量,都会在前面加上以恶搞lock为前缀的指令,lock前缀指令会引发两个事情:

        • 1、将当前处理器缓存行的数据写回到系统内存种
        • 2、一个处理器的缓存回写到内存会导致其他处理器的缓存失效

        基于这两点volatile便可保证起可见性

    • 通过内存屏障保证可见性--基于上面详解

      • volatile修饰的变量,在每个读操作(load操作)之前都加上屏障,强制从主内存读取最新数据。每次assign赋值后面,加上Store屏障,强制将数据刷新到主内存

    1679304588454.png

    • 通过sychronized关键字保证可见性

      • sychronized底层是通过monitorenter的指令来进行加锁的、通过monitorexit指令来释放锁的。monitorenter指令其实还具有Load屏障的作用。也就是通过monitorenter指令之后,synchronized内部的共享变量,每次读取数据的时候被强制从主内存读取最新的数据。并且monitorexit指令也具有Store屏障的作用,也就是让synchronized代码块内的共享变量,如果数据有变更的,强制刷新回主内存。通过这种方式,数据修改之后立即刷新回主内存,其他线程进入synchronized代码块后,使用共享变量的时候强制读取主内存的数据,上一个线程对共享变量的变更操作,它就能立即看到了。
    • 通过Lock保证可见性

    • 通过final关键字保证可见性-->前提:未发生this引用逃逸

2.2、有序性

  • 概念:为了性能优化,JVM会在不改变数据依赖性的情况下,运行编译起和处理器对指令序列进行重排序,儿有序性的问题指的就是程序代码执行的顺序与程序员编写程序的顺序不一致,导致程序结果不正确的问题。

  • 同上,也让我们想一想我们该如何保证有序性或者保证有序性有哪些方式?

  • 我所知的可以通过以下的几种方式来保证有序性问题:

    • 也是通过volatile关键字来保证有序性
    • 也可以通过内存屏障来保证有序性
    • 也可通过synchronized关键字来保证有序性
    • 也可通过Lock保证可见性
  • 原因:

    • volatile关键字

      • volatile关键字解决有序性的问题其实就是通过内存屏障来进行的

        • 在每个volatile写操作的前面插入一个StoreStore屏障,禁止StoreStore屏障前面的普通写操作和volatile写操作重排序

        • 在每个volatile写操作的后面插入一个StoreLoad屏障,禁止StoreLoad屏障后面的普通读操作和volatile写操作重排序

        • 在每个volatile读操作的后面插入一个LoadLoad屏障,禁止volatile读操作和LoadLoad屏障后面的普通读操作重排序

        • 在每个volatile读操作的后面插入一个LoadStore屏障,禁止volatile读操作和LoadStore屏障后面的普通写操作重排序

      1679303611721.png

    • 内存屏障--同上

    • synchronized

      • 通过monitorenter、monitorexit指令嵌入上面的内存屏障;monitorenter、monitorexit这两条指令其实就既具有加锁、释放锁的功能,同时也具有内存屏障的功能

    1679306167645.png

    • Lock保证可见性

2.3、原子性

  • 概念:一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在

    Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。

  • 同上,再让我们想一想我们该如何保证原子性或者保证原子性有哪些方式?

  • 我所知的可以通过以下的几种方式来保证原子性问题:

    • 也是通过synchronized关键字来保证原子性

      • 通过加锁,保证同一时间只能有一个线程去执行synchronized 中的代码块,这个操作就是不能被其它线程打断的,天然的具有了原子性
    • 通过Lock保证原子性

    • 通过CAS保证原子性

3、由此产生了一系列的问题

3.1、CAS是什么?他有什么优缺点?该如何解决?

  • CAS操作包含三个操作数:内存位置V,期望值A,以及新值B。当且仅当内存位置上的值与期望值相等时,才会将该位置的值更新为新值B,否则,不做任何操作

  • 优点

    • 可以减少系统的开销
  • 缺点

    • ABA问题

      • 如果判断内存里的值和期望值相等,我们就执行更新。但问题是,即使满足这个条件,也不能保证在这段时间里没有其他的线程对这个数据进行更新。因为内存里的值相等,有可能从来没有变化过,也有可能经过一系列变化之后再变回原来的值,这就是ABA问题。在很多情况下,ABA问题不会影响执行正确性,我们并不需要关心。但是也会有例外的情况。

      • 解决方法

        • 除了维护最原始的三个值之外,再维护一个版本号字段,在进行数据更新的时候,版本号也要更新。所以在进行比对的时候,除了比对期望值和内存值,也要比对期望版本号和内存里的版本号。JDK1.5可以利用AtomicStampedReference类来解决这个问题,其内部除了维护value之外,还维护了一个时间戳stamp。那进行更新的时候,除了更新value,还需要更新这个时间戳。
    • 循环时间长开销大

      • CAS一般来说会配合自旋来实现锁功能。但在并发冲突比较严重的场景下,自旋CAS会有大量操作失效,也就是

      • 解决方案

        • 使用JVM处理器提供的pause指令,这样的话,效率会有一定的提升,pause指令有两个作用

          • 延迟流水线执行指令,使CPU不会消耗过多的执行资源
          • 避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率
    • 不能保证多个共享变量的原子操作

      • CAS可以保证一个共享变量的原子操作,但如果涉及多个变量,就无法保证操作的原子性。

      • 解决方法

        • 1.使用互斥锁,对相关操作直接加锁,简单粗暴。

          2.将多个共享变量存储在一个对象之中,然后对一个对象进行原子操作。从JDK1.5开始就有AtomicReference类保证引用对象操作的原子性,只需要把多个共享变量放在一个对象里进行CAS操作即可。

3.2、敬请期待。。。。