【java】樂觀鎖與悲觀鎖

一般來說,對於併發的場景,我們通常使用鎖來保證線程安全:

  • 悲觀鎖是一種悲觀的策略。它總是假設每一次的臨界區操作會產生衝突,因此,必須對每次操作都小心翼翼。如果有多個線程同時需要訪問臨界區資源,就寧可犧牲性能讓線程進行等待,所以說鎖會阻塞線程執行;
  • 樂觀鎖是一種樂觀的策略,它會假設對資源的訪問是沒有衝突的。既然沒有衝突,自然不需要等待,所以所有的線程都可以在不停頓的狀態下持續執行。樂觀鎖的策略使用一種叫做比較交換的技術(CAS Compare And Swap)來鑑別線程衝突,一旦檢測到衝突產生,就重試當前操作直到沒有衝突爲止。

很多時候,對共享資源的訪問主要是對某一數據結構的讀寫操作,如果數據結構本身就帶有排他性訪問的特性,也就相當於該數據結構自帶一個細粒度的鎖,對該數據結構的併發訪問就能更加簡單高效。

使用樂觀鎖的好處:

  1. 在高併發的情況下,它比有鎖的程序擁有更好的性能;
  2. 它天生就是死鎖免疫的,即不會出現死鎖。

就憑藉這兩個優勢,就值得我們冒險嘗試使用樂觀鎖的併發。

那麼,java中有哪些樂觀鎖技術呢?

1.原子操作

原子操作:顧名思義就是不可分割的操作,該操作只存在未開始和已完成兩種狀態,不存在中間狀態,在JDK 1.8 中,java.util.concurrent.atomic 包下類都是原子類,原子類都是基於 sun.misc.Unsafe 實現的,用於實現原子操作。

上文提到的比較交換技術是通過名爲 CAS 的CPU指令實現原子操作,由 CPU 硬件級別上保證原子性:

 CAS(變量, 比較值, 新值)

當變量的當前值與比較值相等時,才把變量更新爲新值。

java.util.concurrent.atomic 包中的原子分爲:原子性基本數據類型、原子性對象引用類型、原子性數組、原子性對象屬性更新器和原子性累加器。具體實現類如下:

  • 原子性基本數據類型:AtomicBooleanAtomicIntegerAtomicLong
  • 原子性對象引用類型:AtomicReferenceAtomicStampedReferenceAtomicMarkableReference
  • 原子性數組:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray
  • 原子性對象屬性更新:AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
  • 原子性累加器:DoubleAccumulatorDoubleAdderLongAccumulatorLongAdder

當多個線程需要操作同一個變量時,我們可以放心地假設是線程安全的。

2. 線程本地存儲

java.lang.ThreadLocal 類用於線程本地化存儲。

線程本地化存儲,就是爲每一個線程創建一個變量,只有本線程可以在該變量中查看和修改值。需要明確的是,這種情況根本不存在資源共享,各個線程各用操作各自的變量,因此,也就不存在死鎖。

典型的使用例子就是,spring 在處理數據庫事務問題的時候,就用了 ThreadLocal 爲每個線程存儲了各自的數據庫連接 Connection。

使用 ThreadLocal 要注意,在不使用該變量的時候,一定要調用 remove() 方法移除變量,否則可能造成內存泄漏的問題。

/**
 * 描述 Java中的ThreadLocal類允許我們創建只能被同一個線程讀寫的變量。
 * 因此,如果一段代碼含有一個ThreadLocal變量的引用,即使兩個線程同時執行這段代碼,
 * 它們也無法訪問到對方的ThreadLocal變量。
 */
public class ThreadLocalExsample {
 /**
 * 創建了一個MyRunnable實例,並將該實例作爲參數傳遞給兩個線程。兩個線程分別執行
 * run()方法,並且都在ThreadLocal實例上保存了不同的值。
 * 如果它們訪問的不是ThreadLocal對象,則第二個線程會覆蓋掉第一個線程設置的值。
 */
public static class MyRunnable implements Runnable {
    private ThreadLocal threadLocal = new ThreadLocal();
    @Override
    public void run() {
        //一旦創建了一個ThreadLocal變量,你可以通過如下代碼設置某個需要保存的值
        threadLocal.set((int) (Math.random() * 100D));
        try {
        	Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        //可以通過下面方法讀取保存在ThreadLocal變量中的值
        System.out.println("-------threadLocal value-------"+threadLocal.get());
    }
}

	public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }
}

運行結果:

-------threadLocal value-------38
-------threadLocal value-------88

3. copy-on-write

copy-on-write,即寫時複製,是指:在併發訪問的情景下,當需要修改JAVA中容器的元素時,不直接修改該容器,而是先複製一份副本,在副本上進行修改。修改完成之後,將指向原來容器的引用指向新的容器(副本容器)。

集合類上的常用操作是:向集合中添加元素、刪除元素、遍歷集合中的元素然後進行某種操作。當多個線程併發地對一個集合對象執行這些操作時就會引發ConcurrentModificationException,比如:

線程A在for-each中遍歷ArrayList,而線程B同時又在刪除ArrayList中的元素,就可能會拋出ConcurrentModificationException,可以在線程A遍歷ArrayList時加鎖避免這一異常;

