1. 程式人生 > 其它 >synchronized和ReentrantLock

synchronized和ReentrantLock


title: synchronized和ReentrantLock
date: 2017/12/24 21:12:55
tags: [ReentrantLock,synchronized,java併發]
categories:

  • 開發
  • java

前置知識

共享可變

執行緒問題原因

  • 有限資源的競爭

如何解決

  • 區域性化
  • final化
  • 同步化
  • 原子變數化
可見性
  • volitile
  • 一個執行緒修改的結果,另外一個執行緒立馬就知道了
原子性
  • 在程式設計世界裡,某個操作如果不可分割我們就稱之為該操作具有原子性
  • 例如i++ ,它不是原子操作 ,存線上程安全問題,這個時候我們需要使用同步機制來保證其原子性,以確保執行緒安全。
有序性
  • 指令優化:重排序
  • 如何解決
    • volatile , final ,synchronized ,顯式鎖
執行緒安全
  • 《Java併發程式設計實戰》當多個執行緒訪問某類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行為,那麼就稱這個類是執行緒安全的

執行緒同步
  • 保證資料的正確完整性

    多執行緒程式設計裡面,一些較為敏感的資料是不允許被多個執行緒同時訪問的,使用執行緒同步技術,確保資料在任何時刻最多隻有一個執行緒訪問,保證資料的完整性。

  • 執行緒同步的4種機制

    • 臨界區(Critical Section)

      通過對多執行緒的序列化來訪問公共資源或一段程式碼,速度快,適合控制資料訪問。在任意時刻只允許一個執行緒對共享資源進行訪問,如果有多個執行緒試圖訪問公共資源,那麼在有一個執行緒進入後,其他試圖訪問公共資源的執行緒將被掛起,並一直等到進入臨界區的執行緒離開,臨界區在被釋放後,其他執行緒才可以搶佔。

    • 互斥量(Mutex)

      採用互斥物件機制。 只有擁有互斥物件的執行緒才有訪問公共資源的許可權,因為互斥物件只有一個,所以能保證公共資源不會同時被多個執行緒訪問。互斥不僅能實現同一應用程式的公共資源安全共享,還能實現不同應用程式的公共資源安全共享

    • 訊號量(Semaphore)

      它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目

    • 事件(Event)

      通過事件通知的方式來保持執行緒的同步,還可以方便實現對多個執行緒的優先順序比較的操作

JAVA中鎖的4種狀態
  • 無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態 它會隨著競爭情況逐漸升級。

  • 鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。

  • 這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率

  • 鎖自旋

    • 在遇到鎖的競爭或者等待時,執行緒可以不著急進入阻塞狀態,而是等一等,看看鎖是不是馬上就釋放了,這就是鎖自旋。鎖自旋在一定程度上可以對執行緒進行優化處理 (旋轉等待哈)
  • 偏向鎖

    • 偏向鎖主要為了解決在沒有競爭情況下鎖的效能問題。(總是偏向老熟人,除非有人強烈想認識我)

      在大多數情況有些鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。當某個執行緒獲得鎖的情況,該執行緒是可以多次鎖住該物件,但是每次執行這樣的操作都會因為CAS(CPU的Compare-And-Swap指令)操作而造成一些開銷消耗效能,為了減少這種開銷,這個鎖會偏向於第一個獲得它的執行緒,如果在接下來的執行過程中,該鎖沒有被其他的執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。當有其他執行緒在嘗試著競爭偏向鎖時,持有偏向鎖的執行緒就會釋放鎖。

  • 鎖膨脹

    多個或多次呼叫粒度太小的鎖,進行加鎖解鎖的消耗,反而還不如一次大粒度的鎖呼叫來得高效

  • 輕量級鎖

    “對於絕大部分的鎖,在整個同步週期內都是不存在競爭的” , 如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖會比傳統的重量級鎖更慢。

synchronized

synchronized主要包括兩種方法:synchronized 方法、synchronized 塊。

來個問題
  • 當一個執行緒進入一個物件的一個synchronized方法後,其它執行緒是否可進入此物件的其它方法?

    分幾種情況:

    • 其他方法前是否加了synchronized關鍵字,如果沒加,則能。
    • 如果這個方法內部呼叫了wait,則可以進入其他synchronized方法。
    • 如果其他個方法都加了synchronized關鍵字,並且內部沒有呼叫wait,則不能。
    • 如果其他方法是static,它用的同步鎖是當前類的位元組碼,與非靜態的方法不能同步,因為非靜態的方法用的是this。
    • 總的來說: 其它執行緒能不能訪問其它方法,1.就看其它方法有沒有上鎖 2.用的是不是同一把鎖 3.當前執行緒有沒有wait
儘量選擇synchronized塊
  • 它使臨界區變的儘可能小了

  • 使用如下

    synchronized(object) {  
        //共享資料保護起來  
    }
    
    synchronized (this) {
        //共享資料保護起來 
    }
    
