JUC 并发编程

本文介绍了Java多线程与并发编程的核心概念。首先阐述了进程作为资源分配单位、线程作为执行单位的基本概念,以及并发与并行的区别。详细讲解了Java中创建线程的三种方式(Thread、Runnable、FutureTask)和线程状态管理。重点解析了synchronized关键字的实现原理,包括Monitor管程机制、锁升级过程(无锁→偏向锁→轻量级锁→重量级锁)、锁自旋和锁消除等优化策略。介绍了volatile关键字通过内存屏障保证可见性和有序性的原理,以及wait-notify机制。阐述了CAS无锁编程思想,对比了乐观锁与悲观锁的特点。最后介绍了Java.util.concurrent.atomic包中的原子类,包括基本原子类、原子引用、原子数组、原子更新器和原子累加器等,展示了如何利用CAS实现高效的线程安全操作。

1.概念:

1.1进程与线程

进程:
一个程序(磁盘上的静态文件) 要运行就要加载对应的资源到内存随后运行指令,这时就开启了一个进程。
进程可以视为程序的一个实例,(一个程序可以有多个进程实例)

在Windows中,进程是不活动的,仅仅是线程的容器。

线程:
一个线程就是一个指令流,一个执行的通道。(一个进程可以有1到多个线程)

在java中线程作为最小调度单位,而进程作为资源分配的最小单位。
管理内存,管理IO资源是以进程为单位的;执行指令是以线程为单位的。
进程的上下文切换成本要比线程要高。

通信方式

同一台计算机可以使用IPC(inter-process communication)通信。不同的计算机需要通过网络及上层协议通信。对比进程,线程之间共享内存等资源,通信会更加方便。

1.2并发并行

分时共用:
操作系统中的时间调度器组件将CPU的时间片(windwos下最小15ms)分给不同线程使用。

在同一时间,一个CPU核心轮流处理多个线程的任务,称为并发执行。
在同一时间,多个CPU核心同时处理多个线程的任务,称为并发并行。
多核CPU通常是并发并行同时存在的。

并发 concurrent:
<img src="https://www.reboots.top/api/file/69cf17debf8448c3b3324b912aae3875_0.7.jpg" alt="image-20240806221021880" style="height:300px" />
并行 parallel:
<img src="https://www.reboots.top/api/file/69d53409cd264c17988166a329debe0c_0.7.jpg" alt="image-20240806220937567" style="height:300px" />

1.3同步异步

在调用上讲
同步 async:需要等待方法返回结果,才能继续执行。
异步 sync:不需要等待方法返回结果,就能继续执行。

在单核CPU的情况下,多线程反而要比单线程要慢(多了上下文切换消耗),但不代表多线程在单核CPU下没有意义,在一些情况需要避免阻塞,多线程也是不可缺少的。

IO操作不会占用CPU,但是拷贝文件(阻塞式IO)需要CPU进行等待,没有办法充分利用CPU。(可以通过非阻塞IO和异步IO进行优化)

Java中的多线程实现方式

  1. Thread

    Thread thread = new Thread() {
        @Override
        public void run() {
            log.info("running");
        }
    };
    thread.start();
    
    log.info("running");
    
  2. Runnable (更灵活,更容易和线程池配合使用)

    Runnable runnable = new Runnable() {
        public void run() {
            log.info("Hello World");
        }
    };
    
    Thread thread = new Thread(runnable) ;
    thread.start();
    
    log.info("running");
    
  3. FutureTask + Callable (可以拥有返回结果)

FutureTask<Integer> futureTask = new FutureTask<>(() -> {
    log.info("running");
    Thread.sleep(1000);
    return 100;
});

new Thread(futureTask).start();
log.info("finished");

Integer i = futureTask.get();
System.out.println(i);
log.info("done");

2.进程管理

Windows:

  • 使用任务管理器;
  • 使用tasklist 查看所有进程
  • taskkill命令 taskkill /F /PID &lt;pid>

Linux:

  • ps -fe 查看所有进程;
  • ps -fT -p &lt;pid> 查看PID对应的进程信息;
  • kill 结束进程;
  • top 按大写H显示线程。

