无锁优化、自旋锁、乐观锁
首先要明确一点,自旋而”无锁”并不一定就比有锁快,因为自旋是在占用CPU,如果很多个线程一起自旋很久,想想都觉得效率很低…具体情况还是要具体分析.
概念 Compare And Swap/Set
下面是一段说明CAS原理的伪代码:
/**
* nowValue:当前值,这个值是随时可能被修改的
* expectedValue:期望值,在调用casMethod前获取到的nowValue
* newValue:想要修改的新的值
*/
casMethod(nowValue, expectedValue, newValue){
// 如果当前值和期望值相等,说明本次修改期间没有其他线程修改,则赋值
if(nowValue == expectedValue) {
// 问题:此时,已经判定为其他线程没有修改,那么在赋值前会不会被其他线程修改了?
// 答:不会,cas操作是CPU原语支持,是CPU指令指令级别上的支持,中间不能被打断
nowValue = newValue;
} else {
// 如果当前值和期望值不等,说明本次修改期间有其他线程已经修改了值,
// 那么就再试一次或者直接返回修改失败的结果
// todo try again or return fail
}
应用:AtomicInteger等atomic类
简单使用
下面这段代码,用了AtomicInteger后,自增部分(count++)不需要加同步锁,输出结果为100000
public class T01_AtomicInteger {
/*volatile*/ //int count1 = 0;
AtomicInteger count = new AtomicInteger(0);
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++){
//if count1.get() < 1000
count.incrementAndGet(); //count1++
}
}
public static void main(String[] args) {
T01_AtomicInteger t = new T01_AtomicInteger();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
简单的分析一波incrementAndGet()
跟踪下去,count.incrementAndGet():
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
继续跟踪,到了Unsafe.class,这里compareAndSwapInt就是用到了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;
}
Unsafe.class比较复杂,直接操作JVM中的内存,类似C和C++的操作,比如分配一个对象不用new,而是直接写在内存中;操作对象的属性也可以根据”地址”(or指针)和偏移量定位.
ABA问题
什么是ABA?
在开始使用CAS修改一个值后,在CAS中判断nowValue == expectedValue前,假设有一个线程先把nowValue改成了其他值x,然后又把x改回了nowValue,那么这时候虽然nowValue等于expectedValue,但这个值其实已经被修改过了.
ABA会带来什么问题?
如果是AtomicInteger等数值类型,其实ABA是没有影响的,无所谓.
如果是一个对象引用,多数情况是不允许ABA情况的(比如小黄和其女朋友小白要结婚了,但是突然来了小黑抢走了小白,他们成为恋人,过了段时间又把小白还了回来,这婚多半就结不成了).
怎么避免ABA?
加上版本号version,做任何操作时都把version+1,同时比较nowValue和version
假设nowValue值为A,version为1,如果有ABA情况发生,即nowValue值变为B后又变回A,那么此时version是3,就可以根据version知道值已经被修改过了.
例如java.util.concurrent.atomic.AtomicStampedReference
Atomic的常见问题
高并发实现递增的三种方式?
同步static long count2 = 0L;
synchronized (lock) {
count2++;
}
CAS原子操作AtomicLong count1 = new AtomicLong(0L);
count1.incrementAndGet();
分段锁LongAdder count3 = new LongAdder();
count3.increment();
下面代码是一个简单的测试(1000个线程,累加10W次),从中可以LongAdder最快,AtomicLong次之,synchronize最慢.
synchronize慢是因为会升级成重量级锁,向OS申请资源加锁.
注意:如果线程数较少,或者累加次数较少,LongAdder比AtomicLong慢.所以实际项目中,还是要看项目中的并发度如何.
public class T02_AtomicVsSyncVsLongAdder {
static long count2 = 0L;
static AtomicLong count1 = new AtomicLong(0L);
static LongAdder count3 = new LongAdder();
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int k = 0; k < 100000; k++) {
count1.incrementAndGet();
}
});
}
long start = System.currentTimeMillis();
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
long end = System.currentTimeMillis();
//TimeUnit.SECONDS.sleep(10);
System.out.println("Atomic: " + count1.get() + " time " + (end - start));
//-----------------------------------------------------------
Object lock = new Object();
for (int i = 0; i < threads.length; i++) {
threads[i] =
new Thread(new Runnable() {
@Override
public void run() {
for (int k = 0; k < 100000; k++) {
synchronized (lock) {
count2++;
}
}
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
end = System.currentTimeMillis();
System.out.println("Sync: " + count2 + " time " + (end - start));
//----------------------------------
for (int i = 0; i < threads.length; i++) {
threads[i] =
new Thread(() -> {
for (int k = 0; k < 100000; k++) {
count3.increment();
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
end = System.currentTimeMillis();
//TimeUnit.SECONDS.sleep(10);
System.out.println("LongAdder: " + count1.longValue() + " time " + (end - start));
}
}
为什么高并发下,LongAdder比AtomicLong快?
LongAdder内部实现类似”分段锁”(分段锁也是CAS操作),把值放在数组里,每个元素作为一个相对独立的部分,分散开线程的压力,最后再汇总起来.(有点类似于MapReduce思想)
参考
语雀:从CAS到手写原子类
https://www.yuque.com/humingming/gmztzt/pvgp83