JUC包中的原子变量操作类
JUC包中的类是为解决高并发而设计的类,其中包括原子操作类,这些类的底层都是基于Unsafe类的方法实现的,JUC包中的原子操作类包括AtomicInteger、AtomicIong、AtomicBoolean等原子操作类,它们的功能和Integer、Long、Boolean类似,只是这些原子操作类的底层由Unsafe类的方法实现,是原子性操作,满足高并发的要求,在多线程下,使用这些原子操作类会比较可靠。这些原子操作类通过CAS实现在高并发下代替synchronize实现更高的效率,下面我们来具体地分析。
AtomicLong
先看一下AtomicLong的使用吧!JUC包是JavaEE下的并发包,如果使用的是eclipse需要切换JavaSE为JavaEE,然后建一个简单的web项目在里面做测试。在下面这测试中,我用测试了在多线程下普通long变量和原子long变量的区别。
public class Test {
private static AtomicLong atomicLong=new AtomicLong();
private static long number=0;
public static void main(String[] args)throws Exception{
Thread thread1=new Thread(){
public void run(){
for(int i=0;i<1200;i++){
atomicLong.incrementAndGet();
}
}
};
Thread thread2=new Thread(){
public void run(){
for(int i=0;i<1200;i++){
atomicLong.incrementAndGet();
}
}
};
Thread thread3=new Thread(){
public void run(){
for(int i=0;i<400;i++){
number+=3;
}
}
};
Thread thread4=new Thread(){
public void run(){
for(int i=0;i<400;i++){
number+=3;
}
}
};
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
System.out.println("原子操作:"+atomicLong.get());
System.out.println("普通long:"+number);
}
}