Java

  • jps 查看所有java进程
  • jconsole 图形化监控
  • jstack &lt;pid> 查看某个java进程的所有线程状态

java程序允许Jconsole进行远程链接 启动参数:

-Djava.rmi.server.hostname=192.168.1.1
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=20001
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

3.栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

Jvm虚拟机由堆、栈、方法区组成,其中的栈内存是分配给线程使用的,而栈由多个栈帧组成,且只有一个活动的栈帧,即对应的活动方法。

<img src="https://www.reboots.top/api/file/b0514d1d49d74d3d877db77db1384cdc_0.7.jpg" alt="image-20250809140240371" style="zoom:50%;" />

4.线程

4.1上下文切换

以下情况会导致CPU不再执行当前线程(context switch 上下文切换),转而执行另一个线程的代码:

  • 线程时间片耗尽
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等 方法

在 context switch 发生时,需要保存当前的线程状态(程序计数器、以及每个栈帧的信息如局部变量、操作数栈、返回地址等)

频繁的上下文切换会影响性能。(线程数不是越多越好)

4.2Thread 类 API

方法 说明
public void start() 启动一个新线程,Java虚拟机调用此线程的 run 方法
public void run() 线程启动后调用该方法
public void setName(String name) 给当前线程取名字
public void getName() 获取当前线程的名字 线程存在默认名称:子线程是 Thread-索引,主线程是 main
public static Thread currentThread() 获取当前线程对象,代码在哪个线程中执行
public static void sleep(long time) 让当前线程休眠多少毫秒再继续执行 Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争
public static native void yield() 提示线程调度器让出当前线程对 CPU 的使用
public final int getPriority() 返回此线程的优先级
public final void setPriority(int priority) 更改此线程的优先级,常用 1 5 10
public void interrupt() 中断这个线程,异常处理机制
public static boolean interrupted() 判断当前线程是否被打断,清除打断标记
public boolean isInterrupted() 判断当前线程是否被打断,不清除打断标记
public final void join() 等待这个线程结束
public final void join(long millis) 等待这个线程死亡 millis 毫秒,0 意味着永远等待
public final native boolean isAlive() 线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on) 将此线程标记为守护线程或用户线程

4.3 线程的状态

操作系统层面讲可以分为五种:
<img src="https://www.reboots.top/api/file/ebf446cb1cdc4c36834a6c836f19c712_0.7.jpg" alt="image-20250809142600760" style="zoom:50%;" />

在Java层面中,Thread.state 标记了六种状态:

<img src="https://www.reboots.top/api/file/c761203750b64bee9f65579ebaefab89_0.7.jpg" alt="image-20250809153824139" style="zoom:33%;" />

5.Synchonized

Synchonized实际上是使用对象锁,来保证了临界区代码的原子性

临界区

临界资源:一次仅允许一个进程使用的资源成为临界资源

临界区:访问临界资源的代码块

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

一个程序运行多个线程是没有问题,多个线程读共享资源也没有问题,在多个线程对共享资源读写操作时发生指令交错,就会出现问题

为了避免临界区的竞态条件发生(解决线程安全问题):

  • 阻塞式的解决方案:synchronized,lock
  • 非阻塞式的解决方案:原子变量

monitor(管程)

由局部于自己的若干公共变量和所有访问这些公共变量的过程所组成的软件模块,保证同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)

synchronized:对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

互斥和同步都可以采用 synchronized 关键字来完成,区别:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

性能:

  • 线程安全,性能差
  • 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类

锁升级

image-20250809202941418

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级

轻量级锁

一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见)

可重入锁:线程可以进入任何一个它已经拥有的锁所同步着的代码块,可重入锁最大的作用是避免死锁

轻量级锁在没有竞争时(锁重入时),每次重入仍然需要执行 CAS 操作,Java 6 才引入的偏向锁来优化

锁重入实例:

static final Object obj = new Object();
public static void method1() {
    synchronized( obj ) {
        // 同步块 A
        method2();
    }
}
public static void method2() {
    synchronized( obj ) {
        // 同步块 B
    }
}
  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,存储锁定对象的 Mark Word让锁记录中 Object reference 指向锁住的对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
    <img src="https://www.reboots.top/api/file/c3358eab80334cd2b8abc89af33671de_0.7.jpg" alt="image-20250809203351535" style="zoom:50%;" />

  • 如果 CAS 替换成功,对象头中存储了锁记录地址和状态 00(轻量级锁) ,表示由该线程给对象加锁、

    <img src="https://www.reboots.top/api/file/321d06a7448a4d55922b3d1ab242e083_0.7.jpg" alt="image-20250809203426374" style="zoom:50%;" />

  • 如果 CAS 失败,有两种情况:

  • 当退出 synchronized 代码块(解锁时)

    • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1

    • 如果锁记录的值不为 null,这时使用 CAS

      将 Mark Word 的值恢复给对象头

      • 成功,则解锁成功
      • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

    <img src="https://www.reboots.top/api/file/ef2dc1a504334d3296d86f192d8ae82a_0.7.jpg" alt="image-20250809203512052" style="zoom:50%;" />

  • Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

    image-20250809203525431

  • 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

锁自旋

重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁

注意:

  • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
  • 自旋失败的线程会进入阻塞状态

优点:不会进入阻塞状态,减少线程上下文切换的消耗

缺点:当自旋的线程越来越多时,会不断的消耗 CPU 资源

锁消除

JIT(即时编译器)会对热点代码进行优化,将没有意义的加锁操作直接清除,即锁清除。

锁粗化:对同一个锁对象的多个加锁解锁代码块,优化为一个大的代码块,统一加解锁。

wait-notify

需要获取对象锁后才可以调用 锁对象.wait(),notify 随机唤醒一个线程,notifyAll 唤醒所有线程去竞争 CPU

Object 类 API:

public final void notify():唤醒正在等待对象监视器的单个线程。
public final void notifyAll():唤醒正在等待对象监视器的所有线程。
public final void wait():导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll()方法。
public final native void wait(long timeout):有时限的等待, 到n毫秒后结束等待,或是被唤醒

说明:wait 是挂起线程,需要唤醒的都是挂起操作,阻塞线程可以自己去争抢锁,挂起的线程需要唤醒后去争抢锁

对比 sleep():

  • 原理不同:sleep() 方法是属于 Thread 类,是线程用来控制自身流程的,使此线程暂停执行一段时间而把执行机会让给其他线程;wait() 方法属于 Object 类,用于线程间通信
  • 锁的处理机制不同:调用 sleep() 方法的过程中,线程不会释放对象锁,当调用 wait() 方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池(不释放锁其他线程怎么抢占到锁执行唤醒操作),但是都会释放 CPU
  • 使用区域不同:wait() 方法必须放在**同步控制方法和同步代码块(先获取锁)**中使用,sleep() 方法则可以放在任何地方使用

底层原理:

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,唤醒后并不意味者立刻获得锁,需要进入 EntryList 重新竞争

6.volatile

volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性
  • 不保证原子性
  • 保证有序性(禁止指令重排)

性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小

synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性

  • 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
  • 线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中(JMM 内存交互章节有讲)

缓存一致

使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定,在线程修改完共享变量后写回主存,其他的 CPU 核心上运行的线程通过 CPU 总线嗅探机制会修改其共享变量为失效状态,读取时会重新从主内存中读取最新的数据

