Java并发编程——锁相关Java内存模型volatileCAS原子操作类线程池

Posted AC_Jobim

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程——锁相关Java内存模型volatileCAS原子操作类线程池相关的知识,希望对你有一定的参考价值。

一、Java锁相关

1.1 悲观锁和乐观锁

悲观锁:

  • 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁:

  • 乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过。乐观锁的实现方案一般来说有两种: 版本号机制CAS实现 。乐观锁多适用于多度的应用类型,这样可以提高吞吐量。
  • 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

1.2 公平锁和非公平锁

公平锁:

  • 所谓的公平是指在锁等待队列中获取到锁先后关系,先到先得的思想。优先队列中的线程获取锁。

非公平锁:

  • 加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
    • 非公平锁性能比公平锁高,因为公平锁需要在多核的情况下维护一个队列
    • Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的也是非公平锁。

1.3 死锁(重点)

什么是死锁?

  • 死锁是指两个或多个以上的进程在执行过程中,因争夺资源而造成一种互相等待的现象,若无外力干涉那他们都将无法推进下去。

形成死锁的四个必要条件是什么?

  • 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  • 请求与保持条件(占有且等待):一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

如何预防线程死锁?

  • 破坏互斥条件

    这个条件很多时候没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。当然也可以把某些独占设备在逻辑上改成共享设备。

  • 破坏请求与保持条件

    一次性申请所有的资源。

  • 破坏不剥夺条件

    方法一:占用部分资源的线程进一步申请其他资源时,如果在一段时间申请不到,可以主动释放它占有的资源

    方法二:采用剥夺式的调度算法

  • 破坏循环等待条件

    规定线程获取锁的顺序

  • 银行家算法

死锁的排查(重点)

  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁

  • 方法一:使用jps查出java相关的后台进程,再用 jstack 定位死锁

    1、使用 jps 定位进程 id:

    2、使用 jstack 定位死锁:


  • 方法二:使用 jconsole工具

1.4 独占锁(写锁)和共享锁(读锁)

独占锁:

  • 独占锁也叫排它锁,是指每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。ReentrantLock和Synchronized都是独占锁。

共享锁:

  • 共享锁则允许多个线程同时获取锁,并发访问共享资源。ReentrantReadWriteLock其读锁是共享锁,写锁是独占锁。

1.5 自旋锁(spinlock)

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁优缺点:

  • 自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!
  • 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;

1.6 无锁、偏向锁、轻量锁、重量锁

synchronized锁一共有4种状态,级别从低到高依次是:无锁状态 --> 偏向锁状态 --> 轻量级锁状态 --> 重量级锁状态,这几种状态会随着竞争情况逐渐升级。锁可以升级但不能降级。

获得锁的方式优点缺点适用场景
偏向锁在对象头和栈帧中的锁记录里面存储偏向的线程ID加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距如果线程存在锁竞争,会带来额外的锁撤销的消耗只有一个线程访问同步块
轻量级锁CAS操作成功更新对象Mark Word指向Lock Record的指针竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程,使用自旋会消耗CPU追求响应时间,同步块执行速度非常快
重量级锁获取到对象的monitor线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行时间长

二、Java内存模型

一篇文章搞懂java内存模型、JMM三大特征、volatile关键字

2.1 是什么JMM?

JMM 即 Java Memory Model (Java内存模型),因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。 它从Java层面定义了 主存、工作内存抽象概念,底层对应着CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

2.2 JMM下三大特性

  • 原子性 - 指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰
  • 可见性 - 可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了 ,保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

原子性:

  • 原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。

  • 分析下面代码的原子性

    int i = 2;
    int j = i;
    i++;
    

    第一句是基本类型赋值操作,必定是原子性操作。

    第二句先读取i的值,再赋值到j,两步操作,不能保证原子性。

    第三句,先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性。

  • JMM只能保证基本的原子性,如果要保证一个代码块的原子性,提供了monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。

可见性:

  • 可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。 当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。

  • 除了volatile关键字之外,final和synchronized也能实现可见性。

  • synchronized的原理先清空工作内存 → 在主内存中拷贝最新变量的副本到工作内存 → 执行完代码 → 将更改后的共享变量的值刷新到主内存中→ 释放互斥锁

有序性:

  • 为了提供性能,编译器和处理器通常会对指令序列进行重新排序

  • 指令重排可以保证单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。而多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的

  • 在Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别:

    • volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。

    • synchronized的原理是,一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包住的代码块在多线程之间是串行执行的。

2.3 八种内存交互操作

  • Java内存模型定义了八种内存交互操作

以下来自深入理解Java虚拟机第三版

  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则:

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有assign的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

2.4 先行发生原则happens-before

  • 从 JDK5 开始,java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。

  • 如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 管程锁定规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • 线程启动规则:这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。即:对象没有完成初始化之前,是不能调用finalized()方法的

