CAS自旋鎖
是什么,干什么,解決了什么痛點(diǎn)?如何解決,如何使用。
原子類:java.util.concurrent.atomic
在沒有CAS之前,多線程環(huán)境不使用原子類保證線程安全i++等操作,會出現(xiàn)數(shù)據(jù)問題,如果直接加鎖synchronized,資源的開銷就比較大
在出現(xiàn)CAS之后,多線程環(huán)境,使用原子類保證線程安全i++,類似我們的樂觀鎖
CAS是什么
CAS是compare and swap的縮寫,中文翻譯為比較并交換,實(shí)現(xiàn)并發(fā)算法時常用的一種技術(shù)
CAS 包含三個操作數(shù) —— 內(nèi)存位置、預(yù)期原值及更新值
在執(zhí)行CAS操作的時候,將內(nèi)存位置的值與預(yù)期原值比較,
- 如果相匹配,那么處理器會自動將該位置值更新為新值
- 如果不匹配,處理器不做任何操作,多個線程同時執(zhí)行CAS只有一個會成功
CAS的原理
CAS 有三個操作數(shù),位置內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的更新值為B
當(dāng)且僅當(dāng)就得預(yù)期值A(chǔ)與內(nèi)存值V相同時,將內(nèi)存值V修改位B,否則什么都不做,重來——即自旋
這是通過硬件級別保證的
Unsafe 類
CAS是JDK提供的非阻塞原子性操作,它通過硬件保證了比較-更新的原子性。
它是非阻塞的且自身具有原子性,也就是說這玩意效率更高且通過硬件保證,說明這玩意更可靠。
CAS是一條CPU的原子指令(cmpxchg指令),不會造成所謂的數(shù)據(jù)不一致問題,Unsafe提供的CAS方法(如compareAndSwapXXX)底層實(shí)現(xiàn)即為CPU指令cmpxchg。
執(zhí)行cmpxchg指令的時候,會判斷當(dāng)前系統(tǒng)是否為多核系統(tǒng),如果是就給總線加鎖,只有一個線程會對總線加鎖成功,加鎖成功之后會執(zhí)行cas操作,也就是說CAS的原子性實(shí)際上是CPU實(shí)現(xiàn)獨(dú)占的,比起用synchronized重量級鎖,這里的排他時間要短很多,所以在多線程情況下性能會比較好。
進(jìn)入Unsafe方法查看源碼
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
/**
上面三個方法都是類似的,主要對4個參數(shù)做一下說明。
var1:表示要操作的對象
var2:表示要操作對象中屬性地址的偏移量
var4:表示需要修改數(shù)據(jù)的期望的值
var5/var6:表示需要修改為的新值
*/
1 Unsafe
是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng),需要通過本地〈native)方法來訪問,Unsafe相當(dāng)于一個后門,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)。Unsafe類存在于sun.misc包中,共內(nèi)部方法操作可以像C的指針一樣直接操作內(nèi)存,因?yàn)镴ava中CAS操作的執(zhí)行依賴于Unsafe類的方法。
注意Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)任務(wù)
2 變量valueOffset,表示該變量值在內(nèi)存中的偏移地址,因?yàn)閁nsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的。
// AtomicInteger 類
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe 類
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// volatile 修飾,一旦var5被修改會被立即獲知
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
3 變量value使用volatile修飾,保證了多線程之間的內(nèi)存可見性
假設(shè)線程A和線程B兩個線程同時執(zhí)行g(shù)etAndAddInt操作(分別跑在不同CPU上)
- AtomicInteger里面的value原始值為3,即主內(nèi)存中AtomicInteger的value為3,根據(jù)JMM模型,線程A和線程B各自持有一份值為3的value的副本分別到各自的工作內(nèi)存。
- 線程A通過getIntVolatile(var1, var2)拿到value值3,這時線程A被掛起。
- 線程B也通過getlntVolatile(var1, var2)方法獲取到value值3,此時剛好線程B沒有被掛起并執(zhí)行compareAndSwaplnt方法比較內(nèi)存值也為3,成功修改內(nèi)存值為4,線程B打完收工,一切OK。
- 這時線程A恢復(fù),執(zhí)行compareAndSwapInt方法比較,發(fā)現(xiàn)自己手里的值數(shù)字3和主內(nèi)存的值數(shù)字4不一致,說明該值已經(jīng)被其它線程搶先一步修改過了,那A線程本次修改失敗,只能重新讀取重新來一遍了。
- 線程A重新獲取value值,因?yàn)樽兞縱alue被volatile修飾,所以其它線程對它的修改,線程A總是能夠看到,線程A繼續(xù)執(zhí)行compareAndSwapInt進(jìn)行比較替換,直到成功。
原子引用AtomicReference
public class CASDemo {
public static void main(String[] args) {
AtomicReference<User> atomicReference = new AtomicReference<>();
User zhangsan = new User("zhangsan", 22);
User lisi = new User("lisi", 24);
atomicReference.set(zhangsan);
System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
}
}
CAS與自旋鎖
通過cas操作完成自旋鎖,A線程先進(jìn)來,調(diào)用lock方法自己持有鎖5秒;
B隨后進(jìn)來發(fā)現(xiàn)當(dāng)前線程支持有所,進(jìn)行自旋等待,直到A釋放鎖后B隨后搶到
public class CASDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println("==============="+Thread.currentThread().getName()+" come in ==============");
while (!atomicReference.compareAndSet(null,thread)) {}
}
public void unlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println("==============="+Thread.currentThread().getName()+" task is over ==============");
}
public static void main(String[] args) throws InterruptedException {
CASDemo casDemo = new CASDemo();
new Thread(()->{
casDemo.lock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
casDemo.unlock();
}
},"t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
casDemo.lock();
try {
TimeUnit.SECONDS.sleep(8);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
casDemo.unlock();
}
},"t2").start();
}
}
CAS的缺點(diǎn)
- 循環(huán)時間長開銷大
- 具有ABA問題
循環(huán)時間長開銷大
如果cas失敗,會一直進(jìn)行嘗試。如果cas長時間一直不成功,可能會給cpu帶來很大的開銷
ABA問題
CAS會導(dǎo)致“ABA問題”。
CAS算法實(shí)現(xiàn)一個重要前提需要取出內(nèi)存中某時刻的數(shù)據(jù)并在當(dāng)下時刻比較并替換,那么在這個時間差類會導(dǎo)致數(shù)據(jù)的變化。
比如說一個線程1從內(nèi)存位置V中取出A,這時候另一個線程2也從內(nèi)存中取出A,并且線程2進(jìn)行了一些操作將值變成了B,然后線程2又將V位置的數(shù)據(jù)變成A,這時候線程1進(jìn)行CAS操作發(fā)現(xiàn)內(nèi)存中仍然是A,預(yù)期OK,然后線程1操作成功。
盡管線程1的CAS操作成功,但是不代表這個過程就是沒有問題的。
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
private String name;
private int id;
}
public class ABADemo {
public static void main(String[] args) {
Book java = new Book("java",1);
Book mysql = new Book("mysql",2);
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
new Thread(()->{
// 初始條件是java
System.out.println(stampedReference.getReference()+"\t初始條件是java:" + stampedReference.getStamp());
// 此時郵戳莫有啟動,但是已經(jīng)被改為mysql了
stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
System.out.println(stampedReference.getReference()+"\t被改為mysql了" + stampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 修改回Java了
stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp());
System.out.println(stampedReference.getReference()+"\t修改回Java了:" + stampedReference.getStamp());
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 不知道被就該過了
stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
System.out.println(stampedReference.getReference()+"\t不知道被就該過了,此時還能改為mysql" + stampedReference.getStamp());
},"t2").start();
}
}
/**
Book(name=java, id=1) 初始條件是java:1
Book(name=mysql, id=2) 被改為mysql了1
Book(name=java, id=1) 修改回Java了:1
Book(name=mysql, id=2) 不知道被就該過了,此時還能改為mysql1
*/
解決:ABA
使用 AtomicStampedReference
內(nèi)容 | 版本 |
---|---|
A | 1 |
B | 2 |
A | 3 |
解決代碼文章來源:http://www.zghlxwxcb.cn/news/detail-443113.html
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
private String name;
private int id;
}
public class ABADemo {
public static void main(String[] args) {
Book java = new Book("java",1);
Book mysql = new Book("mysql",2);
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
boolean b;
// 如果是java,且郵戳不變,那就換成mysql,同時郵戳+1
b = stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
// 把 java 換回來
b = stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
}
}
上面演示了單線程的情況,下面演示多線程的cas情況文章來源地址http://www.zghlxwxcb.cn/news/detail-443113.html
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
private String name;
private int id;
}
public class ABADemo {
public static void main(String[] args) {
Book java = new Book("java",1);
Book mysql = new Book("mysql",2);
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t首次版本號:" + stampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 此時郵戳莫有啟動,但是已經(jīng)被改為mysql了
stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本號2:" + stampedReference.getStamp());
// 修改回Java了
stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本號3:" + stampedReference.getStamp());
},"t1").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t首次版本號:" + stampedReference.getStamp());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 被修改過了
boolean b = stampedReference.compareAndSet(java, mysql, stamp, stampedReference.getStamp()+1);
System.out.println(b+"\t"+stampedReference.getReference()+"\t" + stampedReference.getStamp());
},"t2").start();
}
}
/**
t1 首次版本號:1
t2 首次版本號:1
t1 Book(name=mysql, id=2) 版本號2:2
t1 Book(name=java, id=1) 版本號3:3
false Book(name=java, id=1) 3
*/
到了這里,關(guān)于JUC并發(fā)編程16 | CAS自旋鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!