lock 前缀指令就相当于内存屏障,Memory Barrier(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

内存屏障有三个作用:

  • 确保对内存的读-改-写操作原子执行
  • 阻止屏障两侧的指令重排序
  • 强制把缓存中的脏数据写回主内存,让缓存行中相应的数据失效

内存屏障

保证可见性

  • 写屏障(sfence,Store Barrier)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    public void actor2(I_Result r) {
        num = 2;
        ready = true; // ready 是 volatile 赋值带写屏障
        // 写屏障
    }
    
  • 读屏障(lfence,Load Barrier)保证在该屏障之后的,对共享变量的读取,从主存刷新变量值,加载的是主存中最新数据

    public void actor1(I_Result r) {
        // 读屏障
        // ready 是 volatile 读取值带读屏障
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    

    img

  • 全能屏障:mfence(modify/mix Barrier),兼具 sfence 和 lfence 的功能

保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到写屏障之前

  • 有序性的保证也只是保证了本线程内相关代码不被重排序

    volatile i = 0;
    new Thread(() -> {i++});
    new Thread(() -> {i--});
    

    i++ 反编译后的指令:

    0: iconst_1			// 当int取值 -1~5 时,JVM采用iconst指令将常量压入栈中
    1: istore_1			// 将操作数栈顶数据弹出,存入局部变量表的 slot 1
    2: iinc		1, 1	
    

    img


交互规则

对于 volatile 修饰的变量:

  • 线程对变量的 use 与 load、read 操作是相关联的,所以变量使用前必须先从主存加载
  • 线程对变量的 assign 与 store、write 操作是相关联的,所以变量使用后必须同步至主存
  • 线程 1 和线程 2 谁先对变量执行 read 操作,就会先进行 write 操作,防止指令重排

Double-Checked Locking

DCL(双端检锁)机制不一定是线程安全的,原因是有指令重排的存在,加入 volatile 可以禁止指令重排

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2,这里的判断不是线程安全的
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                // 这里是线程安全的判断,防止其他线程在当前线程等待锁的期间完成了初始化
                if (INSTANCE == null) { 
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

不锁 INSTANCE 的原因:

  • INSTANCE 要重新赋值
  • INSTANCE 是 null,线程加锁之前需要获取对象的引用,设置对象头,null 没有引用

实现特点:

  • 懒惰初始化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 第一个 if 使用了 INSTANCE 变量,是在同步块之外,但在多线程环境下会产生问题

7.无锁

CAS

无锁编程:Lock Free

CAS 的全称是 Compare-And-Swap,是 CPU 并发原语

  • CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法,调用 UnSafe 类中的 CAS 方法,JVM 会实现出 CAS 汇编指令,这是一种完全依赖于硬件的功能,实现了原子操作
  • CAS 是一种系统原语,原语属于操作系统范畴,是由若干条指令组成 ,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许被中断,所以 CAS 是一条 CPU 的原子指令,不会造成数据不一致的问题,是线程安全的

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

  • 程序是在单核处理器上运行,会省略 lock 前缀,单处理器自身会维护处理器内的顺序一致性,不需要 lock 前缀的内存屏障效果
  • 程序是在多核处理器上运行,会为 cmpxchg 指令加上 lock 前缀。当某个核执行到带 lock 的指令时,CPU 会执行总线锁定或缓存锁定,将修改的变量写入到主存,这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的原子性

作用:比较当前工作内存中的值和主物理内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存的值一致为止

CAS 特点:

  • CAS 体现的是无锁并发、无阻塞并发,线程不会陷入阻塞,线程不需要频繁切换状态(上下文切换,系统调用)
  • CAS 是基于乐观锁的思想

CAS 缺点:

  • 执行的是循环操作,如果比较不成功一直在循环,最差的情况某个线程一直取到的值和预期值都不一样,就会无限循环导致饥饿,使用 CAS 线程数不要超过 CPU 的核心数,采用分段 CAS 和自动迁移机制
  • 只能保证一个共享变量的原子操作
    • 对于一个共享变量执行操作时,可以通过循环 CAS 的方式来保证原子操作
    • 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 引出来 ABA 问题

乐观锁

CAS 与 synchronized 总结:

  • synchronized 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程),因此 synchronized 也称之为悲观锁,ReentrantLock 也是一种悲观锁,性能较差
  • CAS 是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。如果别人修改过,则获取现在最新的值,如果别人没修改过,直接修改共享数据的值,CAS 这种机制也称之为乐观锁,综合性能较好

8.Atomic

常见原子类:AtomicInteger、AtomicBoolean、AtomicLong

构造方法:

  • public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer

常用API:

方法 作用
public final int get() 获取 AtomicInteger 的值
public final int getAndIncrement() 以原子方式将当前值加 1,返回的是自增前的值
public final int incrementAndGet() 以原子方式将当前值加 1,返回的是自增后的值
public final int getAndSet(int value) 以原子方式设置为 newValue 的值,返回旧值
public final int addAndGet(int data) 以原子方式将输入的数值与实例中的值相加并返回 实例:AtomicInteger 里的 value

原理分析

AtomicInteger 原理:自旋锁 + CAS 算法

CAS 算法:有 3 个操作数(内存值 V, 旧的预期值 A,要修改的值 B)

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋

分析 getAndSet 方法:

  • AtomicInteger:

    public final int getAndSet(int newValue) {
        /**
        * this: 		当前对象
        * valueOffset:	内存偏移量,内存地址
        */
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    

    valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

    valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
    //调用本地方法   -->
    public native long objectFieldOffset(Field var1);
    
  • unsafe 类:

    // val1: AtomicInteger对象本身,var2: 该对象值得引用地址,var4: 需要变动的数
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // var5: 用 var1 和 var2 找到的内存中的真实值
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    
        return var5;
    }
    

    var5:从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据

  • 变量 value 用 volatile 修饰,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量

    private volatile int value
    

    CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果

分析 getAndUpdate 方法:

  • getAndUpdate:

    public final int getAndUpdate(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();	//当前值,cas的期望值
            next = updateFunction.applyAsInt(prev);//期望值更新到该值
        } while (!compareAndSet(prev, next));//自旋
        return prev;
    }
    

    函数式接口:可以自定义操作逻辑

    AtomicInteger a = new AtomicInteger();
    a.getAndUpdate(i -> i + 10);
    
  • compareAndSet:

    public final boolean compareAndSet(int expect, int update) {
        /**
        * this: 		当前对象
        * valueOffset:	内存偏移量,内存地址
        * expect:		期望的值
        * update: 		更新的值
        */
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

原子引用

原子引用:对 Object 进行原子操作,提供一种读和写都是原子性的对象引用变量

原子引用类:AtomicReference、AtomicStampedReference、AtomicMarkableReference

AtomicReference 类:

  • 构造方法:AtomicReference<T> atomicReference = new AtomicReference<T>()
  • 常用 API:
    • public final boolean compareAndSet(V expectedValue, V newValue):CAS 操作
    • public final void set(V newValue):将值设置为 newValue
    • public final V get():返回当前值
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        Student s1 = new Student(33, "z3");
        
        // 创建原子引用包装类
        AtomicReference<Student> atomicReference = new AtomicReference<>();
        // 设置主内存共享变量为s1
        atomicReference.set(s1);

        // 比较并交换,如果现在主物理内存的值为 z3,那么交换成 l4
        while (true) {
            Student s2 = new Student(44, "l4");
            if (atomicReference.compareAndSet(s1, s2)) {
                break;
            }
        }
        System.out.println(atomicReference.get());
    }
}