這個鎖到底是什麼
  • 對於同步方法,鎖是當前例項物件。
  • 對於同步方法塊,鎖是Synchonized括號裡配置的物件。
  • 對於靜態同步方法,鎖是當前物件的Class物件。
原理

位元組碼鎖 ,monitor enter ,monitor exit

sync queue

鎖升級 , 物件頭 = mark word +

mark word

LOCK 更強大、靈活的鎖機制

public class ThreadTest {
    Lock lock = new Lock();
    public void test(){
        lock.lock();
        //被保護的資料
        lock.unlock();
    }
}
可重入性
  • 意味著自己可以再次獲得自己的內部鎖,而不需要阻塞
public class Father {
    public synchronized void method(){
        //do something
    }
}
public class Child extends Father{
    public synchronized void method(){
        //do something 
        super.method();
    }
}

如果鎖是不可重入的,上面的程式碼就會死鎖,因為呼叫child的method(),首先會獲取父類Father的內建鎖然後獲取Child的內建鎖,當呼叫父類的方法時,需要再次後去父類的內建鎖,如果不可重入,可能會陷入死鎖。

  • 可重入性的實現

    是通過每個鎖關聯一個請求計數器和一個佔有它的執行緒,當計數為0時,認為該鎖是沒有被佔有的,那麼任何執行緒都可以獲得該鎖的佔有權。當某一個執行緒請求成功後,JVM會記錄該鎖的持有執行緒 並且將計數設定為1,如果這時其他執行緒請求該鎖時則必須等待。當該執行緒再次請求請求獲得鎖時,計數會+1;當佔有執行緒退出同步程式碼塊時,計數就會-1,直到為0時,釋放該鎖。這時其他執行緒才有機會獲得該鎖的佔有權。

主要介面或者子類

java.util.concurrent.locks提供了非常靈活鎖機制,為鎖定和等待條件提供一個框架的介面和類,它不同於內建同步和監視器,該框架允許更靈活地使用鎖定和條件

  • ReentrantLock:一個可重入的互斥鎖,是一種遞迴無阻塞的同步機制,為lock介面的主要實現。

  • ReadWriteLock 維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。

  • ReentrantReadWriteLock 實現了ReadWriteLock 介面 ,ReadLock WriteLock 是其靜態內部類

與synchronized的區別

ReentrantLock與synchronized的區別

  • ReentrantLock 可重入的互斥鎖 ,是一種遞迴無阻塞的同步機制

  • ReentrantLock具備更強的擴充套件性。例如:時間鎖等候,可中斷鎖等候,鎖投票。

  • ReentrantLock還提供了條件Condition,對執行緒的等待、喚醒操作更加詳細和靈活,所以在多個條件變數和高度競爭鎖的地方,ReentrantLock更加適合

  • ReentrantLock提供了可輪詢的鎖請求。它會嘗試著去獲取鎖,如果成功則繼續,否則可以等到下次執行時處理,而synchronized則一旦進入鎖請求要麼成功要麼阻塞,所以相比synchronized而言,ReentrantLock會不容易產生死鎖些。

  • ReentrantLock支援更加靈活的同步程式碼塊,但是使用synchronized時,只能在同一個synchronized塊結構中獲取和釋放。注:ReentrantLock的鎖釋放一定要在finally中處理,否則可能會產生嚴重的後果。

  • ReentrantLock支援中斷處理,且效能較synchronized會好些。

  • ReentrantLock提供公平鎖機制,構造方法接收一個可選的公平引數。這些鎖將訪問權授予等待時間最長的執行緒。否則該鎖將無法保證執行緒獲取鎖的訪問順序。但是公平鎖與非公平鎖相比,公平鎖吞吐量更低。

