本地鎖和分布式鎖的理解


本地所和分布式鎖的理解

1. 本地鎖和分布式鎖的區別。

1.1. 本地鎖的意義

​ 在單進程的系統中,當存在多個線程可以同時改變某個變量(可變共享變量)時,就需要對變量或代碼塊做同步,使其在修改這種變量時能夠線性執行,以防止並發修改變量帶來數據不一致或者數據污染的現象。
​ 而為了實現多個線程在一個時刻同一個代碼塊只能有一個線程可執行,那么需要在某個地方做個標記,這個標記必須每個線程都能看到,當標記不存在時可以設置該標記,其余后續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記后再去嘗試設置標記。這個標記可以理解為鎖。

1.2. 分布式鎖的意義

​ 如果是單機情況下(單JVM),線程之間共享內存,只要使用線程鎖就可以解決並發問題。但如果是分布式情況下(多JVM),線程A和線程B很可能不是在同一JVM中,這樣線程鎖就無法起到作用了,這時候就要用到分布式鎖來解決。

​ 分布式鎖是控制分布式系統同步訪問共享資源的一種方式。

2. 本地鎖

2.1. 常用的本地鎖有什么?

​ synchronized和lock

2.2. 本地鎖鎖的是什么?

​ 在非靜態方法中,鎖的是對象,在非靜態方法中,鎖的是類字節碼

2.3. lock鎖的原理?

​ 通過查看公平鎖的源碼得知在java.util.concurrent.locks的抽象類中AbstractQueuedSynchronizer,存在一個int類型的狀態值,然后進行判斷這個鎖的狀態。

​ Java ReenttrantLock通過構造函數指定該鎖是否公平,默認是非公平鎖,因為非公平鎖的優點在於吞吐量比公平鎖大,對於synchronized而言,也是一種非公平鎖

2.4. volatile是什么意思?干什么用的?

2.41 volatile定義

​ 是java虛擬機提供的輕量級的同步機制。保證可見性,不保證原子性,禁止指令重排。

​ jmm

2.42 volatile的可見性

