Java 多線程 volitile 和 atomic
Java 多線程 volitile 和 atomic
volitile
關鍵字
public class MTester { public static class TestKey{ int x = 0; } public static TestKey key0 = new TestKey(); public static void main(String[] args) { Thread thread = new Thread(()->{ while (key0.x == 0){ } System.out.println("key0"+key0.x); }); Thread thread1 = new Thread(()->{ try { Thread.sleep(1000); key0.x=1; } catch (InterruptedException e) { e.printStackTrace(); }finally { System.out.println("over"); } }); thread.start(); thread1.start(); } }
嘗試運行以上代碼,發現thread永遠也無法發現key0的x被改變
所以這個時候需要加上volitile關鍵字
具體原因是java中每個線程都有工作內存,以及主存
我的理解就是不加volitile,線程讀寫變量是先在自己的工作內存中處理,然後再寫回主存,但是有的線程處理的是工作內存,但是並沒有從主存裏面讀取,加上volitile關鍵字之後,會通知其他線程,讓他們強制從主存中讀取數據
https://www.cnblogs.com/zhengbin/p/5654805.html
volatile還有一個特性:禁止指令重排序優化。
可以見這個文章
https://www.cnblogs.com/chengxiao/p/6528109.html
但是volitile只能保證可見性,不能保證原子性,也就是說如果多線程操作 i++還是無法保證正確
ExecutorService service = Executors.newCachedThreadPool(); for (int i = 0;i<10;i++){ service.submit(()->{ for (int j = 0;j <10;j++){ key0.x++; System.out.println(key0.x); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } service.awaitTermination(10, TimeUnit.SECONDS); System.out.println(key0.x);
還是無法保證原子性
這個時候可以考慮使用 java.util.concurrent.atomic;中的類
這些類裏面的類大部分都是使用CAS算法進行操作的
CAS compare and set
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
這個 unsafe是unsafe類,裏面的方法都是native方法
CAS其實就是期望的值進行比較,如果不相等,就證明有其他線程更改過了,然後不執行操作然後返回失敗,CAS看起來很麻煩,但是卻可以映射一些CPU指令,實際上執行起來還是很快的(參考java核心技術)
unsafe裏面的方法大部分都是native方法
比如說我們想要對AtomicInteger執行一個 increase操作,就先比較自己跟期望的值,如果不等,那就在下次循環接著嘗試更改,直到更改成功
//AtomicInteger.java
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
不過這種不斷嘗試比較,對CPU開銷還是比較大,不過相對於synchronized來說更輕量級,因為synchronized需要不斷嘗試獲取鎖釋放鎖,而且只能獨占
在並發量不是特別大的情況下,效率相對於synchronized還是很高的,當自選嚴重沖突的時候synchronized還是效率更高一些
CAS 算法屬於自旋
不過CAS算法也有其他的缺點,常見的就是ABA問題
舉個例子
線程1:從內存位置 1 取回A
線程2:從內存位置 1取到 A
線程2:做了一些操作
線程2:從內存位置 1 寫入A
線程1:發現位置1還是A CAS成功但是卻不知道線程2做了什麽操作,可能引發一些後果
解決辦法
AtomicStampedReference
可以用一個timestamp 或者mask來判斷是否有其他操作
自旋鎖的簡單實現:
思路:每次只有一個線程進入臨界區
import java.util.concurrent.atomic.AtomicReference;
public class MSpinLock {
AtomicReference<Thread> reference = new AtomicReference<>();
public void lock(){
do {
}while (!this.reference.compareAndSet(null,Thread.currentThread()));
}
public void unlock(){
do {
}while (!this.reference.compareAndSet(Thread.currentThread(),null));
}
public static void main(String[] args) {
MSpinLock lock = new MSpinLock();
Thread thread = new Thread(()->{
while (true){
try{
lock.lock();
System.out.println("thread had lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("thread will unlock");
lock.unlock();
}
}
});
Thread thread1 = new Thread(()->{
while (true){
try{
lock.lock();
System.out.println("thread1 had lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("thread1 will unlock");
lock.unlock();
}
}
});
thread.start();
thread1.start();
}
}
每次都是成對出現的
如果註釋掉lock
顯然不對
不過我們做的自選鎖不可重入
假如有個函數需要遞歸,那麽自旋鎖就會發生死鎖
所以我們需要一個Integer來判斷一下
public void lock(){
if(Thread.currentThread().equals(reference.get())){
atomicInteger.incrementAndGet();
return ;
}
do {
}while (!this.reference.compareAndSet(null,Thread.currentThread()));
atomicInteger.incrementAndGet();
}
public void unlock(){
if(Thread.currentThread().equals(reference.get())){
int n = atomicInteger.decrementAndGet();
if(n>0){
return;
}
}
do {
}while (!this.reference.compareAndSet(Thread.currentThread(),null));
}
這樣就可重入了
https://www.cnblogs.com/qjjazry/p/6581568.html
Java 多線程 volitile 和 atomic