1.AtomicInteger和AtomicLong
对于多线程操作一个数的加减,可以使用AtomicInteger代替synchronized,其性能更好。Atomic类皆为自旋性质。
源码:
public class AtomicInteger extends Number implements Serializable{
private volatile int value; //使用volatile修饰一个int变量,对其进行CAS操作。
//自增方法
public final int getAndIncrement(){
for(;;){
int current = get();
int next = current + 1;
if(compareAndSet(current, next)){
return current;
}
}
}
//自减方法
public final int getAndIncrement(){
for(;;){
int current = get();
int next = current - 1;
if(compareAndSet(current, next)){
return current;
}
}
}
}
-
悲观锁和乐观锁
悲观锁:即认为数据发生冲突的概率很大,所以在读操作之前就上锁。synchronized和ReentrantLock都是悲观锁的典型例子。 乐观锁:即认为数据发生冲突的概率比较小,读操作之前不上锁。 写操作时,再判断数据在此期间是否被其它线程修改。被修改就读出来,未被修改则写回去。 判断数据修改,同时写回新值,这两个操作要合成一个原子操作,也就是CAS(CompareAndSet)。 AtomicInteger是典型的乐观锁实现。 -
Unsafe的CAS详解
//原子操作类中的CAS方法调用的是Unsafe中的一个native函数: public final boolean compareAndSet(int expect, int update){ return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } public final native boolean compareAndSwapInt(Object obj,long offset, int v1, int v2){}; //Unsafe中成员变量转变为偏移量方法 public native long objectFieldoffset(Field var1);compareAndSet方法中的第一个参数expect是指变量的旧值(是读出来的值,写回去的时候,期望不会被其它线程修改,所以称为expect), 第二个参数update是指变量的新值(修改过的希望写入的值)。 当expect等于变量当前的值时,说明在修改的期间,没有其它线程对此变量进行过修改,所以可以成功写入,变量更新为update,返回true,否则返回false。 Unsafe中的所有方法都是native的。 compareAndSwapInt方法有四个参数,其中,第一个是对象,第二个是对象的成员变量(即AtomicInteger里面包的int变量value)后面两个参数不变。 其中第二个参数,经常被称为,XXXoffset,意思是某个成员变量在类中的内存偏移量(即变量在内存中的地址),表示该成员变量本身。 所有调用CAS方法的地方,都会先通过objectFieldoffset方法将成员变量转变为一个偏移量。 -
自旋与阻塞
当一个线程拿不到锁的时候,有以下两种基本等待策略: 1、放弃cpu,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度 2、不放弃cpu,空转,不断重试,即“自旋” AtomicInteger的实现使用的就是自旋,单核cpu只能使用策略1。synchronized使用的是策略1和2复合。
2.AtomicBoolean和AtomicDouble
-
如何支持AtomicBoolean和AtomicDouble
在Unsafe中只提供了三种类型的CAS操作:int,long,object(即引用类型)。 AtomicBoolean使用int类型代替 AtomicDouble依赖于double提供的一对double和long类型互转方法。
3.AtomicStamedReference和AtomicMarkableReference
-
ABA问题的解决
ABA问题,即变量值被修改过,但修改后的值与原值相同,CAS会认为其没有被修改过。 AtomicStamedReference的CAS方法: public final boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp); 后两个参数是版本号的旧值和新值。 当expectedReference != 当前对象的Reference时,说明被修改过 当expectedReference == 当前对象的Reference时,再进一步比较expectedStamp是否等于当前的版本号。 -
AtomicMarkableReference
AtomicMarkableReference与AtomicStamedReference原理相似,只是Pair内部类里的版本号是boolean类型的,而不是整型的累加变量。因为是boolean类型,所以只有true和false两种状态,不能完全避免ABA问题,只能减少其发生的概率。
4.AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater
-
为什么需要Atomic***FieldUpdater
对一个非自己编写的,已有的类要想实现对其成员变量的原子操作,就需要Atomic***FieldUpdater。 -
限制条件
要想使用Atomic***FieldUpdater,则必须保证成员变量是volatile关键字修饰的
5.AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray
注意:这三个类并不是对整个数组进行原子操作,而是针对数组中的一个元素。
其底层还是使用的compareAndSwapInt方法
6.Striped和LongAdder
jdk8开始,针对Long型的原子操作,java又提供了LongAddr、Long Accumulator,针对Double,java提供了DoubleAddr、DoubleAccumulator
-
LongAddr原理
AtomicLong内部是一个volatile long型变量,在高并发场景下依旧不够快。 把一个变量拆成多份,变为多个变量。即把一个Long型,拆分为一个base变量外加多个Cell,每个Cell包装一个Long型。当多个变量进行并发累加时,如果并发度低,则直接加到base上,如果并发度高,则平摊到这些Cell上。最后取值,使Cell和base进行sun操作即可。 在sum中并没有对cells[]数组进行加锁,即不保证强一致性,只保证最终一致性。 所以,LongAddr适合高并发的统计场景,而不适合要对某个long型进行严格同步的场景。 -
伪共享与缓存填充
变量在同一个缓存行(cache line)中,缓存一致性协议,cpu1其中一个变量x修改,会导致cpu2中的同一个变量x失效,该缓存行失效,从而导致该缓存行中的其它未被修改的两cpu共享的变量失效,导致伪共享问题。 原因是x,y,z处在了同一个缓存行中。 解决:进行缓存行填充,即一个变量一个缓存行,在64bit的机器上一个缓存行是64byte,即假设声明一个long,还需要填充7个long类型的无用变量,进行缓存行填充,是为了不让cell[]数组中相邻的元素落到同一个缓存行里。 jdk8提供了@sun.misc.Contended声明进行填充缓存行。 -
LongAdder核心实现
最核心的实现是add方法。当一个线程调用add方法的时候,会首先尝试加在base变量上,不成功则分配到cell元素,再不成功则会调用longAccumulate方法 -
LongAccumulator
LongAdder只能进行累加操作,并且初始值默认为0,LongAccumulator可以自己定义一个二元操作符。