三、volatile关键字

3.1 volatile特性

volatile关键字的特性?

Volatile是Java虚拟机提供的轻量级的同步机制,主要的作用:

  1. 保证线程间变量的可见性。

    volatile修饰的变量,当一个线程改变了该变量的值,会将共享变量值立即刷新回主内存。而线程读取共享变量必须从主内存中读取。

  2. 禁止CPU进行指令重排序。

  3. 不能保证操作的原子性

保证可见性:

  • volatile 修饰的变量,汇编指令中会存在于一个lock指令会将当前处理器缓存行的数据写回到系统内存,同时触发缓存一致性协议,使在其他CPU里缓存了该内存地址的数据无效。

参考文章:volatile是如何保证可见性和有序性的

禁止CPU进行指令重排:

  • volatile 的底层实现原理是内存屏障

  • 在每个volatile读操作后插入LoadLoad屏障,在读操作后插入LoadStore屏障

  • 在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个SotreLoad屏障

  • 即:

    • volatile写之前的操作,都禁止重排序到volatile之后
    • volatile读之后的操作,都禁止重排序到volatile之前

如何解决volatile不保证原子性问题?

  • 在方法上加入 synchronized,虽然能够保证原子性,但是为了解决number++,而引入重量级的同步机制

  • 如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger

3.2 内存屏障

  • 内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
  • 内存屏障其实就是一种JVM指令,对于操作volatile变量,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。

内存屏障可分为四类:

  • LoadLoad 屏障:对于这样的语句Load1,LoadLoad,Load2。在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:对于这样的语句Store1, StoreStore, Store2。在Store2及后续写入操作执行前,保证Store1的写入操作以刷新到主内存。
  • LoadStore 屏障:对于这样的语句Load1, LoadStore,Store2。在Store2及后续写入操作被刷出前,保证Load1要读操作已读取完毕。
  • StoreLoad 屏障:对于这样的语句Store1, StoreLoad,Load2。在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

内存屏障的源码:

volatile变量规则:

  • 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
  • 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。

volatile和内存屏障的关联

  • volatile写

    • 在每个 volatile 写操作的 前⾯ 插⼊⼀个 StoreStore 屏障
    • 在每个 volatile 写操作的 后⾯ 插⼊⼀个 StoreLoad 屏障

  • volatile写

    • 每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
    • 每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

3.3 DCL双端锁

