CAS
CAS即是一种典型的乐观锁(无锁)思想,它的原理是:在修改共享变量之后,先对共享变量的最新值进行一个比对,如果最新值和旧值是一样的,说明对共享变量操作这段时间没有其他线程修改变量,就可以将新值赋给共享变量;否则就需要重新取得最新值来重新计算,为了保证变量的可见性,需要使用volatile修饰共享变量
CAS与synchronized效率对比
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇;而synchronized的悲观锁思想,会让线程在没有获得锁的时候,发生上下文切换,上下文切换时线程的阻塞和恢复都需要在内核态来执行
- 在无锁情况下,因为线程一直保持运行,需要额外的CPU支持,会消耗大量的时间片,如果没有分到时间片,虽然不会进入阻塞态,但仍然会导致上下文切换;而且频繁的失败重试会造成大量CPU资源的无端浪费
CAS特点
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,出现了修改就失败重试
- synchronized是基于悲观锁的思想:最悲观的估计,在自己占有锁的时候其他线程不能同时修改共享变量,只有解完了锁其他线程才有机会
- CAS体现的是无锁并发、无阻塞并发:没有加锁,如果不会陷入阻塞;但如果竞争激烈,频繁的重试反而会让效率受影响
CAS的实现(以AtomicInteger为例)
@Slf4j
public final class Demo{
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
add(1);
}
},"t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 1000; i++) {
add(-1);
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("ans={}", atomicInteger.get());
}
private static void add(int num){
// 在循环块中进行操作
while(true){
// 取得旧值
int prev = atomicInteger.get();
// 对旧值进行计算
int update = prev+num;
// compareAndSet方法底层调用的是compareAndSwap方法,比对旧值是否被改变,没有被改变就更新数据并且跳出循环
if(atomicInteger.compareAndSet(prev,update)){
break;
}
}
}
}
原子类
JUC(java.util.concurrent)并发包提供了原子整数、原子引用、原子数组等原子类,内部实现就是基于CAS无锁并发
原子整数
AtomicInteger原子整型、AtomicLong原子长整型、AtomicBoolean原子布尔型,内部方法基本相同,内部提供了部分已经封装好的方法供开发者使用,同时也可以使用提供的CAS方法来自己实现CAS操作
方法 | 功能 |
---|---|
getAndIncrement、incrementAndGet | 原子自增 |
getAndDecrement、decrementAndGet | 原子自减 |
getAndAdd、addAndGet | 原子增加 |
getAndUpdate、updateAndGet | 可传入一个lambda表达式来作复杂的原子计算,底层原理是传入一个函数式接口类型,来满足灵活性和复用性 |
getAndAccumulate、accumulateAndGet | 累加操作 |
compareAndSet | 底层调用compareAndSwap(CAS)方法,可以自己实现CAS操作 |
原子引用
- AtomicReference:原子更新引用
- AtomicStampedReference:原子更新带有版本号的引用类型,该类将整数数值与引用关联起来,可用于原子的更新数据和数据的版本号,解决ABA问题
- AtomicMarkableReference:芋艿更新带有标记位的引用类型,可以原子更新一个布尔类型的标记位和引用类型
ABA问题的解决
ABA问题
@Slf4j
public final class Demo{
private static AtomicReference<String> atomicString = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
String prev = atomicString.get();
String update = "C";
// 将原子引用两次修改
update("B");
update("A");
Thread.sleep(1000);
log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update));
}
private static void update(String update){
String prev = atomicString.get();
log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update));
}
}
如果修改过程中,有一个线程将原子引用修改两次,当前线程是无法感知
使用AtomicStampedReference解决ABA问题
@Slf4j
public final class Demo{
private static AtomicStampedReference<String> atomicString = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
// 获取共享变量值
String prev = atomicString.getReference();
// 获取版本号
int stamp = atomicString.getStamp();
String update = "C";
// 将原子引用两次修改
update("B");
update("A");
Thread.sleep(1000);
log.debug("{}->{},stamp:{},{}",prev,update,stamp,atomicString.compareAndSet(prev,update,stamp,stamp+1));
}
private static void update(String update){
String prev = atomicString.getReference();
int stamp = atomicString.getStamp();
log.debug("{}->{},stamp:{},{}",prev,update,stamp,atomicString.compareAndSet(prev,update,stamp,stamp+1));
}
}
在改动时,不光基于共享变量的值,还需要比较版本号,这样根据版本号就可以追踪原子引用的变化次数
使用AtomicMarkableReference解决ABA问题
@Slf4j
public final class Demo{
private static AtomicMarkableReference<String> atomicString = new AtomicMarkableReference<>("A",true);
public static void main(String[] args) throws InterruptedException {
// 获取共享变量值
String prev = atomicString.getReference();
String update = "C";
// 将原子引用两次修改
update("B");
update("A");
Thread.sleep(1000);
log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update,true,false));
}
private static void update(String update){
String prev = atomicString.getReference();
log.debug("{}->{},{}",prev,update,atomicString.compareAndSet(prev,update,true,false));
}
}
AtomicMarkableReference只使用一个布尔值做一个标记,可以用来保证某个操作只进行一次,当标记发生改变时,就不再进行操作,他不会统计改变的次数,而只是检测是否改变
原子数组
- 使用原子引用来包装数组只能保护数组的引用的线程安全性,但是无法保证数组内部数据的线程安全性,所以需要使用到原子数组
- AtomicIntgerArray:原子整型数组
- AtomicLongArray:原子长整型数组
- AtomicReferenceArray:原子引用类型数组
数组的线程安全性通用测试方法
@Slf4j
public final class Demo{
public static void main(String[] args) throws InterruptedException {
// 检测普通数组的线程安全性
test(
()->new int[10],
(array)->array.length,
(array,index)->array[index]++,
(array)->System.out.println(Arrays.toString(array))
);
// 检测原子数组的线程安全性
test(
()->new AtomicIntegerArray(10),
(array)->array.length(),
(array,index)->array.getAndIncrement(index),
(array)-> System.out.println(array)
);
}
/***
* supplier-提供者,无参数有结果,用于提供数据
* function-函数,有参数有结果;一个参数一个结果称为Function,两个参数一个结果称为BiFunction
* consumer-消费者,有参数无结果,对数据作处理,一个参数称为Consumer,两个参数称为BiConsumer
*
* 数组的线程安全性检测通用测试方法
* @param arraySupplier 提供需要检测的数组
* @param lengthFun 提供获取数组长度的方法
* @param putConsumer 数组操作的方法
* @param printConsumer 打印数组的方法
* @param <T> 数组类型
*/
private static <T> void test(
Supplier<T> arraySupplier,
Function<T,Integer> lengthFun,
BiConsumer<T,Integer> putConsumer,
Consumer<T> printConsumer
){
List<Thread> threads = new ArrayList<>();
T array = arraySupplier.get();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
threads.add(new Thread(()->{
for (int j = 0; j < 10000; j++) {
// 将一万次数据操作均摊到每个数组元素上
putConsumer.accept(array,j%length);
}
}));
}
// 开启所有线程
threads.forEach(thread -> thread.start());
// 等待所有线程结束
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 打印数组
printConsumer.accept(array);
}
}
字段更新器
- 原子引用只能原子的修改引用的指向,不能修改引用的对象内部的属性,对内部的属性需要使用字段更新器来处理
- AtomicReferenceFieldUpdater:引用类型字段更新器
- AtomicIntegerFieldUpdater:整型字段更新器
- AtomicLongFieldUpdater:长整型字段更新器
@Slf4j
public final class Demo{
public static void main(String[] args) throws InterruptedException {
Student student = new Student("Jack");
// 为字段绑定更新器
AtomicReferenceFieldUpdater<Student,String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
// 更新器的方法和其他原子类基本一致
updater.compareAndSet(student,"Jack","Tom");
log.debug("{}",student);
}
}
class Student {
volatile String name;
public Student(String name){
this.name =name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
原子累加器
除了可以使用原子基本类型的getAndIncrement方法,还有特定的原子累加器如LongAddr、DoubleAddr,可以提供更高性能的累加;性能提升的原因是在有竞争时,设置多个累加单元,每个线程进行自己的累加操作,最后将多个线程累加操作汇总,因此减少了CAS重试失败
LongArr与AtomicLong对比
@Slf4j
public final class Demo{
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
test(
()->new LongAdder(),
longAdder -> longAdder.increment()
);
}
log.debug("");
for (int i = 0; i < 5; i++) {
test(
()->new AtomicLong(0),
atomicLong->atomicLong.getAndIncrement()
);
}
}
private static <T> void test(
Supplier<T> supplier,
Consumer<T> action
){
T addr = supplier.get();
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
threads.add(new Thread(()->{
action.accept(addr);
}));
}
long start = System.nanoTime();
threads.forEach(thread -> thread.start());
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
log.debug(addr+"用时{}",(end-start)/1000_000);
}
}
LongAddr源码分析
LongAddr类中有几个关键域:
// 累加单元数组,懒惰初始化
transient volatile Cell[] cells;
// 基础值,如果没有竞争,就用cas累加这个域
transient volatile long base;
// 在cells创建或扩容时,置为1,表示加锁;结合cas和该标志位实现cas加锁
transient volatile int cellsBusy;
cas锁
public class LockCas{
// 标志位,为0时表示没有加锁,为1时表示加锁
private AtomicInteger state = new AtomicInteger(0);
// 加锁操作,通过cas来修改标志位,只有标志位为0时才可以修改成功,符合没有加锁时才能加锁的特点
public void lock(){
while(true){
if(state.compareAndSet(0,1)){
break;
}
}
}
// 解锁操作,将标志位修改为0
public void unlock(){
state.set(0);
}
}
CAS锁不推荐在生产开发中使用,虽然底层使用到了这个方式;加锁的场景一般是为了应对高并发场景,而CAS本就不适合大量线程竞争的场景
累加单元类Cell
// 该注解防止缓存行伪共享
@sun.misc.Contended
static final class Cell{
volatile long value;
Cell(long x) {value = x;}
// 最重要的方法,用cas的方式来进行累加,prev表示旧值,next表示新值
final boolean cas(long prev,long next){
return UNSAFE.compareAndSwapLong(this,valueOffset,prev,next);
}
// 省略不重要代码
}
伪共享
- 因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率
- 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)
- 缓存的加入虽然中和了CPU和内存之间的速度差异,但是导致了数据副本的产生,即不同CPU核心对应不同的缓存行,同一份数据会缓存在不同核心的缓存行中,如果其中一个核心对数据进行了修改,不同缓存行的数据就不一致了
- CPU要保证数据的一致性,如果某个CPU核心修改了数据,其他CPU核心对应的整个缓存行必须失效
在如上缓存行的基础上,回到cells数组的累加操作:
一个Cell对象的大小约是16byte对象头+8byte长整型,即24byte;所以一个缓存行(64byte)能够放下Cell数组中的两个Cell对象,假设此时核心1需要修改Cell[0],核心2需要修改Cell[1],那么无论谁修改成功,都会导致另一个核心的缓存行失效,需要重新从内存读取,从而使性能受到影响,这个缓存行失效的问题就称为伪共享
@sun.misc.Contended
就用来解决这个缓存行失效的问题,它的原理是在使用此注解的对象或字段前后各增加128字节大小padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样就不会造成对方的缓存行失效
该注解是Java8提供的,在jdk1.7及以前,通常通过手动地前后填充的方式来避免伪共享:
public volatile long value = 0;
改为
volatile long p0, p1, p2, p3, p4, p5, p6;
public volatile long value = 0;
volatile long q0, q1, q2, q3, q4, q5, q6;
累加操作源码分析
increment和decremnt方法底层都是调用add方法
add方法分析:
add方法主要做了对无竞争时使用base进行累加,和对已经创建Cell的线程进行累加操作,在累加的cas操作失败时,表示出现了竞争,则进入longAccumulate方法创建Cell
longAccumulate方法分析:
cells已经创建的情况:
cells未创建且cellsBusy加锁成功的情况:
获取累加最终结果:
Unsafe
Unsafe对象提供了非常底层的,操作内存、线程的方法;Unsafe时非常底层的类,它的对象不能直接创建(不推荐开发者使用),只能通过反射获得
获取Unsafe对象
public class UnsafeAccessor{
static Unsafe unsafe;
static {
try{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
}catch(NoSuchFieldException|IllegalAccessException e){
throw new Error(e);
}
}
}
使用unsafe进行cas操作
@Slf4j
public final class Demo{
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = getUnsafe();
long idOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student();
student.id = 1;
student.name = "Jack";
unsafe.compareAndSwapInt(student,idOffset,1,2);
unsafe.compareAndSwapObject(student,nameOffset,"Jack","Tom");
log.debug("{}",student);
}
private static Unsafe getUnsafe(){
Unsafe unsafe;
try{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
}catch(NoSuchFieldException|IllegalAccessException e){
throw new Error(e);
}
return unsafe;
}
}
class Student {
int id;
String name;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
模拟实现原子整数
public class MyAtomicInteger {
private volatile int value;
static final Unsafe unsafe;
static final long DATA_OFFSET;
static {
// 初始化Unsafe对象和字段偏移量
try{
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);
DATA_OFFSET = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("data"));
}catch(NoSuchFieldException|IllegalAccessException e){
e.printStackTrace();
throw new RuntimeException(e);
}
}
public MyAtomicInteger(int value){
this.value = value;
}
public int get(){
return value;
}
public void decrement(){
update(value-1);
}
public void increment(){
update(value+1);
}
public void update(int next){
while(true){
int prev = value;
if(unsafe.compareAndSwapInt(this,DATA_OFFSET,prev,next)){
break;
}
}
}
}