但由於遍歷操作是一種常見的操作,加鎖之後會影響程序的性能,因此for-each遍歷選擇了不對ArrayList加鎖,而是當有多個線程修改ArrayList時拋出ConcurrentModificationException,因此,這是一種設計上的權衡。

爲了應對這種多線程併發修改集合的這種情況,通常有下面的兩種策略:

  • “寫時複製” 機制;
  • 線程安全的容器類,例如利用CopyOnWriteArrayList代替ArrayList,利用ConcurrentHashMap代替HashMap。它們並不是從“複製”這個角度來應對多線程併發修改,而是引入了分段鎖和CAS鎖解決多線程併發修改的問題。

這裏,主要討論"寫時複製"機制。由於不會其修改原始容器,只修改副本容器。因此,可以對原始容器進行併發地讀。其次,實現了讀操作與寫操作的分離,讀操作發生在原始容器上,寫操作發生在副本容器上;數據一致性問題:讀操作的線程可能不會立即讀取到新修改的數據,因爲修改操作發生在副本上。但最終修改操作會完成並更新容器,因此這是最終一致性。

CopyOnWrite容器適用於讀多寫少的場景。因爲寫操作時,需要複製一個容器,造成內存開銷很大,也需要根據實際應用把握初始容器的大小。而不適合於數據的強一致性場合,即當數據修改之後要求能夠立即被讀到,則不能用寫時複製技術。因爲它是最終一致性

Java 中的 copy-on-write 容器包括:CopyOnWriteArrayListCopyOnWriteArraySet
這裏通過如下實例說明一下CopyOnWriteArrayList的用法:

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;

public class TestCopyOnWrite {
 
	private static final Random R = new Random();
	
	private static CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>();
	//private static ArrayList<Integer> cowList = new ArrayList<Integer>();
	
	public static void main(String[] args) throws InterruptedException {
		List<Thread> threadList = new ArrayList<Thread>();
		//啓動 10 個線程,向 cowList 添加 5 個隨機整數
		for (int i = 0; i < 10; i++) {
			Thread t = new Thread(() -> {
				for (int j = 0; j < 5; j++) {
					//休眠 10 毫秒,讓線程同時向 cowList 添加整數,引出併發問題
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					int tmp = R.nextInt(100);
					cowList.add(tmp);
					System.out.println(Thread.currentThread().getName()+" :  "+tmp);
				}
			}) ;
			t.start();
			threadList.add(t);
		}
		
		//主線程等待所有的線程運行結束
		for (Thread t : threadList) {
			t.join();
		}
		System.out.println(cowList.size());
	}
}

使用CopyOnWriteArrayList可以保證列表中數據個數始終未50個,結果如下:

Thread-2 :  30
Thread-1 :  90
Thread-9 :  88
Thread-0 :  67
Thread-8 :  29
Thread-7 :  30
Thread-6 :  28
Thread-5 :  18
Thread-4 :  23
Thread-3 :  30
Thread-2 :  10
Thread-0 :  1
Thread-8 :  79
Thread-7 :  21
Thread-9 :  22
Thread-1 :  10
Thread-4 :  78
Thread-5 :  81
Thread-3 :  44
Thread-6 :  47
Thread-2 :  24
Thread-7 :  90
Thread-0 :  83
Thread-8 :  93
Thread-1 :  10
Thread-5 :  43
Thread-3 :  67
Thread-6 :  23
Thread-9 :  40
Thread-4 :  36
Thread-2 :  29
Thread-0 :  3
Thread-7 :  6
Thread-1 :  43
Thread-8 :  43
Thread-3 :  90
Thread-9 :  26
Thread-5 :  65
Thread-6 :  58
Thread-4 :  76
Thread-2 :  85
Thread-8 :  95
Thread-1 :  10
Thread-7 :  65
Thread-0 :  95
Thread-3 :  40
Thread-6 :  89
Thread-4 :  53
Thread-9 :  86
Thread-5 :  95
50

而如果使用ArrayList,則偶爾會出現列表中數據個數少於50個的情況:

Thread-2 :  30
Thread-8 :  38
Thread-7 :  42
Thread-6 :  96
Thread-3 :  59
Thread-5 :  46
Thread-1 :  14
Thread-4 :  18
Thread-0 :  54
Thread-9 :  17
Thread-8 :  26
Thread-6 :  37
Thread-1 :  87
Thread-5 :  26
Thread-9 :  7
Thread-3 :  66
Thread-7 :  9
Thread-2 :  56
Thread-0 :  81
Thread-4 :  30
Thread-8 :  19
Thread-5 :  59
Thread-6 :  10
Thread-7 :  23
Thread-3 :  17
Thread-4 :  4
Thread-0 :  19
Thread-2 :  45
Thread-9 :  81
Thread-1 :  8
Thread-8 :  22
Thread-6 :  9
Thread-3 :  51
Thread-0 :  23
Thread-5 :  45
Thread-4 :  8
Thread-7 :  24
Thread-1 :  44
Thread-2 :  35
Thread-9 :  21
Thread-8 :  67
Thread-3 :  53
Thread-5 :  46
Thread-4 :  96
Thread-1 :  68
Thread-0 :  82
Thread-6 :  12
Thread-7 :  63
Thread-9 :  80
Thread-2 :  11
49

本文內容參考資料:Java 中有哪些無鎖技術來解決併發問題?如何使用?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章