本次內(nèi)容主要講原子操作的概念、原子操作的實(shí)現(xiàn)方式、CAS的使用、原理、3大問題及其解決方案,最后還講到了JDK中經(jīng)常使用到的原子操作類。
?
1、什么是原子操作?
? 所謂原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作,這種操作一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程上下文切換。原子操作可以是一個(gè)步驟,也可以是多個(gè)操作步驟,但是其順序不可以被打亂,也不可以被切割而只執(zhí)行其中的一部分。我們常用的i++看起來(lái)雖然簡(jiǎn)單,但這并不是一個(gè)原子操作,具體原理后面單獨(dú)介紹。
假定有兩個(gè)操作A和B,如果從執(zhí)行A的線程來(lái)看,當(dāng)另一個(gè)線程執(zhí)行B時(shí),要么將B全部執(zhí)行完,要么完全不執(zhí)行B,那么A和B對(duì)彼此來(lái)說(shuō)是原子的。
將整個(gè)操作視作一個(gè)整體是原子性的核心特征。
2、如何實(shí)現(xiàn)原子操作?
2.1 鎖機(jī)制實(shí)現(xiàn)原子操作及其問題
實(shí)現(xiàn)原子操作可以使用鎖。鎖機(jī)制滿足基本的需求是沒有問題的,但是有的時(shí)候我們的需求并非這么簡(jiǎn)單,我們需要更有效,更加靈活的機(jī)制。synchronized
關(guān)鍵字是基于阻塞的鎖機(jī)制,也就是說(shuō)當(dāng)一個(gè)線程擁有鎖的時(shí)候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖。使用synchronized關(guān)鍵字存在這樣的問題:
(1)如果被阻塞的線程優(yōu)先級(jí)很高很重要怎么辦?
(2)如果獲得鎖的線程一直不釋放鎖怎么辦?
(3)如果有大量的線程來(lái)競(jìng)爭(zhēng)資源,那CPU將會(huì)花費(fèi)大量的時(shí)間和資源來(lái)處理這些競(jìng)爭(zhēng),同時(shí),還有可能出現(xiàn)一些例如死鎖之類的情況。
使用鎖機(jī)制是一種比較粗糙、粒度比較大的機(jī)制,我們可以想象多個(gè)線程操作同一個(gè)計(jì)數(shù)器的業(yè)務(wù)場(chǎng)景,使用鎖機(jī)制的話顯得太過笨重。
2.2 CAS機(jī)制
實(shí)現(xiàn)原子操作還可以使用當(dāng)前的處理器基本都支持CAS(Compare And Swap)
的指令,CPU指令集上提供了CAS操作相關(guān)指令,實(shí)現(xiàn)原子操作可以使用這些指令。每一個(gè)CAS操作過程都包含3個(gè)運(yùn)算參數(shù):一個(gè)內(nèi)存地址V,一個(gè)期望的值A(chǔ)和一個(gè)新值
B,操作的時(shí)候如果這個(gè)地址上存放的值等于這個(gè)期望的值A(chǔ),則將地址上的值賦為新值B,否則不做任何操作。
2.3 CAS使用
先來(lái)模擬一個(gè)多個(gè)線程操作同一個(gè)計(jì)數(shù)器的場(chǎng)景,JDK中提供了boolean、int和long基本類型對(duì)應(yīng)的原子包裝類AtomicBoolean、
AtomicInteger和AtomicLong。我們用AtomicInteger演示,通過
CountDownLatch進(jìn)行并發(fā)模擬,如果對(duì)CountDownLatch用法不了解,歡迎查看上一篇文章,有通俗易懂的例子。先對(duì)AtomicInteger的主要API做一個(gè)介紹:
(1)int addAndGet(int delta):以原子方式將輸入的數(shù)值與實(shí)例中的值(AtomicInteger里的value)相加,并返回結(jié)果。
(2)boolean compareAndSet(int expect,int
update):如果當(dāng)前數(shù)值等于expect,則以原子方式將當(dāng)前值設(shè)置為update。
(3)int getAndIncrement():以原子方式將當(dāng)前值加1,注意,這里返回的是自增前的值。
(4)int getAndSet(int newValue):以原子方式設(shè)置為newValue的值,并返回舊值。
import java.util.concurrent.CountDownLatch; import
java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerDemo {
static AtomicInteger counter = new AtomicInteger(0); static CountDownLatch
countDownLatch =new CountDownLatch(20); static class CounterThread implements
Runnable { @Overridepublic void run() { try { countDownLatch.await(); } catch
(InterruptedException e) { e.printStackTrace(); } counter.getAndIncrement(); } }
public static void main(String[] args) throws InterruptedException { for (int i
= 0; i < 20; i++) { Runnable thread = new CounterThread(); new
Thread(thread).start(); countDownLatch.countDown(); } Thread.sleep(2000); //
保證子線程全部執(zhí)行完成 System.out.println("20個(gè)線程并發(fā)執(zhí)行g(shù)etAndIncrement()方法后的結(jié)果:" +
counter.get()); counter.compareAndSet(20, 18);//如果counter當(dāng)前數(shù)值為20,則以原子方式更新為18
System.out.println("compareAndSet(20, 18)后的結(jié)果:" + counter.get()); } }
程序中模擬了20個(gè)線程并發(fā)對(duì)一個(gè)計(jì)數(shù)器進(jìn)行自增操作,結(jié)果輸出為20,可以看到這段代碼并沒有用任何的鎖,也達(dá)到了原子操作目的。
2.4 CAS原理
CAS的基本思路就是,如果內(nèi)存地址V上的值和期望的值A(chǔ)相等,則給其賦予新值B,否則不做任何事兒。CAS就是在一個(gè)循環(huán)里不斷的做CAS操作,直到成功為止。
CAS是怎么實(shí)現(xiàn)線程的安全呢?語(yǔ)言層面不做處理,JDK 調(diào)用這些指令來(lái)完成CAS操作,本質(zhì)上就是將其交給CPU和內(nèi)存,利用CPU
的多處理能力,實(shí)現(xiàn)硬件層面的阻塞,再加上volatile變量的特性即可實(shí)現(xiàn)基于原子操作的線程安全。用一張圖來(lái)說(shuō)明。
3、CAS實(shí)現(xiàn)原子操作的三大問題
?3.1 ABA問題
因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來(lái)是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實(shí)際上卻變化了。舉個(gè)通俗易懂的例子,我的同事老王今年35歲了,還沒有女朋友,我問他有什么要求,給他介紹一個(gè)女朋友。老王就說(shuō)了,只要是沒有結(jié)婚、35歲以下的女的就行。于是我就給他介紹了一個(gè)28歲,剛剛離婚不久的女同志,他還感謝了我好久,可能是他現(xiàn)在都還不知道他這個(gè)女朋友離過婚。這就是典型的ABA問題,只關(guān)心當(dāng)前狀態(tài),而不管中間經(jīng)歷了什么。ABA問題的解決思路就是使用版本號(hào)。給變量追加一個(gè)版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1,那么A→B→A就會(huì)變成1A→2B→3A。就好比老王的要求改成:35歲以下,沒有結(jié)婚并且離婚次數(shù)為0的女性,就不會(huì)發(fā)生剛剛的事情了。
?3.2?循環(huán)時(shí)間長(zhǎng)開銷大。
? CAS自旋如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來(lái)非常大的執(zhí)行開銷。
3.3?只能保證一個(gè)共享變量的原子操作
當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來(lái)保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。怎么解決這個(gè)問題呢?
從Java 1.5開始,JDK提供了AtomicReference類來(lái)保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行CAS操作。
4、JDK中相關(guān)原子操作類
4.1?AtomicReference
AtomicReference,可以原子更新的對(duì)象引用。AtomicReference有一個(gè)compareAndSet()方法,它可以將已持有引用與預(yù)期引用進(jìn)行比較,如果它們相等,則在AtomicReference對(duì)象內(nèi)設(shè)置一個(gè)新的引用。看一段代碼:
import java.util.concurrent.atomic.AtomicReference; public class
AtomicReferenceDemo {static AtomicReference<UserInfo> atomicReference; public
static void main(String[] args) { //原引用 UserInfo oldUser = new UserInfo("老王", 35
); atomicReference= new AtomicReference<>(oldUser); //新引用 UserInfo updateUser =
new UserInfo("小宋", 21); atomicReference.compareAndSet(oldUser, updateUser);
System.out.println("使用compareAndSet()替換原有引用后的結(jié)果:" + atomicReference.get());
System.out.println("原引用:" + oldUser); } static class UserInfo { private String
name;private int age; public UserInfo(String name, int age) { this.name = name;
this.age = age; } public String getName() { return name; } public int getAge() {
return age; } public void setName(String name) { this.name = name; } public void
setAge(int age) { this.age = age; } @Override public String toString() { return
"UserInfo{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
從程序輸出可以看到,atomicReference的持有的引用被修改了,但是原引用對(duì)象并沒有發(fā)生改變。
?
4.2?AtomicStampedReference
? AtomicStampedReference,利用版本戳的形式記錄了每次改變以后的版本號(hào),這樣的話就不會(huì)存在ABA問題了。
AtomicStampedReference有一個(gè)內(nèi)部類Pair,使用Pair的int stamp作為計(jì)數(shù)器使用,看下Pair的源碼:
private static class Pair<T> { final T reference; final int stamp; private
Pair(T reference,int stamp) { this.reference = reference; this.stamp = stamp; }
static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference,
stamp); } }
還是老王那個(gè)例子,如果使用AtomicStampedReference的話,老王更關(guān)心的是介紹的女朋友離過幾次婚。用一段代碼來(lái)模擬給老王介紹女朋友的場(chǎng)景:
import java.util.concurrent.atomic.AtomicStampedReference; public class
AtomicStampedReferenceDemo {static AtomicStampedReference<String> asr = new
AtomicStampedReference("介紹的女朋友", 0); public static void main(String[] args)
throws InterruptedException { final String oldReference = asr.getReference();//
初始值,表示介紹的女朋友 final int oldStamp = asr.getStamp();//初始版本0,表示介紹的女朋友沒有離過婚 Thread
thread1= new Thread(() -> { String newReference = oldReference + " 離婚1次";
boolean first = asr.compareAndSet(oldReference, newReference, oldStamp, oldStamp
+ 1); if (first) { System.out.println("介紹的女朋友第一次離婚。。。"); } boolean second =
asr.compareAndSet(newReference, oldReference + "又離婚了", oldStamp + 1, oldStamp +
2); if (second) { System.out.println("介紹的女朋友第二次離婚。。。"); } }, "介紹的女朋友離婚");
Thread thread2= new Thread(() -> { String reference = asr.getReference();//
介紹的女朋友最新狀態(tài)//判斷介紹的女朋友最新狀態(tài)是否符合老王的要求 boolean flag = asr.compareAndSet(reference,
reference + "沒有離過婚", oldStamp, oldStamp + 1); if (flag) { System.out.println(
"老王笑嘻嘻地對(duì)我說(shuō),介紹的女朋友符合我的要求"); } else { System.out.println("老王拳頭緊握地對(duì)我說(shuō),介紹的女朋友居然離過"
+ asr.getStamp() + "次婚,不符合我要求!?。?!"); } }, "老王相親"); thread1.start();
thread1.join(); thread2.start(); thread2.join(); } }
?啟動(dòng)2個(gè)子線程,分別代表介紹的女朋友多次離婚以及老王相親的場(chǎng)景。從程序輸出可以看到,介紹的女朋友不符合老王的要求,老王為了避免喜當(dāng)?shù)?,果斷拒絕了。
?老王判斷的依據(jù)是,介紹的女朋友應(yīng)該是沒有離過婚,stamp值等于0才對(duì)。但是老王仔細(xì)一看,stamp已經(jīng)是2,不符合我的要求,不能要。
4.3?AtomicMarkableReference
?
AtomicMarkableReference,可以原子更新一個(gè)布爾類型的標(biāo)記位和引用類型。構(gòu)造方法是AtomicMarkableReference(V
initialRef,booleaninitialMark)。AtomicMarkableReference也有一個(gè)內(nèi)部類Pair,使用Pair的boolean
?
mark來(lái)標(biāo)記狀態(tài)。還是老王那個(gè)例子,使用AtomicStampedReference可能關(guān)心的是離婚次數(shù),AtomicMarkableReference關(guān)心的是有沒有離過婚。用一段代碼來(lái)模擬:
import java.util.concurrent.atomic.AtomicMarkableReference; public class
AtomicMarkableReferenceDemo {static AtomicMarkableReference markableReference;
public static void main(String[] args) throws InterruptedException { String girl
= "介紹的女朋友"; markableReference = new AtomicMarkableReference(girl, false);
Thread t1= new Thread(() -> { markableReference.compareAndSet(girl, girl + "離婚",
false, true); System.out.println(markableReference.getReference()); },
"介紹的女朋友離婚了"); Thread t2 = new Thread(() -> { //老王檢查標(biāo)記,只關(guān)心這個(gè)標(biāo)志位 boolean marked =
markableReference.isMarked();if (marked) { System.out.println(
"你給我介紹的女朋友離過婚,我不要!!"); } else { System.out.println("兄弟,大兄弟,親生兄弟啊??!這個(gè)女朋友我要了"); }
},"老王鑒定介紹的女朋友有沒有離過婚"); t1.start(); t1.join(); t2.start(); t2.join(); } }
程序輸出可以看到,老王還是堅(jiān)持了自己的原則。
4.4?AtomicIntegerArray
? AtomicIntegerArray,元素可以原子更新的數(shù)組。其常用方法如下:
(1)int addAndGet(int i,int delta):以原子方式將輸入值與數(shù)組中索引i的元素相加。
(2)boolean compareAndSet(int i,int expect,int
update):如果當(dāng)前值等于預(yù)期值,則以原子方式將數(shù)組位置i的元素設(shè)置成update值。
需要注意的是,數(shù)組value通過構(gòu)造方法傳遞進(jìn)去,然后AtomicIntegerArray會(huì)將當(dāng)前數(shù)組復(fù)制一份,所以當(dāng)AtomicIntegerArray對(duì)內(nèi)部的數(shù)組元素進(jìn)行修改時(shí),不會(huì)影響傳入的數(shù)組。
用法比較簡(jiǎn)單,看一個(gè)例子:
public class AtomicIntegerArrayDemo { static int[] value = new int[]{1, 2};//
原始數(shù)組 static AtomicIntegerArray atomicIntegerArray = new
AtomicIntegerArray(value);public static void main(String[] args) {
atomicIntegerArray.getAndSet(0, 3); System.out.println(
"atomicIntegerArray的第一個(gè)元素:" + atomicIntegerArray.get(0)); System.out.println(
"原始數(shù)組的第一個(gè)元素:" + value[0]);//原數(shù)組不會(huì)變化 } }
程序輸出可以看到,原始數(shù)組并沒有受到影響。
?順便看一下AtomicIntegerArray的構(gòu)造方法:
public AtomicIntegerArray(int[] array) { // Visibility guaranteed by final
field guarantees this.array = array.clone(); }
?5、結(jié)語(yǔ)
文中例子純屬虛構(gòu),便于對(duì)知識(shí)點(diǎn)的理解,不摻雜任何其他意思。下一篇內(nèi)容中會(huì)介紹Java的顯示鎖Lock相關(guān)知識(shí)點(diǎn),閱讀過程中如發(fā)現(xiàn)描述有誤,請(qǐng)指出,謝謝。
?
?
熱門工具 換一換