樂觀鎖與悲觀鎖
synchronized是悲觀鎖:
每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖。
CAS操做的就是樂觀鎖(樂觀鎖在Java中的使用,是無鎖編程,經常採用的是CAS算法,典型的例子就是原子類):
每次不加鎖而是假設沒有衝突而去完成某項操做,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。
若是由於衝突失敗就重試,直到成功爲止。
悲觀鎖適合寫操做很是多的場景,樂觀鎖適合讀操做很是多的場景
CAS機制(Compare And Swap)
CAS是一種系統原語,原語屬於操做系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程。
而且原語的執行必須是連續的,在執行過程當中不容許被中斷,也就是說CAS是一條CPU的原子指令,不會形成所謂的數據不一致問題
Java中CAS操做的執行依賴於Unsafe類的方法,Unsafe類中的方法都直接調用操做系統底層資源執行相應任務
//Unsafe類中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
getAndAddInt()經過一個while循環不斷的重試更新要設置的值,直到成功爲止,底層調用本地方法:
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
CAS機制當中使用了3個基本操做數:
內存地址V(主內存中存放的V值,全部線程共享)
舊的預期值A(線程上次從內存中讀取的V值A存放在線程的幀棧中,每一個線程私有)
要修改的新值B(須要寫入內存中並改寫V值的B值)
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,
纔會將內存地址V對應的值修改成B(防止多線程併發問題)。
CAS的缺點:
1.自旋時間過長
使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(無非就是一個死循環)進行下一次嘗試,若是這裏自旋時間過長對性能是很大的消耗。
在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。
2.不能保證代碼塊的原子性
CAS機制所保證的只是一個變量的原子性操做,而不能保證整個代碼塊的原子性。
好比須要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。
3.ABA問題(解決:AtomicStampedReference)
CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,
可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。
ABA問題的解決思路就是使用版本號:
在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
public class AtomicStampedReference<V> {
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);
}
}
private volatile Pair<V> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
public V getReference() {
return pair.reference;
}
public int getStamp() {
return pair.stamp;
}
}
基於CAS實現的原子操做基本類型與數組類型
java.util.concurrent.atomic:該包中提供了許多基於CAS實現的原子操做類
AtomicBoolean,AtomicInteger,AtomicLong等
incrementAndGet() :
以原子的方式將實例中的原值進行加1操做,並返回最終相加後的結果;
getAndIncrement():
以原子的方式將實例中的原值加1,返回的是自增前的舊值;
getAndSet(int newValue):
將實例中的值更新爲新值,並返回舊值;
addAndGet(int delta) :
以原子方式將輸入的數值與實例中本來的值相加,並返回最後的結果;
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray(原子更新引用類型數組中的元素)
addAndGet(int i, int delta):
以原子更新的方式將數組中索引爲i的元素與輸入值相加;
getAndIncrement(int i):
以原子更新的方式將數組中索引爲i的元素自增長1;
compareAndSet(int i, int expect, int update):
將數組中索引爲i的位置的元素進行更新
原子引用(AtomicReference
:提供了引用變量的讀寫原子性操做)
原理:CAS + Unsafe
賦值操做不是線程安全的。若想不用鎖來實現,能夠用AtomicReference<V>這個類,實現對象引用的原子更新。
AtomicReference則對應普通的對象引用,底層使用的是compareAndSwapObject實現CAS:
比較的是兩個對象的地址是否相等(線程在操做value時不會被中斷)
private static AtomicReference<User> reference = new AtomicReference<>();
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user); //a,1
System.out.println(reference.get()); //b,2
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
// 獲取Unsafe對象,Unsafe的做用是提供CAS操做
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// volatile類型
private volatile V value;
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final V getAndSet(V newValue) {
while (true) {
V x = get();
if (compareAndSet(x, newValue))
return x;
}
}
}
Java實現自旋鎖(非公平鎖)
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
//上鎖
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
//解鎖
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
優勢:
自旋鎖不會使線程狀態發生切換,一直處於用戶態,即線程一直都是active的;
不會使線程進入阻塞狀態,減小了沒必要要的上下文切換,執行速度快
非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入內核態,當獲取到鎖的時候須要從內核態恢復,須要線程上下文切換。