**为什么会少?**结果多次的测试下,原子操作的输出一直稳定在2400,在多线程下也可以稳点地计数,而普通long结果有很大的不确定性,一般会小于2400。原因很简单,这是在主存和缓冲之间读取结果不一致导致的,假设一个线程在主存中读到变量1,写到缓存中,然后另一个线程也去读这个还没有被更新的变量1到自己的缓存,他们都对它进行加1运算,结果都是把原来的1更新为2,所以会“少”。
**那么number用volatile关键字修饰会怎么样呢?**结果依然会出现“少”的现象。volatile关键字可以保证变量的可见性,但不能保证变量的原子性。可见性是指:线程要获取volatile关键字修饰的变量,会先清除本地工作内存的变量,然后从主存中读取最新值;修改后把写入工作内存的变量同步到主存。这个过程和synchronize关键字修饰的同步代码块的进入和离开环节很像,这可以保证变量的可见性,但是不能保证原子性,因为和synchronize关键字修饰的代码块在执行中别的线程不能进入不一样,volatile关键字修饰的变量在一个线程读取的时候别的线程还是可以去读取,依然会导致总数值变“少”。
为什么AtomicLong这么可靠呢?
来看看源码吧!
private static final long serialVersionUID = 1927816293512124184L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
private static native boolean VMSupportsCS8();
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile long value;
public AtomicLong(long initialValue) {
value = initialValue;
}
public AtomicLong() {
}
版本号:第一行是版本号的变量,这是与“乐观锁”有关,当线程读取某个变量值之后会先记录版本号,在修改计算之后,再拿expect(预期在那个偏移量处的值)和该偏移处的实际值比对,同时需要保证版本号没有被动过(如果之间数据被改过了,版本号就不一致了)一致则更新。
获取unsafe实例:原子操作的底层都是由unsafe的方法来实现的,他们都是对unsafe的功能扩展,unsafe提供原子操作。
偏移量:原子操作的重要参数。
判断JVM支持:判断JVM是否支持Long型无锁CAS。调用了native方法。
获取偏移地址:通过Class对象获取成员变量的偏移地址。value变量可以由构造方法指定,也可以默认为0。
获取原值并设置新值的方法:方法内实际调用的是unsafe类的getAndSetLong()方法。是原子操作。
public final long getAndSet(long newValue) {
return unsafe.getAndSetLong(this, valueOffset, newValue);
}
加一操作:底层也是unsafe。
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
读取value值:
public final long get() {
return value;
}
以下四个方法分别是返回原值并加一、返回原值并减一、加一并返回新值、减一并返回新值。
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
public final long getAndDecrement() {
return unsafe.getAndAddLong(this, valueOffset, -1L);
}
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
public final long decrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}
getAndIncrement()方法在JDK7中的实现逻辑:
如下代码实现了CAS算法的经典逻辑。
1、读取值。
2、对值进行运算。
3、比较并设置参数,返回原值。
4、如果设置失败则重试。
这段代码没有使用Unsafe类,而是自己实现了CAS算法,其中比较并设置是原子操作。
public final long getAndIncrement(){
while(true){
long current=get();
long next=current+1;
if(compareAndSet(current,next))
return current;
}
}
JDK8中原子操作都被封装到了Unsafe类中,底层变为调用Unsafe的方法。
LongAdder类
虽然AtomicLong可以在高并发下有稳定的性能,但是在同一时间依然只能有一个线程对它进行操作,那些不能成功操作变量的线程会不断地自旋尝试,白白耗费了CPU资源。LongAdder类可以解决这个问题,它的设计思路是把一个变量拆分为几个部分,可以分别由不同线程对它们进行操作,一般用一个cells数组来存储数据,其中cell变量的数量一般与CPU数量一致为最佳,计算sum的时候一般以base变量为基础进行累加。接下来为大家解释几个问题,就足够让大家了解这个类了。
当前线程应该访问Cells数组中的那个元素,由什么来决定?
如下是add的方法:
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
如果数组不为null则执行下面的代码,如果数组为null就在base的基础上进行累加。
getProbe() & m计算选择哪个cell,m为当前数组中cell的数量减一,getProbe()获取随机值,它在longAccumulate(x, null, uncontended);方法中初始化。a = as[getProbe() & m]方法从数组中取出cell然后对这个cell的值进行操作。
Cell的成员变量: volatile long value;
Cell的构造方法:Cell(long x) { value = x; }
如何初始化Cell数组?
如下代码实现了Cells数组的初始化。
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
先判断cellsBusy是否为0,如果是0则表示目前数组空闲,没有线程在对它执行初始化或者扩容。开始初始化后把cellsBusy标志设为1,init标志设为false,然后实例化Cells数组和Cell对象,开始数组的容量为2,实例化其中的一个Cell,其他为null。
如何扩容?
如下代码实现了扩容。
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
这段代码的进入条件依然是对标志判断并修改,修改cellsBusy是原子操作。 Cell[] rs = new Cell[n << 1];实例化一个大小是原来两倍的数组,并通过循环赋值,多余的Cell都是null,需要的时候再实例化。
扩容的触发条件是:
两个判断条件,collide是冲突标志,当发生冲突的时候它会被置为true,因此,只有当这两个else if中的条件都不满足的时候才会触发扩容,即当前Cells数组的元素个数小于CPU个数且发生了多线程访问Cell的冲突,就会扩容。
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if(){
..........
}
发生冲突如何处理?
一旦发生冲突,会重新计算随机值,访问别的Cell进行尝试,并在合适的实际触发扩容,减少冲突发生的概率。
如何保证Cell元素的原子性?
通过cas函数来保证原子性,底层是由硬件来保障的。
如何避免伪共享?
伪共享是指多个连续的数组元素因为连续存储,在内存中的连续缓存条中,导致读取一个元素的时候,处在同一缓存条中的别的元素不可以被其他线程访问的现象。为了提高效率,Cell元素采用sum.misc.Contended注解进行修饰,使得缓存条中只存放一个元素,剩下部分被填充,这提高了访问的效率。
求和的准确性:
对没和元素进行遍历,取出Cell值并累加它们的value值。这个过程不是线程安全的,可能在累加的过程中发送了扩容或者修改操作,结果是不确定的。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
LongAccumulator类
LongAccumulator类相比前者功能更加强大,它可以自定义累加的原则且可以指定初值,而LongAdder一般只能执行简单的累加操作不能指定初值。
如下代码对两者的功能做了简单的测试:
public class Test {
private LongAdder longAdder=new LongAdder();
//传入一个双目运算器实例和一个累加初值
private LongAccumulator longAccumulator=new LongAccumulator(new LongBinaryOperator() {
public long applyAsLong(long left, long right) {
return (left+1)*right;
}
}, 0);
public static void main(String[] args){
Test test=new Test();
test.longAdder.add(2);
test.longAdder.add(4);
System.out.println(test.longAdder.sum());//求和
System.out.println(test.longAdder.sumThenReset());//求和后清零
System.out.println(test.longAdder.longValue());//等价于sum()
test.longAccumulator.accumulate(20);
test.longAccumulator.accumulate(30);
System.out.println(test.longAccumulator.longValue());
}
}
输出:

LongAccumulator的初始化:传入一个运算器实例,和一个初值。运算器中可以自定义累加原则,left是上一次的计算结果,而right是下一次操作传入的数据,可以自主实现累加的方法。如下采用了对原值加一再乘上传入的值。
((0+1)*20+1)*30=630
private LongAccumulator longAccumulator=new LongAccumulator(new LongBinaryOperator() {
public long applyAsLong(long left, long right) {
return (left+1)*right;
}
}, 0);
本文介绍了Java并发包(JUC)中的AtomicLong类,探讨了其在高并发下的可靠性,通过源码分析揭示了其基于Unsafe类和CAS算法实现的原子性操作。此外,还对比分析了LongAdder类,它通过拆分变量以减少冲突,提高并发性能,并详细阐述了其内部结构和扩容机制。
24万+

被折叠的 条评论
为什么被折叠?



