由于 JVM 的 Synchronized 重量级锁涉及操作系统(如Linux)内核态下互斥锁的使用,因此其线程阻塞和唤醒,都涉及进程在用户态到内核态的频繁切换,导致重量级锁开销大、性能低。而 JVM 的Synchronized 轻量级锁使用 CAS(Compare And Swap,比较并交换)进行自旋抢锁,CAS 是 CPU 指令级的原子操作,并处于用户态下,所以JVM轻量级锁的开销较小。
JDK 5增加 JUC(java.util.concurrent)并发包,对操作系统的底层CAS原子操作进行了封装,为上层Java程序提供了 CAS 操作的API。
补充说明一下 用户态 和 内核态 :
用户态执行的程序,进程所能访问的内存空间和对象受到限制,其所处于的cpu是可被抢占的。
内核态的进程,可以访问所有的内存空间和对象,且所占有的cpu是不允许被抢占的。
用户态进程可以通过【系统调用】主动要求切换到内核态,【中断】和【异常】也可导致两种状态间的切换。
Unsafe 类的全限定名为sun.misc.Unsafe,Unsafe提供了 CAS 方法,Unsafe大量的方法都是native方法,基于C++语言实现,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。
Unsafe提供的CAS方法主要如下:
/**
* 定义在Unsafe类中的三个“比较并交换”原子方法
* @param o 需要操作的字段所在的对象
* @param offset 需要操作的字段的偏移量(相对的,相对于对象头)
* @param expected 期望值(旧的值)
* @param update 更新值(新的值)
* @return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(
Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(
Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(
Object o, long offset, long expected, long update);
CAS是一种无锁算法,该算法关键依赖两个值:期望值(旧值)和新值。底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。
操作系统层面的 CAS 是一条 CPU 的原子指令(cmpxchg指令),正是由于该指令具备原子性,因此使用 CAS 操作数据时不会造成数据不一致的问题,Unsafe提供的CAS方法直接通过native方式(封装C++代码)调用了底层的CPU指令cmpxchg。
使用CAS进行无锁编程的步骤大致如下:
① 获得字段的期望值(oldValue)。
② 计算出需要替换的新值(newValue)。
③ 通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败就重复第 ① 步到第 ② 步,一直到CAS成功,这种重复俗称CAS自旋。
使用CAS进行无锁编程的伪代码如下:
do{
获得字段的期望值(oldValue);
计算出需要替换的新值(newValue);
} while (!CAS(内存地址,oldValue,newValue))
当CAS将内存地址的值与预期值进行比较时,如果相等,就证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行;如果不相等,就说明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS的性能会很高;当并发修改的线程多,冲突出现的机会多时,自旋的次数也会很多,CAS的性能会大大降低。所以,提升CAS无锁编程效率的关键在于减少冲突的机会。
使用 synchronized
的同步操作可以保证线程的安全性
,但是会降低并发程序的性能
。所以,JDK为线程不安全的操作提供了一些原子类,与 synchronized 同步机制相比,JDK原子类是基于CAS轻量级原子操作
的实现,使得程序运行效率变得更高
。
JUC并发包中的原子类存放在java.util.concurrent.atomic类路径下。根据操作的目标数据类型,可以将JUC包中的原子类分为4类:基本原子类
、数组原子类
、原子引用类
和 字段更新原子类
。
① 基本原子类
② 数组原子类
③ 引用原子类
AtomicReference:引用类型原子类。
AtomicMarkableReference:带有更新标记位的原子引用类型。
AtomicStampedReference:带有更新版本号的原子引用类型。
AtomicMarkableReference 类
将 boolean 标记与引用关联起来,可以 解决
使用 AtomicBoolean 进行原子更新时 可能出现的ABA问题
。
AtomicStampedReference 类
将整数值与引用关联起来,可以 解决
使用 AtomicInteger 进行原子更新时 可能出现的ABA问题
。
④ 字段更新原子类
AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicReferenceFieldUpdater:原子更新引用类型中的字段。
在多线程环境下,如果涉及基本数据类型的并发操作,不建议采用synchronized重量级锁进行线程同步,而是建议优先使用基础原子类保障并发操作的线程安全性。
AtomicInteger、AtomicLong、AtomicBoolean三个基础原子类的方法几乎相同,我们以AtomicInteger为例详细说明。
常用方法:
// 获取当前的值
public final int get()
// 获取当前的值,然后设置新的值
public final int getAndSet(int newValue)
// 获取当前的值,然后自增
public final int getAndIncrement()
// 获取当前的值,然后自减
public final int getAndDecrement()
// 获取当前的值,并加上预期的值
public final int getAndAdd(int delta)
//通过CAS方式设置整数值
boolean compareAndSet(int expect, int update)
代码示例:
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class AtomicIntegerDemo {
public static void main(String[] args) {
final AtomicInteger atomic = new AtomicInteger(0);
final AtomicInteger compareAndSetAtomic = new AtomicInteger(10);
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 5; i++) {
pool.execute(()-> {
log.info("getAndIncrement -> atomic = {}", atomic.getAndIncrement());
// 获取当前的值
log.info("get-1 -> atomic = {}", atomic.get());
});
}
for (int i = 0; i < 5; i++) {
pool.execute(()-> {
log.info("getAndDecrement -> atomic = {}", atomic.getAndDecrement());
});
}
for (int i = 0; i < 5; i++) {
pool.execute(()-> {
log.info("getAndSet -> atomic = {}",
atomic.getAndSet(new Random().nextInt(10)));
});
}
for (int i = 0; i < 5; i++) {
pool.execute(()-> {
// 通过CAS方式设置整数值 先比较再设值,如果当前值等于第一个参数,就设置为第二个值
log.info("compareAndSet -> compareAndSetAtomic = {}",
compareAndSetAtomic.compareAndSet(10, 100));
// 获取当前的值
log.info("get-2 -> compareAndSetAtomic = {}", compareAndSetAtomic.get());
});
}
pool.shutdown();
}
}
AtomicInteger线程安全原理:
基础原子类(以AtomicInteger为例)主要通过CAS自旋 + volatile 的方案实现,CAS用于保障变量操作的原子性,volatile关键字用于保障变量的可见性。采用这种方案,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。
AtomicInteger源码中的主要方法都是通过CAS自旋实现的。CAS自旋的主要操作为:如果一次CAS操作失败,获取最新的value值后,再次进行CAS操作,直到成功。
AtomicInteger所包装的内部value成员,是一个使用关键字volatile修饰的内部成员。关键字volatile可以保证任何线程,在任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。
数组原子类的功能是通过原子方式更数组中的某个元素的值。以 AtomicIntegerArray 类为例子,AtomicIntegerArray类的常用方法如下:
//获取 index=i 位置元素的值
public final int get(int i)
//返回 index=i 位置当前的值,并将其设置为新值:newValue
public final int getAndSet(int i, int newValue)
//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndIncrement(int i)
//获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndDecrement(int i)
//获取 index=i 位置元素的值,并加上预期的值
public final int getAndAdd(int i, int delta)
//如果输入的数值等于预期值,就以原子方式将位置i的元素值设置为输入值getAndAdd
boolean compareAndSet(int expect, int update)
//最终将位置i的元素设置为newValue
//lazySet()方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
public final void lazySet(int i, int newValue)
代码示例 2-3-1 :
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class AtomicReferenceDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
AtomicReference<User> atomicReference = new AtomicReference<>();
User user1 = new User("姓名1", 1);
atomicReference.set(user1);
log.info("初始 atomicReference -> get() -> {}", atomicReference.get());
pool.execute(() -> {
User user2 = new User("姓名2", 2);
log.info("compareAndSet -> user2 -> {}",
atomicReference.compareAndSet(user1, user2));
log.info("user2 -> atomicReference -> get() -> {}", atomicReference.get());
});
pool.execute(() -> {
User user3 = new User("姓名3", 3);
log.info("compareAndSet -> user3 -> {}",
atomicReference.compareAndSet(user1, user3));
log.info("user3 -> atomicReference -> get() -> {}", atomicReference.get());
});
pool.shutdown();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class User {
// 姓名
private String name;
// 年龄
private Integer age;
}
打印结果 2-3-1 :
14:34:54.274 [main] INFO com.AtomicReferenceDemo - 初始 atomicReference -> get() -> User(name=姓名1, age=1)
14:34:54.278 [pool-1-thread-1] INFO com.AtomicReferenceDemo - compareAndSet -> user2 -> true
14:34:54.279 [pool-1-thread-2] INFO com.AtomicReferenceDemo - compareAndSet -> user3 -> false
14:34:54.279 [pool-1-thread-1] INFO com.AtomicReferenceDemo - user2 -> atomicReference -> get() -> User(name=姓名2, age=2)
14:34:54.279 [pool-1-thread-2] INFO com.AtomicReferenceDemo - user3 -> atomicReference -> get() -> User(name=姓名2, age=2)
Process finished with exit code 0
如果需要保障对象某个字段(或者属性)更新操作的原子性,就需要用到属性更新原子类。
使用属性更新原子类保障属性安全更新的流程大致需要两步:
第一步,更新的对象属性必须使用public volatile修饰符。
第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须调用静态方法newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。
代码示例 2-4-1 :
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
@Slf4j
public class AtomicIntegerFieldUpdaterDemo {
public static void main(String[] args) {
// 调用静态方法newUpdater()创建一个更新器updater
AtomicIntegerFieldUpdater<Dog> updater =
AtomicIntegerFieldUpdater.newUpdater(Dog.class, "age");
Dog dog = new Dog("旺财", "白色");
log.info("init get -> {}", updater.get(dog));
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.execute(() -> {
//使用属性更新器的 getAndIncrement 增加 dog 的 age
log.info("getAndIncrement -> {}", updater.getAndIncrement(dog));
});
pool.execute(() -> {
//使用属性更新器的 getAndAdd 增加 dog 的 age
log.info("getAndAdd -> {}", updater.getAndAdd(dog,3));
log.info("getAndAdd -> get -> {}", updater.get(dog));
});
//使用属性更新器的get获取 dog 的age值
log.info("get -> {}", updater.get(dog));
pool.shutdown();
}
}
class Dog {
// 名字
private String name;
// 颜色
private String colour;
// 年龄
public volatile int age;
public Dog(String name, String colour) {
this.name = name;
this.colour = colour;
}
}
打印结果 2-4-1 :
14:55:34.526 [main] INFO com.AtomicIntegerFieldUpdaterDemo - init get -> 0
14:55:34.533 [pool-1-thread-1] INFO com.AtomicIntegerFieldUpdaterDemo - getAndIncrement -> 0
14:55:34.533 [main] INFO com.AtomicIntegerFieldUpdaterDemo - get -> 1
14:55:34.533 [pool-1-thread-2] INFO com.AtomicIntegerFieldUpdaterDemo - getAndAdd -> 1
14:55:34.534 [pool-1-thread-2] INFO com.AtomicIntegerFieldUpdaterDemo - getAndAdd -> get -> 4
Process finished with exit code 0
CAS原子操作使用不当就会存在ABA问题,举个例子说明ABA 问题:线程1从内存位置M中取出值A,另一个线程2也取出值A。现在假设线程2进行了一些操作之后将M位置的数据A变成了B,然后又在一些操作之后将B变成了A。这个时候线程1以为内存位置M的值A没被改过,但实际上被动过A->B->A.
解决ABA问题的常用方案,就是采用乐观锁使用版本号(version)方式处理。乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。
JDK提供了一个AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference 的 compareAndSet() 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳(Stamp)标志的值更新为给定的更新值。
// 构造函数,V表示要引用的原始数据,initialStamp表示最初的版本印戳(版本号)
AtomicStampedReference(V initialRef, int initialStamp)
/******** 常用方法: *******/
//获取被封装的数据
public V getRerference();
//获取被封装的数据的版本印戳
public int getStamp();
/**
* @param expectedReference 预期引用值
* @param newReference 更新后的引用值
* @param expectedStamp 预期印戳 (Stamp)标志值
* @param newStamp 更新后的印戳(Stamp)标志值
**/
public boolean compareAndSet(V expectedReference, V newReference,
int expectedStamp, int newStamp);
代码示例 3-2-1 :
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
@Slf4j
public class AtomicStampedReferenceDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
Mail mail1 = new Mail("测试邮件", 1);
AtomicStampedReference<Mail> stampedReference =
new AtomicStampedReference<>(mail1, 1);
pool.execute(() -> {
// 获取当前版本号
int stamp = stampedReference.getStamp();
log.info("测试邮件->状态2 sleep 前,版本号={},mail={}",stamp,
stampedReference.getReference());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Mail mail2 = new Mail("测试邮件", 2);
Boolean result = stampedReference.compareAndSet(mail1, mail2, stamp, stamp + 1);
log.info("测试邮件->状态2 -> 执行情况 = {}", result);
stamp++;
log.info("此刻的版本号是->{}", stampedReference.getStamp());
Boolean resultToo =
stampedReference.compareAndSet(mail2, new Mail("测试邮件", 3), stamp, stamp + 1);
log.info("测试邮件->状态3 -> 执行情况 = {}", resultToo);
});
pool.execute(() -> {
// 获取当前版本号
int stamp = stampedReference.getStamp();
log.info("测试邮件->状态4 sleep 前,版本号={},mail={}",stamp,
stampedReference.getReference());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Boolean result =
stampedReference.compareAndSet(mail1, new Mail("测试邮件", 4), stamp, stamp + 1);
log.info("测试邮件->状态4 -> 执行情况 = {}", result);
});
pool.shutdown();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Mail {
// 主题
private String subject;
// 状态
private Integer status;
}
执行结果 3-2-1 :
15:46:25.822 [pool-1-thread-2] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态4 sleep 前,版本号=1,mail=Mail(subject=测试邮件, status=1)
15:46:25.822 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态2 sleep 前,版本号=1,mail=Mail(subject=测试邮件, status=1)
15:46:30.837 [pool-1-thread-2] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态4 -> 执行情况 = false
15:46:30.837 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态2 -> 执行情况 = true
15:46:30.837 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 此刻的版本号是->2
15:46:30.838 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态3 -> 执行情况 = true
Process finished with exit code 0
AtomicMarkableReference适用于只要知道对象是否被修改过,而不适用于对象被反复修改的场景。
代码示例 3-3-1 :
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicMarkableReference;
@Slf4j
public class AtomicMarkableReferenceDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
AtomicMarkableReference<Integer> markableReference =
new AtomicMarkableReference<Integer>(1, false);
pool.execute(() -> {
Integer reference = markableReference.getReference();
Boolean mark = markableReference.isMarked();
log.info("mark={}, reference={}", mark, reference);
markableReference.compareAndSet(reference, 12, mark, !mark);
Boolean newMark1 = markableReference.isMarked();
log.info("newMark1={}, reference={}", newMark1,
markableReference.getReference());
});
pool.execute(() -> {
Integer reference = markableReference.getReference();
Boolean mark_ = markableReference.isMarked();
log.info("mark_ ={}, reference_={}", mark_, reference);
markableReference.compareAndSet(reference, 12, mark_, !mark_);
Boolean newMark2 = markableReference.isMarked();
log.info("newMark2_={}, reference_={}", newMark2,
markableReference.getReference());
});
pool.shutdown();
}
}
执行结果 3-3-1 :
16:19:35.120 [pool-1-thread-2] INFO com.AtomicMarkableReferenceDemo - mark_ =false, reference_=1
16:19:35.120 [pool-1-thread-1] INFO com.AtomicMarkableReferenceDemo - mark=false, reference=1
16:19:35.124 [pool-1-thread-1] INFO com.AtomicMarkableReferenceDemo - newMark1=true, reference=12
16:19:35.124 [pool-1-thread-2] INFO com.AtomicMarkableReferenceDemo - newMark2_=true, reference_=12
Process finished with exit code 0
CAS操作的弊端主要有三点:ABA问题、开销问题、只能保证一个共享变量之间的原子性操作。
① ABA问题的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候将版本号加1,那么操作序列A==>B==>A就会变成 A1==>B2==>A3,如果将A1当作A3的预期数据,就会操作失败。
② AtomicStampedReference
③ AtomicMarkableReference
自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功),就会给CPU带来非常大的执行开销。
解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案为:
① 分散操作热点,使用LongAdder替代基础原子类 AtomicLong,LongAdder将单个CAS热点(value值)分散到一个cells 数组中。
② 使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。JUC中非常重要的基础类AQS(抽象队列同步器)就是这么做的。
当对一个共享变量执行操作时,可以使用CAS的方式来保证原子操作,但对多个共享变量操作时,CAS就无法保证操作的原子性。常把多个共享变量合并成一个共享变量来操作,来规避该问题。AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个AtomicReference实例后再进行CAS操作。
.