double-checked locking(双重检查锁) 单例模式

  • 分析不使用volatile的问题:

    public class Singleton {
    
        private static Singleton instance = null;
    
        private Singleton(){ }
    
        public static Singleton getInstance() {
            if(instance == null) {
                synchronized(Singleton.class) {
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    
    }
    

    代码问题分析:

    **instance = new Singleton();**可以分解成三行伪代码

    //1:分配对象的内存空间
    memory = allocate();
    //2:初始化对象
    ctorInstance(memory);  
    //3:设置instance指向刚分配的内存地址
    instance = memory;     
    

    但上面三行伪代码中的2和3之间,可能会被重排序

    //1:分配对象的内存空间
    memory = allocate(); 
    //3:设置instance指向刚分配的内存地址,注意:此时对象还没有被初始化
    instance = memory;
    //2:初始化对象
    ctorInstance(memory);
    

    如果线程A由于指令重排,在设置instance指向刚分配的内存地址但还未初始化对象时,此时instance已经不为null了,正好线程B调用该方法,将会获得一个未初始化完毕的单例

  • 正确的DCL单例模式代码

    public class Singleton {
    
        //如果不使用volatile,则instance = new Singleton();中指令重排序可能导致程序出问题
        private static volatile Singleton instance = null;
    
        private Singleton(){ }
    
        public static Singleton getInstance() {
            if(instance == null) {
                synchronized(Singleton.class) {
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    
    }
    

静态内部类实现单例:

public class SingletonDemo {
    private SingletonDemo() { }

    private static class SingletonDemoHandler {
        private static SingletonDemo instance = new SingletonDemo();
    }

    public static SingletonDemo getInstance() {
        return SingletonDemoHandler.instance;
    }
}
  • 类加载本身就是懒惰的,在没有调用getInstance方法时是没有执行SingletonDemoHandler内部类的类加载操作的。静态内部类不会随着外部类的加载而加载, 这是静态内部类和静态变量的区别

  • 同时也不会有并发问题,因为是通过类加载创建的单例, JVM保证不会出现线程安全

四、CAS

4.1 CAS与volatile

乐观锁与悲观锁的概念:

  • 悲观锁:

    悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

  • 乐观锁:

    乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。 乐观锁的一种实现方式 CAS 实现的 。

CAS与volatile:

  • compare and swap的缩写,中文翻译成:比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。某线程执行CAS操作的时候,将内存位置的值与预期原值比较:

    • 如果相匹配,那么处理器会自动将该位置值更新为新值
    • 如果不匹配,那么重新获取该内存位置的值,然后线程进行自旋,到下次循环才有机会执行。

  • CAS需要与volatile结合使用,因为获取共享变量时,需要保证该变量的可见性

CAS的缺点:

  • 如果竞争激烈,则会导致循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)

  • 只能保证一个共享变量的原子操作

    • 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
    • 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • ABA 问题

注意:

  • CAS是原子性操作,它是由硬件进行的比较-更新的原子性。

  • CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的 原子性。

  • 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

如何解决ABA问题?

  • ABA问题介绍:一个线程1从内存中取出A,这个时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,线程1此时还被阻塞,线程2又进行了一些操作,然后将B又变成了A,此时线程1获得资源,开始执行,但是在进行cas操作的时候发现内存中还是A,然后线程1执行成功。(说白了就是可能存在一个线程根本不知道数值发生了变化)
  • 使用AtomicStampedReference(加版本号解决ABA问题)

4.2 自旋锁(spinlock)

  • 是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
  • 当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

自己实现一个自旋锁:

public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        Thread thread = Thread.currentThread();
        //把atomicReference中的null修改成thread
        while(!atomicReference.compareAndSet(null,thread)) {
            //上锁成功就结束循环
        }
    }

    public void myUnLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null); //解锁,将atomicReference中的当前线程对象改成null
    }
}

4.3 Unsafe类

  • 是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

  • 可以看到AtomicInteger底层调用的就是Unsafe类中的compareAndSwapInt()方法

五、原子操作类

5.1 基本类型原子类

  • AtomicBoolean:以原子更新的方式更新boolean;
  • AtomicInteger:以原子更新的方式更新Integer;
  • AtomicLong:以原子更新的方式更新Long;

常用的API:(以AtomicInteger为例)

  • addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;
  • incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
  • getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;
  • getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;
  • compareAndSet(int expect, int update) :如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

代码示例:

class MyNumber{
    @Getter
    private AtomicInteger atomicInteger = new AtomicInteger();
    public void addPlusPlus(){
        atomicInteger.incrementAndGet();
    }
}
  
public class AtomicIntegerDemo{
    public static void main(String[] args) throws InterruptedException{
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(100);

        for (int i = 1; i <=100; i++) {
            new Thread(() -> {
                try{
                    for (int j = 1; j <=5000; j++){
                        myNumber.addPlusPlus();
                    }
                }finally {
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(myNumber.getAtomiInteger().get());
    }
}

5.2 数组类型原子类

  • AtomicIntegerArray:原子更新整型数组中的元素;
  • AtomicLongArray:原子更新长整型数组中的元素;
  • AtomicReferenceArray:原子更新引用类型数组中的元素

常用API:(以AtomicIntegerArray)

  • addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;
  • getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
  • compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新

代码示例:

public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});

        int tmpInt = 0;

        tmpInt = atomicIntegerArray.getAndSet(0,1122);
        System.out.println(tmpInt+"\\t"+atomicIntegerArray.get(0)); // 0	1122
        atomicIntegerArray.getAndIncrement(1);
        atomicIntegerArray.getAndIncrement(1);
        tmpInt = atomicIntegerArray.getAndIncrement(1);
        System.out.println(tmpInt+"\\t"+atomicIntegerArray.get(1)); // 2	3
    }
}

5.3 引用类型原子类

  • AtomicReference:原子更新引用类型
  • AtomicStampedReference:携带版本号的引用类型原子类(可以解决ABA问题)
  • AtomicMarkableReference:原子更新带有标记位的引用类型(用来解决变量是否被修改)

代码示例:(加版本号解决ABA问题)

public class Test1 {
    //指定版本号
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) {
        new Thread(() -> {
            String pre = ref.getReference();
            //获得版本号
            int stamp = ref.getStamp(); // 此时的版本号还是第一次获取的
            try {
                other();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //把ref中的A改为C,并比对版本号,如果版本号相同,就执行替换,并让版本号+1
            System.out.println("change A->C stamp " + stamp + " " + ref.compareAndSet(pre, "C", stamp, stamp + 1));
        }).start();
    }

    static void other() throws InterruptedException {
        new Thread(() -> {
            int stamp = ref.getStamp(

以上是关于Java并发编程——锁相关Java内存模型volatileCAS原子操作类线程池的主要内容,如果未能解决你的问题,请参考以下文章

java并发编程:管程内存模型无锁并发线程池AQS原理与锁线程安全集合类并发设计模式

java并发编程:管程内存模型无锁并发线程池AQS原理与锁线程安全集合类并发设计模式

并发编程-JMM&Lock锁以及原理

Java并发编程Java内存模型

并发编程面试Java内存模型相关问题

互联网架构多线程并发编程高级教程(下)