/**
     * 預設構造方法,非公平鎖
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * true公平鎖,false非公平鎖
     * @param fair
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
ReentrantLock Demo
// 任務內容
public class PrintQueue {
	private final Lock printLock = new ReentrantLock();
	public void printJob(Object document){
		try {
			// 獲取鎖
			printLock.lock();
			System.out.println(Thread.currentThread().getName() 
                               + ": 準備列印一份檔案……");
			Long duration = (long) (Math.random() * 10000);
			System.out.println(Thread.currentThread().getName() 
                               + ": 正在列印,預計耗時 " + (duration / 1000) + " s");
			Thread.sleep(duration);
			System.out.println(Thread.currentThread().getName() 
                               + ": 文件列印完成!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			// 釋放鎖
			printLock.unlock();
		}
	}
}
// 列印任務
public class Job implements Runnable{
    
    private PrintQueue printQueue;
    
    public Job(PrintQueue printQueue){
        this.printQueue = printQueue;
    }
    
    @Override
    public void run() {
        printQueue.printJob(new Object());
    }
}

// TEST
public class Test {
	public static void main(String[] args) {
		PrintQueue printQueue = new PrintQueue();
		for (int i = 0; i < 5; i++) {
			new Thread(new Job(printQueue), "執行緒 " + i).start();
		}
	}
}

// 執行結果 
> lock.reentrantLock.Test
執行緒 1: 準備列印一份檔案……
執行緒 1: 正在列印,預計耗時 4 s
執行緒 1: 文件列印完成!
執行緒 0: 準備列印一份檔案……
執行緒 0: 正在列印,預計耗時 8 s
執行緒 0: 文件列印完成!
執行緒 3: 準備列印一份檔案……
執行緒 3: 正在列印,預計耗時 5 s
執行緒 3: 文件列印完成!
執行緒 2: 準備列印一份檔案……
執行緒 2: 正在列印,預計耗時 6 s
執行緒 2: 文件列印完成!
執行緒 4: 準備列印一份檔案……
執行緒 4: 正在列印,預計耗時 8 s
執行緒 4: 文件列印完成!

Process finished with exit code 0

ReentrantLock結構圖
graph BT; ReentrantLock(ReentrantLock<br/>sync: Sync)-.實現.->Lock(interface<br/>*********</br>Lock) ReentrantLock--組合-->Sync Sync--繼承-->AQS FairSync--繼承-->Sync NoFairSync--繼承-->Sync

ReentrantLock實現Lock介面

Sync與ReentrantLock是組合關係,且FairSync(公平鎖)、NonfairySync(非公平鎖)是Sync的子類。

Sync繼承AQS(AbstractQueuedSynchronizer)

  • AQS(AbstractQueuedSynchronizer)為java中管理鎖的抽象類。該類為實現依賴於先進先出 (FIFO) 等待佇列的阻塞鎖和相關同步器(訊號量、事件,等等)提供一個框架。該類提供了一個非常重要的機制,在JDK API中是這樣描述的:為實現依賴於先進先出 (FIFO) 等待佇列的阻塞鎖和相關同步器(訊號量、事件,等等)提供一個框架。此類的設計目標是成為依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。子類必須定義更改此狀態的受保護方法,並定義哪種狀態對於此物件意味著被獲取或被釋放。假定這些條件之後,此類中的其他方法就可以實現所有排隊和阻塞機制。子類可以維護其他狀態欄位,但只是為了獲得同步而只追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操作以原子方式更新的 int 值。 這麼長的話用一句話概括就是:維護鎖的當前狀態和執行緒等待列表。

  • CLH:AQS中“等待鎖”的執行緒佇列。我們知道在多執行緒環境中我們為了保護資源的安全性常使用鎖將其保護起來,同一時刻只能有一個執行緒能夠訪問,其餘執行緒則需要等待,CLH就是管理這些等待鎖的佇列。

  • CAS(compare and swap):比較並交換函式,它是原子操作函式,也就是說所有通過CAS操作的資料都是以原子方式進行的。

公平和非公平鎖
  • 非公平鎖

    final void lock() {  
        // 通過cas嘗試設定鎖狀態,若成功直接將鎖的擁有者設定為當前執行緒 - 簡單粗暴
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //否則呼叫acquire()嘗試獲取鎖;
            acquire(1);
      }
    
  • 公平鎖

    通過hasQueuedPredecessors()來判斷該執行緒是否位於CLH佇列中頭部,是則獲取鎖;而非公平鎖則不管你在哪個位置都直接獲取鎖。

    protected final boolean tryAcquire(int acquires) {
            //當前執行緒
            final Thread current = Thread.currentThread();
            //獲取鎖狀態state
            int c = getState();
            /*
             * 當c==0表示鎖沒有被任何執行緒佔用,在該程式碼塊中主要做如下幾個動作:
             * 則判斷“當前執行緒”是不是CLH佇列中的第一個執行緒執行緒(hasQueuedPredecessors),
             * 若是的話,則獲取該鎖,設定鎖的狀態(compareAndSetState),
             * 並切設定鎖的擁有者為“當前執行緒”(setExclusiveOwnerThread)。
             */
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            /*
             * 如果c != 0,表示該鎖已經被執行緒佔有,則判斷該鎖是否是當前執行緒佔有,若是設定state,否則直接返回false
             */
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    
  • 不同點

    獲取鎖機制不同,公平鎖在獲取鎖時採用的是公平策略(CLH佇列),而非公平鎖則採用非公平策略它無視等待佇列,直接嘗試獲取。

  • unlock

    • unlock最好放在finally中!
     public void unlock() {
        sync.release(1);
     }
     
     public final boolean release(int arg) {
         if (tryRelease(arg)) {
             Node h = head;
             if (h != null && h.waitStatus != 0)
                 unparkSuccessor(h);
             return true;
         }
         return false;
     }
    

    release(1),嘗試在當前鎖的鎖定計數(state)值上減1。成功返回true,否則返回false。當然在release()方法中不僅僅只是將state - 1這麼簡單,- 1之後還需要進行一番處理,如果-1之後的新state = 0 ,則表示當前鎖已經被執行緒釋放了,同時會喚醒執行緒等待佇列中的下一個執行緒,當然該鎖不一定就一定會把所有權交給下一個執行緒,能不能成功就看它運氣了

參考文件

chenssy併發程式設計實戰系列