​ 由於JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(也叫棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行。操作過程有三步。

  1. 首先要將變量從主內存拷貝到自己的工作內存空間。
  2. 然后對變量進行操作。
  3. 操作完成后再將變量寫回主內存,不能直接操作主內存中的變量。

各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程如下。

img

2.43 volatile的不保證原子性

​ 原子性定義:不可分割,完整性,也就是說某個線程正在做某個具體業務時,中間不可以被加塞或者被分割,需要具體完成,要么同時成功,要么同時失敗。

image-20200309174220675

2.44 如何讓volatile保證原子性

  1. 最簡單的方法,加sync的鎖。
  2. 可以使用JUC下面的原子包裝類。

2.45 volatile禁止指令重排

單線程環境里面確保最終執行結果和代碼順序的結果一致

處理器在進行重排序時,必須要考慮指令之間的數據依賴性

多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。

public void mySort() {
	int x = 11;
	int y = 12;
	x = x + 5;
	y = x * x;
}

按照正常單線程環境,執行順序是 1 2 3 4

但是在多線程環境下,可能出現以下的順序:

  • 2 1 3 4
  • 1 3 2 4

上述的過程就可以當做是指令的重排,即內部執行順序,和我們的代碼順序不一樣

但是指令重排也是有限制的,即不會出現下面的順序

  • 4 3 2 1

因為處理器在進行重排時候,必須考慮到指令之間的數據依賴性

因為步驟 4:需要依賴於 y的申明,以及x的申明,故因為存在數據依賴,無法首先執行

2.5. synchronized原理

常用的使用方法:


java對象布局?

​ 整合對象一共16B,其中對象頭有12B,還有4B是對齊的字節(64位虛擬機上對象的大小必須是8的倍數)

對象頭里邊存的是什么呢?

​ 對象頭就是所有對象開頭的公共部分。

​ 參考網站:https://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

2.5. bit和byte的區別?

​ bit意為“位”或“比特”,是計算機運算的基礎;

​ byte意為“字節”,是計算機文件大小的基本計算單位;

​ byte=字節即1byte=8bits,兩者換算是1:8的關系。

3. 鎖的類型:

​ 從線程是否需要對資源加鎖可以分為 悲觀鎖 和 樂觀鎖

​ 從鎖的公平性進行區分,可以分為公平鎖 和 非公平鎖

​ 從多個線程能否獲取同一把鎖分為 共享鎖 和 排他鎖

​ 從資源已被鎖定,線程是否阻塞可以分為 自旋鎖

3.1. 悲觀鎖 和 樂觀鎖

3.11 悲觀鎖

​ 悲觀鎖是一種悲觀思想,它總認為最壞的情況可能會出現,它認為數據很可能會被其他人所修改,所以悲觀鎖在持有數據的時候總會把資源 或者 數據 鎖住,這樣其他線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放為止。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。悲觀鎖的實現往往依靠數據庫本身的鎖功能實現。

​ Java 中的 Synchronized 和 ReentrantLock 等獨占鎖(排他鎖)也是一種悲觀鎖思想的實現,因為 Synchronzied 和 ReetrantLock 不管是否持有資源,它都會嘗試去加鎖,生怕自己心愛的寶貝被別人拿走。

3.12 樂觀鎖

​ 而樂觀鎖的思想與悲觀鎖的思想相反,它總認為資源和數據不會被別人所修改,所以讀取不會上鎖,但是樂觀鎖在進行寫入操作的時候會判斷當前數據是否被修改過(具體如何判斷我們下面再說)。樂觀鎖的實現方案一般來說有兩種:版本號機制 和 CAS實現 。樂觀鎖多適用於多讀的應用類型,這樣可以提高吞吐量。比如在MyBaits-Plus中是支持樂觀鎖機制的。

樂觀鎖實現方式:版本號機制,參考代碼

3.2. 公平鎖 和 非公平鎖

3.21 公平鎖定義

​ 在並發環境中,多個線程需要對同一資源進行訪問,同一時刻只能有一個線程能夠獲取到鎖並進行資源訪問,那么剩下的這些線程怎么辦呢?這就好比食堂排隊打飯的模型,最先到達食堂的人擁有最先買飯的權利,那么剩下的人就需要在第一個人后面排隊,這是理想的情況,即每個人都能夠買上飯。那么現實情況是,在你排隊的過程中,就有個別不老實的人想走捷徑,插隊打飯,如果插隊的這個人后面沒有人制止他這種行為,他就能夠順利買上飯,如果有人制止,他就也得去隊伍后面排隊。

​ 根據以上總結,公平鎖表示在並發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果為空,或者當前線程是等待隊列中的第一個,就占用鎖,否者就會加入到等待隊列中,以后按照FIFO的規則從隊列中取到自己

img

3.22 公平鎖使用

​ 在 Java 中,我們一般通過 ReetrantLock 來實現鎖的公平性。

public class MyFairLock extends Thread {
    //創建公平鎖
    private ReentrantLock lock = new ReentrantLock(true);

    public void fairLock() {
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "正在持有鎖");
        } finally {
            System.out.println(Thread.currentThread().getName() + "釋放了鎖");
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "啟動");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for (int i = 0; i < 10; i++) {
            thread[i] = new Thread(runnable);
        }
        for (int i = 0; i < 10; i++) {
            thread[i].start();
        }
    }
}

​ 查看ReentrantLock源碼可知,如果是 true 的話,那么就會創建一個 ReentrantLock 的公平鎖,然后並創建一個 FairSync ,FairSync 其實是一個 Sync 的內部類,它的主要作用是同步對象以獲取公平鎖。

通過查看FairSync和NonfairSync的源代碼對比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()。

​ 它主要是用來 查詢是否有任何線程在等待獲取鎖的時間比當前線程長,也就是說每個等待線程都是在一個隊列中,此方法就是判斷隊列中在當前線程獲取鎖時,是否有等待鎖時間比自己還長的隊列,如果當前線程之前有排隊的線程,返回 true,如果當前線程位於隊列的開頭或隊列為空,返回 false。

​ 綜上,公平鎖就是通過同步隊列來實現多個線程按照申請鎖的順序來獲取鎖,從而實現公平的特性。非公平鎖加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,所以存在后申請卻先獲得鎖的情況。

