在Java中惧互,談到線程安全哎媚,首先想到的就是synchronized關(guān)鍵字。但是該同步方法性能低下是不爭(zhēng)的事實(shí)壹哺,在高并發(fā)的應(yīng)用中存在性能問題抄伍。于是艘刚,人們想出了CAS管宵,即Compare And Swap的縮寫。仰仗這一思路攀甚,實(shí)現(xiàn)了JUC中很多并發(fā)工具箩朴,搞清楚它的來龍去脈很有必要。本文初步介紹CAS以及其使用方法秋度。
CAS的概念
從其字面意思來看炸庞,就是比較再交換。那么構(gòu)成CAS機(jī)制的關(guān)鍵元素是什么荚斯?
CAS機(jī)制中使用了3個(gè)基本操作數(shù):
- 內(nèi)存地址V
- 內(nèi)存中存放的舊的預(yù)期值A(chǔ)
- 要修改的新值B
更新一個(gè)變量的時(shí)候埠居,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí),才會(huì)將內(nèi)存地址V對(duì)應(yīng)的值修改為B事期。與synchronized的悲觀鎖思路不同滥壕,這種思路是典型的樂觀鎖。該機(jī)制保證了并發(fā)安全兽泣,但是不能保證并發(fā)同步绎橘。
CAS的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
1.無(wú)鎖,永遠(yuǎn)不會(huì)死鎖唠倦,是一種輕量級(jí)的樂觀鎖称鳞。
缺點(diǎn)
1.會(huì)有ABA的問題,可以使用AtomicStampedReference解決稠鼻,本質(zhì)是增加一個(gè)版本信息(timestamp)
2.嘗試更新的自旋操作會(huì)不斷地消耗CPU資源
實(shí)現(xiàn)機(jī)制
Java中冈止,CAS依賴的是Unsafe提供的一系列原子操作。其中提供了很多的原子方法候齿。其中用的比較多的方法如下:
compareAndSet(oldValue, newValue)
通常是返回boolean靶瘸,嘗試設(shè)置一次,失敗返回false毛肋;成功返回true怨咪。如果newValue依賴oldValue,則需要調(diào)用該方法的地方實(shí)現(xiàn)自旋(不停的循環(huán)調(diào)用润匙,直到成功)诗眨,參見下節(jié)中的源碼。getAndSet(newValue)
內(nèi)部執(zhí)行自旋操作孕讳。即:循環(huán)查找最新的值匠楚,然后嘗試調(diào)用compareAndSet來設(shè)置newValue巍膘。下面是AtomicBoolean的方法代碼,一看便知芋簿。
public final boolean getAndSet(boolean newValue) {
boolean prev;
do {
prev = get();// 這里的值很可能在調(diào)用compareAndSet前被修改了峡懈。
} while (!compareAndSet(prev, newValue));
return prev;
}
CAS的使用場(chǎng)景
最常用的場(chǎng)景就是從V中讀取A,并根據(jù)A做計(jì)算得到B与斤,然后通過CAS以原子的方式把V中的A變成B肪康。下面是代碼示例。
public class AtomicReferenceMain {
static final AtomicReference<User> AI = new AtomicReference<User>(new User());
public static class WorkThread implements Runnable {
AtomicReference<User> ai;
public WorkThread(AtomicReference<User> ai) {
this.ai = ai;
}
public void run() {
int i = 0;
while (i < 10) {
// 標(biāo)志著CAS操作是否成功撩穿。
boolean res = false;
User newValue = null;
while (!res) {
// 自旋磷支。每次更新前都獲取最新值,直到更新成功食寡。
User a = AI.get();
// 創(chuàng)建新對(duì)象
newValue = new User();
newValue.setAge(a.getAge() + 1);// 在上一次值的基礎(chǔ)上+1
newValue.setName("name" + i);
// 嘗試設(shè)置新值雾狈,如果失敗,則重新嘗試抵皱。此操作成為自旋善榛。
// 如果執(zhí)行此方法的時(shí)候,有其他線程修改了引用呻畸,則compareAndSet更新失敗移盆,此時(shí)重試。
// 這里需要注意的是擂错,如果更新失敗味滞,始終要從AI中獲取最新的值,在這個(gè)基礎(chǔ)上再次嘗試更新钮呀。
// 這樣才能保證線程安全性剑鞍。否則運(yùn)行很多次,最終的age最大數(shù)是變化的爽醋。
res = AI.compareAndSet(a, newValue);
// 如果這里的新對(duì)象蚁署,跟之前的對(duì)象值(比如age)無(wú)關(guān),
// 那么可以簡(jiǎn)單的調(diào)用AI.getAndSet()方法蚂四,
// AI.getAndSet(newValue)會(huì)自動(dòng)自旋光戈。獲取AI當(dāng)前的最新值,然后把這個(gè)值作為老值遂赠,
// 用來更新newValue久妆。直到更新成功為止。
}
System.out.println(Thread.currentThread().getName() + ":User=" + newValue.toString());
i++;
}
}
}
public static void main(String[] args) {
List<Thread> as = new ArrayList<Thread>(2);
for (int i = 0; i < 2; i++) {
as.add(new Thread(new AtomicReferenceMain.WorkThread(AI)));
as.get(i).start();
}
}
}
為了避免過度的自旋跷睦,多線程的競(jìng)爭(zhēng)度越低越好筷弦。使用的時(shí)候需要注意這一點(diǎn)。