java并发编程:管程内存模型无锁并发线程池AQS原理与锁线程安全集合类并发设计模式
Posted Henrik-Yao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java并发编程:管程内存模型无锁并发线程池AQS原理与锁线程安全集合类并发设计模式相关的知识,希望对你有一定的参考价值。
文章目录
基础
1.进程与线程
进程是程序的实例,包含指令和数据,拥有共享资源,线程是指令流,是进程的子集,线程比进程轻量,上下文切换成本较低
通信
进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
2.并发与并行
并发:微观上串行执行时间片,宏观上并行执行了多任务
并行:同时做多件事
多核 cpu 可以并行跑多个线程,但能否提高程序运行效率要分情况,有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分,也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
3.同步与异步
同步:需要等待结果返回,才能继续运行
异步:不需要等待结果返回,就能继续运行
多线程可以让方法执行变为异步的,在项目中,视频文件需要转换格式等操作比较费时,这时可以开一个新线程处理视频转换,避免阻塞主线程
4.主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束
5.Thread 与 Runnable
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1")
@Override
// run 方法内实现了要执行的任务
public void run()
log.debug("hello");
;
t1.start();
创建线程的方式2-使用 Runnable 配合 Thread
// 创建任务对象
Runnable task2 = new Runnable()
@Override
public void run()
log.debug("hello");
;
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
6.线程方法
方法名 | 功能 | 注意 |
---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 |
join() | 等待线程运行结 | |
束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | |
getId() | 获取线程长整型的 id | id 唯一 |
getName() | 获取线程名 | |
setName(String) | 修改线程名 | 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用 |
getPriority() | 获取线程优先级 | |
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED |
isInterrupted() | 判断是否被打断 | 不会清除打断标记 |
isAlive() | 线程是否存活(还没有运行完毕) | |
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join,会导致被打断的线程抛出 InterruptedException,并清除打断标记 ,如果打断的正在运行的线程,则会设置打断标记 ,park 的线程被打断,也会设置打断标记 |
interrupted() | 判断当前线程是否被打断 | static方法,会清除打断标记 |
currentThread() | 获取当前正在执行的线程 | static方法 |
sleep(long n) | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | static方法,调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态 |
yield() | 提示线程调度器让出当前线程对CPU的使用 | static方法,主要是为了测试和调试, 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态 |
7.线程状态
操作系统层面五种状态
- 初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态(就绪状态):指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 运行状态:指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从运行状态转换至可运行状态,会导致线程的上下文切换
- 阻塞状态:如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态,等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
Java Thread.State 六种状态
- NEW:线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的可运行状态、运行状态和阻塞状态(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED:WAITING , TIMED_WAITING 都是 Java API 层面对阻塞状态的细分
- TERMINATED:当线程代码运行结束
管程
管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发
1.共享问题、临界区、竞态条件
在多个线程对共享资源读写操作时发生指令交错,会出现共享问题,一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区(Critical Section),多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件( Race Condition)
2.Monitor
Monitor 被翻译为监视器或管程,每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
java 对象头结构
其中 Mark Word 结构为
Monitor 结构
- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程
3.synchronized
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化,轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
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 的值存入锁记录
-
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
-
如果 cas 失败,有两种情况:如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程,如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
-
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,成功,则解锁成功,失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程:即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能
- Java 7 之后不能控制是否开启自旋功能
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
偏向锁特征:
- 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID,当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
- 当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁消除
逃逸分析的优化之一
锁粗化
对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化
4.wait & notify
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
sleep(long n) 和 wait(long n) 的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- 它们状态都是 TIMED_WAITING
虚假唤醒
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为虚假唤醒,解决方法,改为 notifyAll
用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了,解决方法,用 while + wait,当条件不成立,再次 wait
5.Park & Unpark
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以线程为单位来阻塞和唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
6.活跃性
死锁
当锁资源循环依赖时出现死锁,一个线程需要同时获取多把锁,这时就容易发生死锁
死锁发生的四个必要条件:
- 互斥条件:同一时间只能有一个线程获取资源
- 不可剥夺条件:一个线程已经占有的资源,在释放之前不会被其它线程抢占
- 请求和保持条件:线程等待过程中不会释放已占有的资源
- 循环等待条件:多个线程互相等待对方释放资源
死锁预防,那么就是需要破坏这四个必要条件
- 由于资源互斥是资源使用的固有特性,无法改变,不讨论
- 破坏不可剥夺条件
一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行 - 破坏请求与保持条件
第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源
第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源 - 破坏循环等待条件
采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试
饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
饥饿分为两种情况:
- 一种是其他的线程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态
- 另一种是因为线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行
解决饥饿的问题有几种方案
- 保证资源充足,很多场景下,资源的稀缺性无法解决
- 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源
- 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短
7.ReentrantLock
详细原理在后文
与 synchronized 一样,都支持可重入(可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住)
// 获取锁
reentrantLock.lock();
try
// 临界区
finally
// 释放锁
reentrantLock.unlock();
ReentrantLock 支持可打断、锁超时、公平锁、条件变量
条件变量
synchronized 中也有条件变量,即 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
条件变量通过 await() 进入,条件变量内的阻塞线程通过 signal() 唤醒
static ReentrantLock lock = new ReentrantLock();
static Condition waitbreakfastQueue = lock.newCondition();
waitbreakfastQueue.await();
waitbreakfastQueue.signal();
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
8.lock vs synchronized
不同点
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
内存模型
JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障
1.线程切换带来的原子性
线程切换会造成指令交错
解决办法:使用 synchronized 关键字上锁用互斥方式保证原子性
2.缓存导致的可见性
当 t 线程频繁从主存中读取热点数据时,JIT 编译器会将热点数据缓存至高速缓存,减少对主存的访问,提高效率
但是当其他线程改变了主存中的值时,t 线程只能读到旧值,也就造成了可见性的问题
解决方法:使用 volatile 关键字修饰成员变量和静态成员变量,可以强制线程必须从主存中读取值
内存屏障
- 可见性
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据 - 有序性
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
3.编译优化带来的有序性
编译优化会造成指令重排
原本的 new 逻辑:
- 分配一块内存 M
- 在内存 M 上初始化 Singleton 对象
- 然后 M 的地址赋值给 instance 变量
实际上优化后的执行路径却是这样的:
- 分配一块内存 M
- 将 M 的地址赋值给 instance 变量
- 最后在内存 M 上初始化 Singleton 对象
解决方法:使用 volatile 关键字禁止指令重排
Happens-Before
Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
JMM 的设计分为两部分,一部分是面向程序员提供,也就是happens-before规则,通俗易懂的向程序员阐述了一个强内存模型,只要理解 happens-before规则,就可以编写并发安全的程序了。 另一部分是针对 JVM 实现的,为了尽可能少的对编译器和处理器做约束,从而提高性能,JMM 在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。 程序员只需要关注前者就好了,也就是理解 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
- 线程启动规则:Thread 对象的 start() 方法,happens-before 于这个线程的任意后续操作
- 线程终止规则:线程中的任意操作,happens-before 于该线程的终止监测
- 线程中断操作:对线程 interrupt() 方法的调用,happens-before 于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成,happens-before 于这个对象的 finalize() 方法的开始
无锁并发
1.CAS 与 volatile
public void withdraw(Integer amount)
while(true)
// 需要不断尝试,直到成功为止
while (true)
// 比如拿到了旧值 1000
int prev = balance.get();
// 在这个基础上 1000-10 = 990
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
- 不一致了,next 作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了 990
那么本线程的这次 990 就作废了,进入 while 下次循环重试
- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next))
break;
以上例子实现无锁并发的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下,因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一,但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
乐观锁与悲观锁
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,不断重试
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,上了锁都改不了,解开锁其他线程才有机会
2.原子整数
juc 包提供了原子整数类型:AtomicBoolean、AtomicInteger、AtomicLong
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
System.out.println(i.decrementAndGet());
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
System.out.println(i.getAndDecrement());
// 获取并加值(i = 0, 结果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值并获取(i = 5, 结果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.updateAndGet(p -> p + 2));
// 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
// 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
// 其中函数中的操作能保证原子,但函数需要无副作用
System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));
3.原子引用
因为要保护的共享数据并不是只有基本类型,其他类型的保护需要原子引用,如下使用
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
juc 包提供了原子引用类型:AtomicReference、AtomicMarkableReference、AtomicStampedReference
用 AtomicStampedReference 中的版本号可以解决 ABA 问题
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException
log.debug("main start...");
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
log.debug("版本 ", stamp);
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C ", ref.compareAndSet(prev, "C", stamp, stamp + 1));
private static void other()
new Thread(() ->
log.debug("change A->B ", ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 ", ref.getStamp());
, "t1").start();
sleep(0.5);
new Thread(() ->
log.debug("change B->A ", ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 ", ref.getStamp());
, "t2").start();
4.原子数组
原子引用只能对对象引用保护,不能保护对象里面的内容,比如数组,这时候可以用原子数组,如下使用
new AtomicIntegerArray(10)
juc 包提供了原子数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
5.字段更新器
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常,如下使用
private volatile int field;
AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Test5以上是关于java并发编程:管程内存模型无锁并发线程池AQS原理与锁线程安全集合类并发设计模式的主要内容,如果未能解决你的问题,请参考以下文章