Java Atomic总结
一、atomic使用
concurrent包下的atomic提供我们这么一种轻量级的数据同步的选择。
package com.example.concurrent.example.atomiccount;
import com.example.concurrent.annoations.NotThreadSafe;
import com.example.concurrent.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@ThreadSafe
public class AtomicConcurrentTest {
// 请求总数
private static int clientTotal = 5000;
// 线程总数
private static int threadTotal = 50;
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
log.info("count:{}", count.get());
}
private static void add() {
// count.getAndIncrement();
count.incrementAndGet();
}
}
// 运行结果都是5000
// 15:47:27.826 [main] INFO com.example.concurrent.example.atomiccount.AtomicConcurrentTest - count:5000
AtomicInteger是如何使用非阻塞算法来实现并发控制的。需要说明的是,如果使用 volatile int count; count++来实现,是错误的。
volatile可以保证可见性,但是无法保证原子性。
volatile修饰的变量count做++操作时,实际的指令包括三个(得到count的值,count+1,将count+1赋值给count),因此可能会出现多个线程交叉执行的结果。
二、atomic原理
以jdk 1.8 为例 AtomicInteger源码片段:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();//获取unsafe 引用,操作地址
private static final long valueOffset; //值的地址
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value; //真实村的值
/**
* getAndAddInt返回的是旧值
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* 旧值+1
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
其核心原理:比较并更新值,先获取当前地址的值var5, compareAndSwapInt要判断的就是var5作为期望的值,(var5+var4)作为更新值,如果期望值等于var5,就替换,否则什么都不做,自旋,直到CAS替换成功。
//Unsafe类,cas操作
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
三、优缺点
CAS相对于其他锁,不会进行内核态操作,有着一些性能的提升。但同时引入自旋,当锁竞争较大的时候,自旋次数会增多。cpu资源会消耗很高。
换句话说,CAS+自旋适合使用在低并发有同步数据的应用场景。
Java 8做出的改进和努力
在Java 8中引入了4个新的计数器类型,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator。他们都是继承于Striped64。
在LongAdder 与AtomicLong有什么区别?
Atomic*遇到的问题是,只能运用于低并发场景。因此LongAddr在这基础上引入了分段锁的概念。可以参考《JDK8系列之LongAdder解析》一起看看做了什么。
大概就是当竞争不激烈的时候,所有线程都是通过CAS对同一个变量(Base)进行修改,当竞争激烈的时候,会将根据当前线程哈希到对于Cell上进行修改(多段锁)。