3.22 非公平鎖定義

​ 非公平鎖就是一種獲取鎖的搶占機制,是隨機獲得鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這個方式可能造成某些線程一直拿不到鎖,結果也就是不公平的了。

img

3.4 獨占鎖和共享鎖

3.4.1 獨占鎖定義

​ 獨占鎖又叫做排他鎖,是指鎖在同一時刻只能被一個線程擁有,其他線程想要訪問資源,就會被阻塞。JDK 中 synchronized和 JUC 中 Lock 的實現類就是互斥鎖。

3.4.2 共享鎖定義

​ 共享鎖指的是鎖能夠被多個線程所擁有,如果某個線程對資源加上共享鎖后,則其他線程只能對資源再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。

3.4.3 實例

		ReadWriteLock lock=new ReentrantReadWriteLock();
        Lock readLock = lock.readLock();
        Lock writeLock = lock.writeLock();

​ ReentrantReadWriteLock 有兩把鎖:ReadLock 和 WriteLock,也就是一個讀鎖一個寫鎖,合在一起叫做讀寫鎖。其中讀鎖是共享鎖,寫鎖是獨享鎖,因為讀鎖和寫鎖是分離的。所以ReentrantReadWriteLock的並發性相比一般的互斥鎖有了很大提升。

3.5 自旋鎖

3.5.1 自旋鎖定義

​ 自旋鎖是一種非阻塞鎖,當一個線程嘗試去獲取某一把鎖的時候,如果這個鎖此時已經被別人獲取(占用),該線程不會被掛起,那么此線程就無法獲取到這把鎖,該線程將會間隔一段時間后會再次嘗試獲取。這種采用循環加鎖 -> 等待的機制被稱為自旋鎖(spinlock)。

3.5.2 自旋鎖的優點

​ 自旋鎖盡可能的減少線程的阻塞,這對於鎖的競爭不激烈,且占用鎖時間非常短的代碼塊來說性能能大幅度的提升,因為自旋的消耗會小於線程阻塞掛起再喚醒的操作的消耗。總的來說自旋鎖是為了解決線程的頻繁切換引起的性能損耗,所以才自旋讓當前線程一直占用資源。

3.5.3 自旋鎖的弊端

​ 如果長時間上鎖的話,自旋鎖會非常耗費性能,它阻止了其他線程的運行和調度。線程持有鎖的時間越長,如果持有鎖的線程發生中斷情況,那么其他線程將一直保持旋轉狀態(反復嘗試獲取鎖),而持有該鎖的線程並不打算釋放鎖,這樣導致的是結果是無限期推遲。

​ 解決上面這種情況一個很好的方式是給自旋鎖設定一個自旋時間,等時間一到立即釋放自旋鎖。自旋鎖的目的是占着CPU資源不進行釋放,等到獲取鎖立即進行處理。但是如何去選擇自旋時間呢?如果自旋執行時間太長,會有大量的線程處於自旋狀態占用 CPU 資源,進而會影響整體系統的性能。因此自旋的周期選的額外重要!JDK在1.6 引入了適應性自旋鎖,適應性自旋鎖意味着自旋時間不是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖擁有的狀態來決定,基本認為一個線程上下文切換的時間是最佳的一個時間。

3.5.4 自旋鎖demo

/**
 * 手寫自旋鎖
 * 循環比較獲取直到成功為止,沒有類似於wait的阻塞
 * 通過CAS操作完成自旋鎖,A線程先進來調用myLock方法自己持有鎖5秒,B隨后進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖后B隨后搶到
 */
public class SpinLock {
    // 現在的泛型裝的是Thread,原子引用線程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void myLock() {
        // 獲取當前進來的線程
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in ");

        // 開始自旋,期望值是null,更新值是當前線程,如果是null,則更新為當前線程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {

        }
    }

    /**
     * 解鎖
     */
    public void myUnLock() {

        // 獲取當前進來的線程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference變成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }

    public static void main(String[] args) {

        SpinLock spinLockDemo = new SpinLock();

        // 啟動t1線程,開始操作
        new Thread(() -> {
            // 開始占有鎖
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 開始釋放鎖
            spinLockDemo.myUnLock();
        }, "t1").start();


        // 讓main線程暫停1秒,使得t1線程,先執行
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒后,啟動t2線程,開始占用這個鎖
        new Thread(() -> {
            // 開始占有鎖
            spinLockDemo.myLock();
            // 開始釋放鎖
            spinLockDemo.myUnLock();
        }, "t2").start();

    }
}