class Student {
    private int id;
    private String name;
    //。。。。
}

原子数组

原子数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

AtomicIntegerArray 类方法:

/**
*   i		the index
* expect 	the expected value
* update 	the new value
*/
public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

原子更新器

原子更新器类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常 IllegalArgumentException: Must be volatile type

常用 API:

  • static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> c, String fieldName):构造方法
  • abstract boolean compareAndSet(T obj, int expect, int update):CAS
public class UpdateDemo {
    private volatile int field;
    
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater
                    .newUpdater(UpdateDemo.class, "field");
        UpdateDemo updateDemo = new UpdateDemo();
        fieldUpdater.compareAndSet(updateDemo, 0, 10);
        System.out.println(updateDemo.field);//10
    }
}

原子累加器

原子累加器类:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator

LongAdder 和 LongAccumulator 区别:

相同点:

  • LongAddr 与 LongAccumulator 类都是使用非阻塞算法 CAS 实现的
  • LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr

不同点:

  • 调用 casBase 时,LongAccumulator 使用 function.applyAsLong(b = base, x) 来计算,LongAddr 使用 casBase(b = base, b + x)
  • LongAccumulator 类功能更加强大,构造方法参数中
    • accumulatorFunction 是一个双目运算器接口,可以指定累加规则,比如累加或者相乘,其根据输入的两个参数返回一个计算值,LongAdder 内置累加规则
    • identity 则是 LongAccumulator 累加器的初始值,LongAccumulator 可以为累加器提供非0的初始值,而 LongAdder 只能提供默认的 0