大家好锯茄,我是小黑厢塘,一個(gè)在互聯(lián)網(wǎng)茍且偷生的農(nóng)民工。
在開(kāi)始講今天的內(nèi)容之前肌幽,先問(wèn)一個(gè)問(wèn)題晚碾,使用int類(lèi)型做加減操作是不是線(xiàn)程安全的呢?比如 i++ ,++i喂急,i=i+1這樣的操作在并發(fā)情況下是否會(huì)有問(wèn)題格嘁?
我們通過(guò)運(yùn)行代碼來(lái)看一下。
public class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
Thread a = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
System.out.println(Thread.currentThread().getName()+"_"+data.increment());
}
}, "A");
Thread b = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
System.out.println(Thread.currentThread().getName()+"_"+data.increment());
}
}, "B");
a.start();
b.start();
// 等待A廊移,B線(xiàn)程執(zhí)行完畢
a.join();
b.join();
System.out.println(data.getI());
}
}
class Data {
private volatile int i = 0;
public int increment() {
i++;
return i;
}
public int getI() {
return i;
}
}
以上代碼比較簡(jiǎn)單糕簿,通過(guò)A,B兩個(gè)線(xiàn)程同時(shí)對(duì)Data對(duì)象中的i執(zhí)行++操作涣易,各自執(zhí)行100000次,最后輸出冶伞,如果說(shuō)i++操作時(shí)線(xiàn)程安全的新症,那么最后輸出的結(jié)果應(yīng)該是200000,但是我們運(yùn)行代碼會(huì)看到如下結(jié)果:
我們發(fā)現(xiàn)最后輸出的并不是200000响禽,而是199982徒爹,如果多執(zhí)行幾次的話(huà),這個(gè)結(jié)果會(huì)發(fā)生變化芋类,并且大多數(shù)情況下不會(huì)是200000隆嗅。這主要是因?yàn)閕nt類(lèi)型的++操作不是原子的,i++同等于i=i+1,也就是加1這一步和對(duì)i重新賦值這一步不是同時(shí)完成的侯繁,不具備原子性胖喳,所以我們得出結(jié)論int類(lèi)型的操作不是線(xiàn)程安全的。
在很多實(shí)際場(chǎng)景中都需要對(duì)一個(gè)數(shù)據(jù)進(jìn)行并發(fā)操作贮竟,比如電商的秒殺活動(dòng)中丽焊,對(duì)一個(gè)商品數(shù)量的扣減,那么我們想保證安全性應(yīng)該怎么做呢咕别?
首先我們可以想到的就是使用synchronized關(guān)鍵字對(duì)increment()這個(gè)方法加鎖技健,這樣就能保證每次只有一個(gè)線(xiàn)程能訪(fǎng)問(wèn)。
但是之前的文章中我們有講到synchronized是一個(gè)重量級(jí)的悲觀(guān)鎖惰拱,我們的業(yè)務(wù)場(chǎng)景的并發(fā)可能是一段時(shí)間內(nèi)的雌贱,多數(shù)情況下可能并不會(huì)有很多競(jìng)爭(zhēng),所以有沒(méi)有更好的處理方式呢偿短,答案就是通過(guò)AtomicInteger欣孤。
AtomicInteger
AtomicInteger是java.util.concurrent.atomic包中的一個(gè)類(lèi)。我們看官方文檔對(duì)于這個(gè)包的描述昔逗,說(shuō)它是支持單個(gè)變量上的無(wú)鎖線(xiàn)程安全編程的工具包降传,好像和我們期望的一樣,在不加鎖的情況下達(dá)到線(xiàn)程安全纤子。
我們來(lái)修改一下上面例子的代碼搬瑰。
class Data {
private volatile AtomicInteger i = new AtomicInteger(0);
public int increment() {
return i.incrementAndGet();
}
public int getI() {
return i.get();
}
}
很簡(jiǎn)單,將原來(lái)的int修改為AtomicInteger控硼,在執(zhí)行increment()方法進(jìn)行增加操作時(shí),調(diào)用incrementAndGet()方法就可以了艾少。同樣我們運(yùn)行代碼卡乾,會(huì)發(fā)現(xiàn),不管運(yùn)行多少次缚够,代碼最后執(zhí)行的結(jié)果都是一樣的幔妨,200000鹦赎。所以我們說(shuō)AtomicInteger是線(xiàn)程安全的。除了incrementAndGet()方法以外误堡,還有很多其他的操作古话,比如decrementAndGet(),getAndIncrement()锁施,getAndDecrement()陪踩,getAndAdd(int delta),addAndGet(int delta)等等悉抵,實(shí)際上就是對(duì)i++肩狂,++i,i=i+n姥饰,i+=n這些操作的原子實(shí)現(xiàn)傻谁。
除了AtomicInteger以外,java.util.concurrent.atomic包中還有一些其他類(lèi)型列粪,比如AtomicBoolean审磁,AtomicLong等。
實(shí)現(xiàn)原理
那么AtomicInteger是如何實(shí)現(xiàn)在不使用synchronized的情況下保證原子性的呢岂座?我們來(lái)看一下源碼力图。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value在內(nèi)存中的地址偏移值
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// value為volatile的保證內(nèi)存可見(jiàn)性
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
public final class Unsafe {
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 獲取volatile的Int,保證拿到的值是最新的
var5 = this.getIntVolatile(var1, var2);
// compareAndSwapInt 比較并交換 native方法
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
}
通過(guò)源碼我們看到在incrementAndGet()方法中調(diào)用了Unsafe類(lèi)的getAndAddInt方法掺逼,在這個(gè)方法內(nèi)部對(duì)value進(jìn)行compareAndSwapInt操作吃媒。通過(guò)這個(gè)方法名我們就可以看出是比較并交換,也就是我們之前提到過(guò)的CAS吕喘。也就是在執(zhí)行賦值操作時(shí)赘那,先看一下當(dāng)前值是不是我加之前的值,如果不是氯质,那我就重新加一次之后再進(jìn)行比較募舟,是一個(gè)循環(huán)的過(guò)程,這個(gè)過(guò)程也稱(chēng)作自旋闻察。
CAS這種處理方式雖然很高效的解決了原子操作拱礁,但是它仍然存在三個(gè)問(wèn)題,在實(shí)際開(kāi)發(fā)中一定要注意辕漂,結(jié)合自己的實(shí)際業(yè)務(wù)場(chǎng)景使用呢灶。
ABA問(wèn)題
什么是ABA問(wèn)題呢,通俗理解钉嘹,就是你大爺還是你大爺鸯乃,你大媽已經(jīng)不是你大媽了~
什么意思呢?就是當(dāng)線(xiàn)程1取到A之后跋涣,有另一個(gè)線(xiàn)程2把A變成了B缨睡,又變成了A鸟悴,當(dāng)線(xiàn)程1再修改完值進(jìn)行CAS比較時(shí),發(fā)現(xiàn)值還是A奖年,和自己取到的一樣细诸,就直接更新了,但是在這個(gè)過(guò)程中陋守,這個(gè)A中間是發(fā)生過(guò)變化的震贵。就好比一個(gè)小偷,偷了別人家錢(qián)然后再還回來(lái)嗅义,還是原來(lái)的錢(qián)嗎屏歹?雖然你的錢(qián)沒(méi)變,但是這個(gè)小偷已經(jīng)觸犯了法律之碗,而你自己還不知道蝙眶。
為了解決這個(gè)問(wèn)題,atomic包中提供了一個(gè)類(lèi)褪那,我們看下是如何解決的幽纷。
public static void main(String[] args) throws InterruptedException {
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
new Thread(() -> {
try {
int stamp = ref.getStamp();
String reference = ref.getReference();
System.out.println("線(xiàn)程1拿到的值:" + reference + " stamp:" + stamp);
// sleep 2秒模擬線(xiàn)程切換到2
TimeUnit.SECONDS.sleep(2);
boolean success = ref.compareAndSet(reference, "C", stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " " + success);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "線(xiàn)程1").start();
new Thread(() -> {
// 先改為B
int stamp = ref.getStamp();
String reference = ref.getReference();
System.out.println("線(xiàn)程2拿到的值:" + reference + " stamp:" + stamp);
ref.compareAndSet(reference, "B", stamp, stamp + 1);
// 再改回A
stamp = ref.getStamp();
reference = ref.getReference();
System.out.println("線(xiàn)程2拿到的值:" + reference + " stamp:" + stamp);
ref.compareAndSet(reference, "A", ref.getStamp(), stamp + 1);
}, "線(xiàn)程2").start();
}
我們可以看到AtomicStampedReference的compareAndSet()方法有4個(gè)參數(shù):
- expectedReference:表示期望的引用值
- newReference:表示要修改后的新引用值
- expectedStamp:表示期望的戳(版本號(hào))
- newStamp:表示修改后新的戳(版本號(hào))
什么意思呢?就是在修改時(shí)不光比較值是不是和獲取到的一樣博敬,還要比較版本號(hào)友浸。這樣的話(huà),每次操作時(shí)都對(duì)版本號(hào)加1偏窝,那么就算值從A改為B再改回A收恢,但是版本號(hào)從0改成了1又改成了2,并沒(méi)有變回0祭往,就可以避免ABA問(wèn)題的發(fā)生伦意。
循環(huán)時(shí)間變長(zhǎng)
在并發(fā)非常大的情況下,使用CAS可能會(huì)存在一些線(xiàn)程一直循環(huán)修改不成功硼补,導(dǎo)致循環(huán)時(shí)間變長(zhǎng)驮肉,會(huì)給CPU帶來(lái)很大的執(zhí)行開(kāi)銷(xiāo)。并且由于A(yíng)tomicReference中的引用是volatile的已骇,為了保證內(nèi)存可見(jiàn)性离钝,需要保證緩存一致性,通過(guò)總線(xiàn)傳輸數(shù)據(jù)褪储,當(dāng)有大量的CAS循環(huán)時(shí)卵渴,會(huì)產(chǎn)生總線(xiàn)風(fēng)暴。
只能保證一個(gè)變量的原子操作
CAS的第三個(gè)問(wèn)題就是AtomicReference中只能存放一個(gè)變量乱豆,如果需要保證多個(gè)變量操作的原子性奖恰,是做不到的。對(duì)于這種情況只能使用synchronized或者juc包中的Lock工具宛裕。
小結(jié)
簡(jiǎn)單做個(gè)小結(jié)瑟啃,使用int類(lèi)型在并發(fā)場(chǎng)景下存在線(xiàn)程安全問(wèn)題,可以用AtomicInteger來(lái)保證原子性操作揩尸,Atomic是通過(guò)CAS做到無(wú)鎖線(xiàn)程安全的蛹屿。但是CAS有三個(gè)問(wèn)題,第一ABA問(wèn)題岩榆,可以通過(guò)AtomicStampedReference解決错负;第二競(jìng)爭(zhēng)激烈情況下循環(huán)時(shí)間會(huì)變長(zhǎng),會(huì)產(chǎn)生總線(xiàn)風(fēng)暴勇边;第三只能保證一個(gè)變量的原子操作犹撒。
具體業(yè)務(wù)場(chǎng)景中是使用synchronized,Lock等鎖工具還是使用Atomic的CAS無(wú)鎖操作粒褒,還是要結(jié)合場(chǎng)景考慮识颊。
好的,今天的內(nèi)容就到這里奕坟,我們下期見(jiàn)祥款。