JUC執行緒筆記(一)

永不想你發表於2020-10-27

1.JUC簡介

在 Java 5.0 提供了 java.util.concurrent (簡稱 JUC )包,在此包中增加了在併發程式設計中很常用 的實用工具類,用於定義類似於執行緒的自定義子 系統,包括執行緒池、非同步 IO 和輕量級任務框架。 提供可調的、靈活的執行緒池。還提供了設計用於多執行緒上下文中的 Collection 實現等。

2.volatile關鍵字

2.1 記憶體可見性

Java 記憶體模型規定,對於多個執行緒共享的變數,儲存在主記憶體當中,每個執行緒都有自己獨立的工作記憶體,並且執行緒只能訪問自己的工作記憶體,不可以訪問其它執行緒的工作記憶體。工作記憶體中儲存了主記憶體中共享變數的副本,執行緒要操作這些共享變數,只能通過操作工作記憶體中的副本來實現,操作完畢之後再同步回到主記憶體當中,其 JVM 模型大致如下圖。

在這裡插入圖片描述
JVM 模型規定:1) 執行緒對共享變數的所有操作必須在自己的記憶體中進行,不能直接從主記憶體中讀寫; 2) 不同執行緒之間無法直接訪問其它執行緒工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來完成。這樣的規定可能導致得到後果是:執行緒對共享變數的修改沒有即時更新到主記憶體,或者執行緒沒能夠即時將共享變數的最新值同步到工作記憶體中,從而使得執行緒在使用共享變數的值時,該值並不是最新的。這就引出了記憶體可見性。

記憶體可見性(Memory Visibility)是指當某個執行緒正在使用物件狀態,而另一個執行緒在同時修改該狀態,需要確保當一個執行緒修改了物件狀態後,其他執行緒能夠看到發生的狀態變化。

可見性錯誤 是指當讀操作與寫操作在不同的執行緒中執行時,我們無法確保執行讀操作的執行緒能適時地看到其他執行緒寫入的值,有時甚至是根本不可能的事情。

public class TestVolatile {
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();
		
		while(true){
			if(td.isFlag()){
				System.out.println("------------------");
				break;
			}
		}
		
	}
}

class ThreadDemo implements Runnable {

	private boolean flag = false;

	@Override
	public void run() {
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		flag = true;
		
		System.out.println("flag=" + isFlag());

	}
	public boolean isFlag() {
		return flag;
	}
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}
//輸出:
//flag=true

2.2 volatile 關鍵字

Java 提供了一種稍弱的同步機制,即 volatile 變數,用來確保將變數的更新操作通知到其它執行緒。當把共享變數宣告為 volatile 型別後,執行緒對該變數修改時會將該變數的值立即重新整理回主記憶體,同時會使其它執行緒中快取的該變數無效,從而其它執行緒在讀取該值時會從主內中重新讀取該值(參考快取一致性)。因此在讀取 volatile 型別的變數時總是會返回最新寫入的值。

volatile遮蔽掉了JVM中必要的程式碼優化(指令重排序),所以在效率上比較低

public class TestVolatile {
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo();
		new Thread(td).start();
		
		while(true){
			if(td.isFlag()){
				System.out.println("------------------");
				break;
			}
		}
		
	}
}

class ThreadDemo implements Runnable {

	private volatile boolean flag = false;

	@Override
	public void run() {
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}

		flag = true;
		
		System.out.println("flag=" + isFlag());

	}
	public boolean isFlag() {
		return flag;
	}
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
}
//輸出:
//flag=true
//------------------

volatile關鍵字最主要的作用是:
1.保證變數的記憶體可見性
2.區域性阻止重排序的發生

可以將 volatile 看做一個輕量級的鎖,但是又與 鎖有些不同:
1.對於多執行緒,不是一種互斥關係
2.不能保證變數狀態的“原子性操作“

3.1 原子變數

3.1.1 i++的原子性問題

public class TestAtomicDemo {
	public static void main(String[] args) {
		AtomicDemo ad = new AtomicDemo();
		for (int i = 0; i < 10; i++) {
			new Thread(ad).start();
		}
	}
}
class AtomicDemo implements Runnable{
	private volatile int serialNumber = 0;
//	private AtomicInteger serialNumber = new AtomicInteger(0);
	
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		System.out.print(getSerialNumber()+" ");
	}
	public int getSerialNumber(){
		return serialNumber++;
//		return serialNumber.getAndIncrement();
	}
}
//執行結果:
//0 4 3 2 1 0 5 6 7 8 ——> 會產生重複
//如果改為:
class AtomicDemo implements Runnable{
//	private volatile int serialNumber = 0;
	private AtomicInteger serialNumber = new AtomicInteger(0);
	
	@Override
	public void run() {
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
		}
		System.out.print(getSerialNumber()+" ");
	}
	public int getSerialNumber(){
		return serialNumber.getAndIncrement();
	}
}
//執行結果
//1 3 2 0 4 6 5 7 8 9 ——> 不會重複

3.1.2 原子變數

實現全域性自增id最簡單有效的方式是什麼?java.util.concurrent.atomic包定義了一些常見型別的原子變數。這些原子變數為我們提供了一種操作單一變數無鎖(lock-free)的執行緒安全(thread-safe)方式。

實際上該包下面的類為我們提供了類似volatile變數的特性,同時還提供了諸如boolean compareAndSet(expectedValue, updateValue)的功能。

不使用鎖實現執行緒安全聽起來似乎很不可思議,這其實是通過CPU的compare and swap指令實現的,由於硬體指令支援當然不需要加鎖了。

核心方法:boolean compareAndSet(expectedValue, updateValue)
原子變數類的命名類似於AtomicXxx,例如,AtomicInteger類用於表示一個int變數。

標量原子變數類
AtomicInteger,AtomicLong和AtomicBoolean類分別支援對原始資料型別int,long和boolean的操作。
當引用變數需要以原子方式更新時,AtomicReference類用於處理引用資料型別。

原子陣列類
有三個類稱為AtomicIntegerArray,AtomicLongArray和AtomicReferenceArray,它們表示一個int,long和引用型別的陣列,其元素可以進行原子性更新。

3.2 CAS演算法

Compare And Swap (Compare And Exchange) / 自旋 / 自旋鎖 / 無鎖
CAS 是一種硬體對併發的支援,針對多處理器操作而設計的處理器中的一種特殊指令,用於管理對共享資料的併發訪問。

CAS 是一種無鎖的非阻塞演算法的實現。

CAS 包含了 3 個運算元:
1.需要讀寫的記憶體值 V
2.進行比較的值 A
3.擬寫入的新值 B
當且僅當 V 的值等於 A 時,CAS 通過原子方式用新值 B 來更新 V的值,否則不會執行任何操作

在這裡插入圖片描述

3.2.1 ABA問題

CAS會導致ABA問題,執行緒1準備用CAS將變數的值由A替換為B,在此之前,執行緒2將變數的值由A替換為C,又由C替換為A,然後執行緒1執行CAS時發現變數的值仍然為A,所以CAS成功。但實際上這時的現場已經和最初不同了,儘管CAS成功,但可能存在潛藏的問題。

解決辦法(版本號 AtomicStampedReference),基礎型別簡單值不需要版本號

相關文章