4. 分布式鎖

4.1 常用的分布式鎖都有什么?

  1. 基於數據庫實現分布式鎖
  2. 基於緩存(Redis等)實現分布式鎖
  3. 基於Zookeeper實現分布式鎖

4.2 分布式鎖的條件

  1. 排他性:同一時間,只能有一個客戶端獲取鎖,其他客戶端不能同事獲取鎖。
  2. 避免死鎖:這把鎖在一定的時間后需要釋放,否則會產生死鎖,這里面包括正常釋放鎖和非正常釋放鎖,比如即使一個客戶端在持鎖期間發生故障而沒有釋放鎖也要保證后續的客戶端能枷鎖。
  3. 自己解鎖:加鎖和解鎖都應該是同一個客戶端去完成,不能去解別人的鎖。
  4. 高可用:獲取和釋放鎖必須高可用且優秀。

4.3 數據庫分布式鎖?

​ 在數據庫中創建一個表,並在字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖。

4.2 Zookeeper分布式鎖?

4.2.1 Zookeeper的節點類型和watch機制

zookeeper的節點類型:

PERSISTENT 持久化節點
PERSISTENT_SEQUENTIAL 順序自動編號持久化節點,這種節點會根據當前已存在的節點數自動加 1
EPHEMERAL 臨時節點, 客戶端session超時這類節點就會被自動刪除
EPHEMERAL_SEQUENTIAL 臨時自動編號節點

zookeeper的watch機制:

Znode發生變化(Znode本身的增加,刪除,修改,以及子Znode的變化)可以通過Watch機制通知到客戶端。那么要實現Watch,就必須實現org.apache.zookeeper.Watcher接口,並且將實現類的對象傳入到可以Watch的方法中。Zookeeper中所有讀操作(getData(),getChildren(),exists())都可以設置Watch選項。Watch事件具有one-time trigger(一次性觸發)的特性,如果Watch監視的Znode有變化,那么就會通知設置該Watch的客戶端。

4.2.2 實現思路

定義鎖:

​ 在通常的java並發編程中,有兩種常見的方式可以用來定義鎖,分別是synchronized機制和JDK5提供的ReetrantLock。然而,在zookeeper中,沒有類似於這樣的API可以直接使用,而是通過Zookeeper上的數據節點來表示一個鎖,例如/exclusive_lock/lock節點就可以定義為一個鎖。

獲取鎖

​ 在需要獲取鎖的時候,所有的客戶端都會試圖通過調用create()接口,在/exclusive_lock節點下創建臨時子節點/exclusive_lock/lock。zookeeper會保證在所有客戶端中,最終只有一個客戶端能夠創建成功,那么就可以認為該客戶端獲得了鎖。同時,所有沒有獲得鎖的客戶端就需要到/exclusive_lock節點上注冊一個子節點變更的Watcher監聽,以便實時監聽到lock節點的變更情況。

釋放鎖

​ 在定義鎖部分,我們已經提到,/exclusive_lock/lock是一個臨時節點,因此在以下兩種情況下,都有可能釋放鎖。

1.當前獲取鎖的客戶端發生宕機,那么Zookeeper上的這個臨時節點就會被移除。

2.正常執行完業務邏輯之后,客戶端就會主動將自己創建的臨時節點刪除

4.2.3 參考示例代碼

​ 參考了ReentrantLock的設計思路,使用了模板方法設計模式。

4.3 redission分布式鎖?

4.31 使用redis的原因

  1. Redis有很高的性能;
  2. Redis命令對此支持較好,實現起來比較方便

4.32 實現思路

  1. 獲取鎖的時候,使用setnx加鎖,並使用expire命令為鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值為一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。
  2. 獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
  3. 釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

4.33 示例代碼

也可以使用Redis的分布式鎖框架--Redission代碼示例


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM