一、什么是原子操作?如何實現(xiàn)原子操作?
CAS:Compare And Swap,比較并且交換。隸屬于樂觀鎖機制。
什么是原子操作?
假設現(xiàn)在有A,B兩個操作,如果某個線程執(zhí)行A操作,當另外一個線程執(zhí)行B操作的時候,要么這個B全部執(zhí)行完,要么這個B完全不執(zhí)行,那么對于A、B來講,他們彼此就是原子的。
在數(shù)據(jù)庫層面,這種操作就是事務操作,嚴格意義上來說事務操作也是屬于原子操作的一種。
如何實現(xiàn)原子操作
可以利用synchronize關鍵字,但是會引發(fā)一系列問題:
- 1.synchronize是阻塞式的,一個線程擁有鎖后,其他的線程必須等待
- 2.等待中的線程優(yōu)先級很高,但是遲遲拿不到鎖怎么辦?
- 3.等待中的線程競爭很激烈,但是拿到鎖的線程遲遲不釋放鎖怎么辦?
解決辦法CAS
CAS可以完美地解決上述的問題,進而更完美地實現(xiàn)原子操作,它利用了現(xiàn)代處理器都支持的CAS指令,這個指令是CPU級別的指令。
CAS包含的要素
1.內(nèi)存地址v:修改的對象或者變量的內(nèi)存地址
2.期望值A:
3.新值B
當我去改這個內(nèi)存地址上所對應的對象或者變量的時候,我期望在我改的時候,這個值是多少,如果是A,我就把他改成B,如果不是A,那我就不能改。將B值替換為A值。
即比較---->交換
。
用java語言來講,這個操作需要兩個語句,一個是比較,一個是交換。
而在CPU層面,只要你執(zhí)行了這個指令,我可以保證別的指令都被阻塞,只有這一個CAS指令操作完了才允許別的指令進行操作。
在JDK層面來講,用到了循環(huán)(自旋、死循環(huán)),直到成功為止,原理如下:
這種思想就是樂觀鎖。
用一句話來概括CAS如何實現(xiàn)線程安全?
CAS在語言層面不作處理,我們把它交給了CPU和內(nèi)存,利用CPU的能力實現(xiàn)硬件層面阻塞,進而實現(xiàn)CAS的線程安全。
二、CAS引起的問題
1.ABA問題
下面的兩種情況下會出現(xiàn)ABA問題。
1.A最開始的內(nèi)存地址是X,然后失效了,又分配了B,恰好內(nèi)存地址是X,這時候通過CAS操作,卻設置成功了
這種情況在帶有GC的語言中,這種情況是不可能發(fā)生的,為什么呢?拿JAVA舉例,在執(zhí)行CAS操作時,A,B對象肯定生命周期內(nèi),GC不可能將其釋放,那么A指向的內(nèi)存是不會被釋放的,B也就不可能分配到與A相同的內(nèi)存地址,CAS失敗。若在無GC的,A對象已經(jīng)被釋放了,那么B被分配了A的內(nèi)存,CAS成功。
2.線程1準備用CAS將變量的值由A替換為B,在此之前,線程2將變量的值由A替換為C,又由C替換為A,然后線程1執(zhí)行CAS時發(fā)現(xiàn)變量的值仍然為A,所以CAS成功。但實際上這時的現(xiàn)場已經(jīng)和最初不同了,盡管CAS成功,但可能存在潛藏的問題。比如:
現(xiàn)有一個用單向鏈表實現(xiàn)的堆棧,棧頂為A,這時線程T1已經(jīng)知道A.next為B,然后希望用CAS將棧頂替換為B:head.compareAndSet(A,B);在T1執(zhí)行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A。而對象B此時處于游離狀態(tài):此時輪到線程T1執(zhí)行CAS操作,檢測發(fā)現(xiàn)棧頂仍為A,所以CAS成功,棧頂變?yōu)锽,但實際上B.next為null,其中堆棧中只有B一個元素,C和D組成的鏈表不再存在于堆棧中,平白無故就把C、D丟掉了。
以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實現(xiàn)中通常都會用版本戳version來對記錄或對象標記,避免并發(fā)操作帶來的問題,在Java中,AtomicStampedReference<E>也實現(xiàn)了這個作用,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變量進行更新,AtomicInteger會成功執(zhí)行CAS操作,而加上版本戳的AtomicStampedReference對于ABA問題會執(zhí)行CAS失敗。
package concur.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
如何解決?
增加版本號,也就是說在每個變量前面都要加一個版本號,每次修改的時候都對其版本+1。其實在大多數(shù)開發(fā)過程中,我們是不關心ABA問題的。但是ABA問題在一線互聯(lián)網(wǎng)公司的面試中是經(jīng)常問到的。
- 1.ABA問題的解決思路是使用版本號,每次變量更新的時候版本號加1,那么A->B->A就會變成1A->2B->3A
- 2.從jdk1.5開始,jdk的Atomic包里就提供了兩個類來解決ABA問題,一個是
AtomicStampedReference
,另一個是AtomicMarkableReference
,AtomicStampedReference這個類中的compareAndSet方法的作用就是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值更新為指定的新值。
AtomicStampedReference
和AtomicMarkableReference
的區(qū)別
AtomicStampedReference帶了版本號,關心被修改過幾次,AtomicMarkableReference只關心有沒有人修改過。
2.開銷問題
自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。如果jvm能支持處理器提供的pause指令,那么效率會有一定的提升。pause指令有兩個作用:
第一,它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零。
第二,它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率。
3.只能保證一個變量的原子操作
當對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個方法,就是把多個共享變量合并成一個共享變量來操作。比如,有兩個共享變量i=2,j=a合并一下ij=2a,然后用CAS來操作ij。從java1.5開始,JDK提供了AtomicReference
類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。
三、原子操作類的使用
jdk中相關原子操作類的使用
- 更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong
- 更新數(shù)組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新引用類:AtomicReference,AtomicMarkableReference,AtomicStampeReference
- 原子更新字段類:AtomicReferenceFiledUpdater,AtomicIntegerFiledUpdater,AtomicLongFiledUpdater
舉例:
import java.util.concurrent.atomic.AtomicInteger;
/**
*類說明:演示基本類型的原子操作類
*/
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
//返回的是我自增以前的值
int i = ai.getAndIncrement(); // i++
//返回自增以后的值
int b = ai.incrementAndGet();// ++i
System.out.println(i +"------"+ b);
//ai.compareAndSet();
int fianl = ai.addAndGet(24);
System.out.println("加了24之后的值為:"+fianl);
}
}
運行結果:
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
*類說明: 演示原子操作數(shù)組
*/
public class AtomicArray {
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);//原數(shù)組不會變化
}
}
運行結果:
注意:
原子操作只會操作原子類的值,不會操作原數(shù)組,原子操作類的值再怎么變也不會影響原數(shù)組的值
運用原子操作類修改兩個變量的值
import java.util.concurrent.atomic.AtomicReference;
/**
*類說明:演示引用類型的原子操作類
*/
public class UseAtomicReference {
static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的實體的實例
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("Bill",17);
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get());
System.out.println(user);
}
//定義一個實體類
static class UserInfo {
private volatile 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;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
運行結果:
這是運用AtomicReference修改兩個變量的值,本質(zhì)上是包裝成一個變量,對這一個變量進行修改。