【JUC2022】第四章 原子类
阅读本文需要先具备JMM 基础知识
一、CAS
在多线程高并发编程的时候,最关键的问题就是保证临界区的对象的安全访问。通常是用加锁来处理,其实加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。而且线程的数量是有限的,依赖于操作系统,而且线程的创建和销毁带来的性能损耗不可忽视
对于并发控制而言,锁是一种悲观策略,会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问不会发生冲突,既然没有冲突,就不需要等待,线程也不会阻塞。但实际上是会发生冲突的,那该怎么办呢?无锁的策略采用一种比较交换技术 CAS(Compare And Swap)来鉴别线程冲突,一旦检测到冲突,就重试当前操作,直到没有冲突
与锁相比,CAS 会使得程序设计比较复杂,但是由于其优越的性能优势、天生免疫死锁,没有线程竞争开销以及线程间频繁调度开销,所以在目前被广泛应用
一个 CAS 方法包含三个参数 CAS(V, E, N),V 表示要更新的变量,E 表示预期的值,N 表示新值。只有当 V 的值等于 E 时,才会将 V 的值修改为 N。如果 V 的值不等于 E,说明已经被其它线程修改了,当前线程可以放弃此操作,也可以再次尝试操作直至修改成功。基于这样的算法,CAS 操作即使没有锁,也可以发现其它线程对当前线程的干扰(可以通过 volatile 保证可见性),并进行恰当的处理
CAS 并发原语体现在 Java 中就是 sun.misc.Unsafe 类中的各个方法。调用 Unsafe 类中的 CAS 方法,JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。CAS 是一种系统原语,原语属于操作系统范畴,是由若干条硬件指令组成,用于完成某个功能。并且,原语的执行必须是连续的,在执行过程中不允许被中断,保证了原子性
JUC 包下的原子类都是采用 CAS 实现的无锁
AtomicInteger
private volatile int value
public final int getAndSet(int newValue){\
for(;;){
int current = get();
if(compareAndSet(current, newValue))
return current;
}
}
public fianl boolean compareAndSet(int expect, int update){
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
二、Unsafe
Unsafe 类是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过 native 方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe 类存在于 sun.misc 包中,其内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中 CAS 操作的执行依赖于 Unsafe 类的方法
compareAndSwapInt() 的参数列表中的 valueOffset 代表的是内存偏移量,从另一方面说明,Unsafe 类是直接操作内存进行 CAS 的
Unsafe 类中的所有方法都是 native 修饰的,也就是说 Unsafe 类中的方法都直接调用操作系统底层资源执行相应的任务
三、原子类
1.原理
原子类利用 CAS、volatile 和 native 方法来保证原子性,从而避免了 synchronized 的高开销
2.基本类型原子类
AtomicInteger、AtomicBoolean、AtomicLong
常用 API
public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,并设置新的值
public final int getAndIncrement() //获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值
package com.sisyphus.Atomic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class MyNumber{
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus(){
atomicInteger.getAndIncrement();
}
}
public class AtomicIntegerDemo {
public static final int SIZE = 50;
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
MyNumber myNumber = new MyNumber();
for (int i = 0; i < SIZE; i++) {
new Thread(()->{
try{
for (int j = 0; j < 1000; j++){
myNumber.addPlusPlus();
}
}finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "result: " + myNumber.atomicInteger.get());
}
}
3.数组类型原子类
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
简单用法
package com.sisyphus.Atomic;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
for(int i = 0; i < atomicIntegerArray.length(); i++){
System.out.println(atomicIntegerArray.get(i));
}
System.out.println();
int tmpInt = 0;
tmpInt = atomicIntegerArray.getAndSet(0, 1122);
System.out.println(tmpInt + "\t" + atomicIntegerArray.get(0));
tmpInt = atomicIntegerArray.getAndIncrement(0);
System.out.println(tmpInt + "\t" + atomicIntegerArray.get(0));
}
}
4.引用类型原子类
AtomicReference、AtomicStampedReference、AtomicMarkableReference
AtomicRerence
package com.sisyphus.Atomic;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.util.concurrent.atomic.AtomicReference;
@Getter
@ToString
@AllArgsConstructor
class User{
String userName;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<User> atomicReference = new AtomicReference<>();
User jack = new User("jack", 18);
User rose = new User("rose", 17);
atomicReference.set(jack);
System.out.println(atomicReference.compareAndSet(jack, rose) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(jack, rose) + "\t" + atomicReference.get().toString());
}
}
手写自旋锁
package com.sisyphus.Atomic;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + "-----come in");
while(!atomicReference.compareAndSet(null, thread)){
}
}
public void unLock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(Thread.currentThread().getName() + "-----task over,unLock.");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.lock();
try{
TimeUnit.SECONDS.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
spinLockDemo.unLock();
},"A").start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.lock();
spinLockDemo.unLock();
},"B").start();
}
}
AtomicStampedReference
package com.sisyphus.Atomic;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.concurrent.atomic.AtomicStampedReference;
@NoArgsConstructor
@AllArgsConstructor
@Data
class Book{
private int id;
private String bookName;
}
public class AtomicStampedDemo {
public static void main(String[] args) {
Book javaBook = new Book(1, "javaBook");
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(javaBook, 1);
System.out.println(stampedReference.getReference() + "\t" + stampedReference.getStamp());
Book mysqlBook = new Book(2, "mysqlBook");
boolean b;
b = stampedReference.compareAndSet(javaBook, mysqlBook, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(b + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
b = stampedReference.compareAndSet(mysqlBook, javaBook, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(b + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
}
}
ABA 问题
package com.sisyphus.Atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference(100, 1);
public static void abaHappen() {
new Thread(()->{
atomicInteger.compareAndSet(100, 101);
try{
TimeUnit.MILLISECONDS.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
atomicInteger.compareAndSet(101, 100);
},"t1").start();
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicInteger.compareAndSet(100, 2022) + "\t" + atomicInteger.get());
},"t2").start();
}
public static void main(String[] args) {
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t" + "初始版本号:" + stamp);
try{
TimeUnit.MILLISECONDS.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t" + "2次流水号:" + stampedReference.getStamp());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t" + "3次流水号:" + stampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
try{
TimeUnit.MILLISECONDS.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
boolean b = stampedReference.compareAndSet(100, 2022, stamp, stamp+1);
System.out.println(b + "\t" + stampedReference.getReference() + "\t" + stampedReference.getStamp());
},"t4").start();
}
}
AtomicMarkableReference
可以理解为一次性的 AtomicStamptedReference,只有 true/false 两种状态
package com.sisyphus.Atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class AtomicMarkableReferenceDemo {
static AtomicMarkableReference markableReference = new AtomicMarkableReference(100, false);
public static void main(String[] args) {
new Thread(()->{
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t" + "默认标识:" + marked);
try{
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
markableReference.compareAndSet(100, 1000, marked, !marked);
},"t1").start();
new Thread(()->{
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t" + "默认标识:" + marked);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = markableReference.compareAndSet(100, 2000,marked, !marked);
System.out.println(Thread.currentThread().getName() + "\t" + "t2线程CASresult:" + b);
System.out.println(Thread.currentThread().getName() + "\t" + markableReference.isMarked());
System.out.println(Thread.currentThread().getName() + "\t" + markableReference.getReference());
},"t2").start();
}
}
5.对象的属性修改原子类
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
AtomicIntegerFieldUpdater<T> 基于反射的实用程序,可以对指定类的指定 volatile int 字段进行原子更新
AtomicIntegerFieldUpdater<T> 基于反射的实用程序,可以对指定类的指定 volatile long 字段进行原子更新
AtomicIntegerFieldUpdater<T> 基于反射的实用程序,可以对指定类的指定 volatile 引用字段进行原子更新
使用目的:以一种线程安全的方式操作非线程安全对象内的某些字段,而不必在方法上加 synchronized,锁住整个对象
使用要求:
- 更新的对象属性必须使用 public volatile 修饰符
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性
AtomicIntegerFieldUpdater
package com.sisyphus.Atomic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
class BankAccount{
String bankName = "CCB";
//更新的对象属性必须使用 public volatile 修饰符
public volatile int money = 0;
public void add(){
money++;
}
//因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater() 创建一个更新器,
//并且需要设置想要更新的类和属性
AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
//不加 synchronized,保证高性能原子性
public void transMoney(BankAccount bankAccount){
fieldUpdater.getAndIncrement(bankAccount);
}
}
public class AtomicIntegerFieldUpdaterDemo {
public static void main(String[] args) throws InterruptedException{
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for(int i = 0; i < 10; i++){
new Thread(()->{
try{
for(int j = 0; j < 1000; j++){
bankAccount.transMoney(bankAccount);
}
} finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + "result:" + bankAccount.money);
}
}
AtomicReferenceFieldUpdater
需求说明:
多线程并发调用一个类的初始化方法,要求只被初始化一次
package com.sisyphus.Atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
class MyVar{
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<MyVar, Boolean> referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, "isInit");
public void init(MyVar myVar){
if(referenceFieldUpdater.compareAndSet(myVar, Boolean.FALSE, Boolean.TRUE)){
System.out.println(Thread.currentThread().getName() + "\t" + "-----start init, need 3 seconds");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "-----over init");
}else {
System.out.println(Thread.currentThread().getName() + "\t" + "-----已经有线程在进行初始化工作");
}
}
}
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for (int i = 0; i < 5; i++){
new Thread(()->{
myVar.init(myVar);
},String.valueOf(i)).start();
}
}
}
6.原子操作增强类
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder
LongAdder
当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共和时,此类通常优于 AtomicLong。在低更新争用下,这两个类具有相似的特征。但在高争用的情况的下,这一类的预期吞吐量明显更高,但代价是空间消耗更高
void add(long x) 将当前的 value 加 x
void increment() 将当前的 value 加 1
void decrement() 将当前的 value 减 1
long sum() 返回当前值。特别注意,在没有并发更新 value 的情况下,sum 会返回一个精确值,在存在并发的情况下,sum 不保证返回精确值
void reset() 将 value 重置为 0,可用于替代重新 new 一个 LongAdder,但此方法只可以在没有并发更新的情况下使用
long sumThenReset() 获取当前的 value,并将 value 重置为 0
package com.sisyphus.Atomic;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.LongBinaryOperator;
public class LongAdderAPIDemo {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());
LongAccumulator longAccumulator = new LongAccumulator(new LongBinaryOperator() {
@Override
public long applyAsLong(long left, long right) {
return left + right;
}
}, 0);
longAccumulator.accumulate(1);
longAccumulator.accumulate(3);
System.out.println(longAccumulator.get());
}
}
需求说明:
热点商品点赞计算器,不要求实时精确
package com.sisyphus.Atomic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
class ClickNumber{
int number = 0;
public synchronized void clickBySynchronized(){
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong(){
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
public void clickByLongAdder(){
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator((x,y) -> x + y, 0);
public void clickByLongAccumulator(){
longAccumulator.accumulate(1);
}
}
public class AccumulatorCompareDemo {
public static final int _1W = 10000;
public static final int threadNumber = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch = new CountDownLatch(threadNumber);
startTime = System.currentTimeMillis();
for(int i = 0; i < threadNumber; i++){
new Thread(()->{
try {
for(int j = 0; j < 100 * _1W; j++){
clickNumber.clickByLongAccumulator();
}
}finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
endTime = System.currentTimeMillis();
System.out.println("-----costTime: " + (endTime - startTime));
System.out.println("clickByLongAccumulator:" + clickNumber.longAccumulator.get());
}
}
需求说明:
一个很大的 List,里面都是 Integer,如何实现相加
Striped64
LongAdder 继承了 Striped64
变量和方法说明
static final int NCPU = Runtime.getRuntime().availableProcessors(); //CPU 数量,即 cells 数组的最大长度
transient volatile Cell[] cells; //cells 数组,为 2 的幂,方便位运算
transient volatile long base; //基础 value 值,当并发较低时,只累加该值,用于没有竞争的情况,通过 CAS 更新
transient volatile int cellsBusy; //创建或者扩容 Cells 数组时使用的自旋锁
NCPU 当前计算机 CPU 数量,Cell 数组扩容时会使用到
base 类似于 AtomicLong 中全局的 value 值。在没有竞争情况下数据直接累加到 base 上,或者 cells 扩容时,也需要将数据写入到 base 上
collide 表示扩容意向,false 一定不会扩容,true 可能会扩容
cellsBusy 初始化 cells 或者扩容 cells 需要获取锁,0 表示无锁状态,1 表示其他线程已经持有了锁
casCellsBusy() 通过 CAS 操作修改 cellsBusy 的值,CAS 成功代表获取锁,返回 true
getProbe() 获取当前线程的 hash 值
advanceProbe() 重置当前线程的 hash 值
LongAdder 的基本思路就是分散热点,将 value 值分散到一个 Cell 数组中,不同的线程会散列到数组的不同下标中,各个线程只对自己对应下标的位置的值进行 CAS 操作,最后获取结果时,将各个下标对应的值累加返回
- 最初无竞争时,只更新 base
- 如果更新 base 失败后,新建一个 Cell[] 数组
- 当多个线程竞争同一个 Cell 比较激烈时,会对 Cell[] 进行扩容