在高并发场景下,count++ 是线程不安全的,如果要采用这种计数的方法,应使用 Atomic包提供的AtomicInteger类。
例子演示:
public class AtomicIntegerTest {
//初始化 线程总数
public static int clientTotal = 5000;
//初始化 同时执行的线程数
public static int threadTotal = 100;
//初始化 0
public 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++) {
//lamda 表达式 使用线程池
executorService.execute(() -> {
try {
semaphore.acquire();
//调用 +1 的方法
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
//关闭线程池
executorService.shutdown();
log.info("count:{}",count.get());
}
private static void add(){
//调用 先加 在 得到的方法
count.incrementAndGet();
}
}
下面对 AtomicInteger 的 incrementAndGet()方法的源码进行阅读:
看到 返回的是一个 unsafe对象的getAndAddInt(this,valueoffset,1)+1;
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
再对 getAndAddInt()这个方法的实现进行查看
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;
}
以 2+1为例子
这里 var1 是传回去的对象;var2 是要被操作的数的偏移量 offset;var4 则是要加的数,就是1。
方法中定义了一个var5 getIntVolatile() 是一个native本地方法 获取obj对象中offset偏移地址对应的整型field的值
然后调用 compareAndSwapInt() 比较obj的offset处内存位置中的值和期望的值,
如果相同则更新。此更新是不可中断的
obj(var1)需要更新的对象,
offset(var2)obj中整型field的偏移量;
expect(var 5) 希望field中存在的值;
update(var5+var4)如果期望值expect与field的当前值相同,设置filed的值为这个新值
下面总结引用该页面的话:https://www.erlo.vip/share/9/22976.html
首先介绍一下什么是Compare And Swap(CAS)?简单的说就是比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。
我们来分析下incrementAndGet的逻辑:
1.先获取当前的value值
2.调用compareAndSet方法来来进行原子更新操作,这个方法的语义是:
先检查当前value是否等于obj中整型field的偏移量处的值,如果相等,则意味着obj中整型field的偏移量处的值 没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。
第一次count 为0时线程A调用incrementAndGet时,传参为 var1=AtomicInteger(0),var2为var1 里面 0 的偏移量,比如为8090,var4为需要加的数值1,var5为线程工作内存值,do里面会先执行一次,通过getIntVolatile 获取obj对象中offset偏移地址对应的整型field的值此时var5=0;while 里面compareAndSwapInt 比较obj的8090处内存位置中的值和期望的值var5,如果相同则更新obj的值为(var5+var4=1),此时更新成功,返回true,则 while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));结束循环,return var5。
当count 为0时,线程B 和线程A 同时读取到 count ,进入到第 8 行代码处,线程B 也是取到的var5=0,当线程B 执行到compareAndSwapInt时,线程A已经执行完compareAndSwapInt,已经将内存地址为8090处的值修改为1,此时线程B 执行compareAndSwapInt返回false,则继续循环执行do里面的语句,再次取内存地址偏移量为8090处的值为1,再去执行compareAndSwapInt,更新obj的值为(var5+var4=2),返回为true,结束循环,return var5。
CAS的ABA问题
当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。