Lock鎖的詳細實現(AQS及Future Task)

系列目錄

浪費了一個週末啥都沒學 QAQjava

日期:2019年7月2日23:05:51數據庫

Lock

核心API

API

方法 描述
lock

獲取鎖的方法,若鎖被其餘線程獲取,則等待(阻塞)緩存

lockinterruptibly

在鎖的獲取過程當中能夠中斷當前線程安全

tryLockbash

嘗試非阻塞地獲取鎖,當即返回併發

unlock

釋放鎖ide

Tips:post

根據Lock接口的源碼註釋,Lock接口的實現, 具有和同步關鍵字一樣的內存語義。性能

實例

可重入


public class ReentrantDemo1 {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();  // block until condition holds
        try {
            System.out.println("第一次獲取鎖");
            System.out.println("當前線程獲取鎖的次數" + lock.getHoldCount());
            lock.lock();
            System.out.println("第二次獲取鎖了");
            System.out.println("當前線程獲取鎖的次數" + lock.getHoldCount());
        } finally {
            lock.unlock();
            lock.unlock();
        }
        System.out.println("當前線程獲取鎖的次數" + lock.getHoldCount());

        // 若是不釋放,此時其餘線程是拿不到鎖的
        new Thread(() -> {
            System.out.println(Thread.currentThread() + " 指望搶到鎖");
            lock.lock();
            System.out.println(Thread.currentThread() + " 線程拿到了鎖");
        }).start();
    }
}
複製代碼


  1. 是可重入的,一個 線程 能夠鎖定屢次
    1. 須要 unluck() 相同的次數
    2. lock() n 次則須要 unlock 相同次數
可響應中斷

中斷只是標記線程應該被中斷,但不是立刻中止ui


// 可響應中斷
public class LockInterruptiblyDemo1 {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        LockInterruptiblyDemo1 demo1 = new LockInterruptiblyDemo1();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    demo1.test(Thread.currentThread());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        Thread.sleep(500); // 等待0.5秒,讓thread1先執行

        thread2.start();
        Thread.sleep(2000); // 兩秒後,中斷thread2

        thread2.interrupt();
    }

    public void test(Thread thread) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ", 想獲取鎖");
        lock.lockInterruptibly();   //注意,若是須要正確中斷等待鎖的線程,必須將獲取鎖放在外面,而後將InterruptedException拋出
        try {
            System.out.println(thread.getName() + "獲得了鎖");
            Thread.sleep(10000); // 搶到鎖,10秒不釋放
        } finally {
            System.out.println(Thread.currentThread().getName() + "執行finally");
            lock.unlock();
            System.out.println(thread.getName() + "釋放了鎖");
        }
    }
}複製代碼


lockInterruptibly()/lock() 區別

  • lockInterruptibly 能夠響應中斷
    • 即,阻塞時被中斷,能夠 throw exception
  • lock() 不響應中斷
    • 被 interrupt 後不會當即中斷,而是繼續等待


ReentrantReadWriteLock 讀寫鎖

讀寫鎖定義

  • 維護一對關聯鎖,一個用於只讀操做,一個用於寫入。
  • 讀鎖能夠由多個讀線程同時持有,寫鎖是排他的。
    • 寫鎖的時候能夠讀。
    • 讀鎖的時候不可寫。
  • 適合讀取線程比寫入線程多的場景,改進互斥鎖的性能。
    • 示例場景:緩存組件、集合的併發線程安全性改造。

鎖降級定義

  • 鎖降級指的是寫鎖降級成爲讀鎖。
    • 把持住當前擁有的寫鎖的同時,再獲取到讀鎖,隨後釋放寫鎖的過程。
  • 寫鎖是線程獨佔,讀鎖是共享,因此寫->讀是升級。(讀->寫, 是不能實現的)

實例

線程安全問題:變量沒有知足 可見性 和 原子性。

讀鎖 -> 共享鎖

寫鎖 -> 獨享鎖

  • 適用於讀多,寫少的場景。若是這類場景加鎖,就會致使資源利用率低下(讀也被加鎖了)
適用sync等互斥鎖效率低
  • image.png
使用讀鎖效率高

能夠多個線程同時讀

image.png

  • 讀鎖能夠由多個讀線程同時持有,寫鎖是排他的。
    • 寫鎖的時候能夠讀。
    • 讀鎖的時候不可寫。

使用讀寫鎖防止緩存雪崩

當沒有讀寫鎖時,大量請求在沒有命中緩存的狀況下,所有打到 db 上。

有可能出現數據不一致的狀況。


// 緩存示例
public class CacheDataDemo {
    // 建立一個map用於緩存
    private Map<String, Object> map = new HashMap<>();
    private static ReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        // 1 讀取緩存裏面的數據
        // cache.query()
        // 2 若是換成沒數據,則取數據庫裏面查詢 database.query()
        // 3 查詢完成以後,數據塞到塞到緩存裏面 cache.put(data)
    }

    public Object get(String id) {
        Object value = null;
        // 首先開啓讀鎖,從緩存中去取
        rwl.readLock().lock();
        try {
            if (map.get(id) == null) {
                // TODO database.query(); 所有查詢數據庫 ,緩存雪崩
                // 必須釋放讀鎖
                rwl.readLock().unlock();
                // 若是緩存中沒有釋放讀鎖,上寫鎖。若是不加鎖,全部請求所有去查詢數據庫,就崩潰了
                rwl.writeLock().lock(); // 全部線程在此處等待 1000 1 999 (在同步代碼裏面再次檢查是否緩存)
                try {
                    // 雙重檢查,防止已經有線程改變了當前的值,從而出現重複處理的狀況
                    if (map.get(id) == null) {
                        // TODO value = ...若是緩存沒有,就去數據庫裏面讀取
                    }
                    rwl.readLock().lock(); // 加讀鎖降級寫鎖,這樣就不會有其餘線程可以改這個值,保證了數據一致性
                } finally {
                    rwl.writeLock().unlock(); // 釋放寫鎖@
                }
            }
            
        /* 在這裏又進行了一系列操做,在操做過程當中,有可能數據改變致使緩存內容改變 此時,要在寫鎖中加入讀鎖,防止相似於 幻讀,髒讀 等的產生 */
           
        } finally {
            rwl.readLock().unlock();
        }
        return value;
    }
}
複製代碼
  • 緩存雪崩
    • 在沒有命中緩存的狀況下,釋放讀鎖,加寫鎖。防止多個線程同時讀 db,致使雪崩
  • 鎖降級 寫鎖 -> 讀鎖
    • 在讀鎖過程當中,能夠在釋放前加寫鎖。
    • 不會形成死鎖!!!

HashMap 的線程安全改造

HashTable 是如何實現線程安全的

如何更高效的改造

實例

手動實現 reentrantLock

  • wait()/notify() 必需要在同步代碼塊(monitor object)
  • 因此要使用 pack/unpack

簡單版本


/** * 本身手動實現的一個 reentrantLock * */
public class GzyLock implements Lock {

    //須要 CAS 自旋的方式去實現
    //模仿 monitor obj 的重量級鎖 有一個 owner
    private static AtomicReference<Thread> owner = new AtomicReference<>();

    private static LinkedBlockingDeque<Thread> waiters = new LinkedBlockingDeque<>();

    @Override
    public void lock() {
        boolean addQueue = true;
        while (!tryLock()){
            //第一次進來會放到queue
            if(addQueue) {
                //若是沒有獲取到鎖,就先存到 queue
                waiters.offer(Thread.currentThread());
                addQueue = false;
            }else {
                //park 等待被喚醒。這裏不能用 wait/notify 由於須要在同步代碼塊用
                LockSupport.park();
            }
            //喚醒後嘗試爭搶,沒搶到繼續等
        }
        //搶到鎖,移除掉
        waiters.remove(Thread.currentThread());
    }
    @Override
    public boolean tryLock() {
        //適用當前線程嘗試加鎖
        return owner.compareAndSet(null, Thread.currentThread());
    }

    @Override
    public void unlock() {
        //unlock 的時候,要喚醒等待線程
        //若是釋放鎖成功了,纔會喚起
        //這裏用 if 是由於,必定不會出現 循環
        if (owner.compareAndSet(Thread.currentThread(), null)) {
            //一次喚醒全部在等待隊列的
            for (Thread waiter : waiters) {
                //喚起等待隊列的線程
                LockSupport.unpark(waiter);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Adder adder = new Adder();
        for (int j = 0; j < 10; j++) {
            Thread addThread = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    adder.add();
                }
            });
            addThread.start();;
        }
        Thread.sleep(1000L);
        System.out.println(adder.i);
    }

    static class Adder{

        Lock lock = new GzyLock();
        int i = 0;
        public void add(){
            lock.lock();
            try {
                i++;
            }finally {
                lock.unlock();
            }
        }
    }
}
複製代碼


  • 注意幾點,必定要在 unlock() 成功之後在進行 uppark
  • 非公平的,有可能在 unlock 的 CAS 執行成功後,unpark waiter 以前,有新的線程進行了 lock

抽象隊列同步器 AQS 的初步理解

結合源碼的初步理解,具體的定義:抽象隊列同步器

  • 能夠實現 公平 與 非公平
  • 抽象隊列同步器,實現了 線程的等待和喚醒 以及 資源的獲取與釋放
  • 只是個同步隊列,沒有定義鎖的獲取與釋放邏輯。
    • 即,抽象了資源的獲取與釋放邏輯
  • 只是持有鎖、鎖池(waiters)以及定義了 acquire/release 資源後的操做
    • acquire/release 資源後的操做,即 線程的等待和喚醒邏輯
  • 具體如何獲取與釋放,由不一樣的實現類去定義
  • Lock
    • Lock 改成持有 抽象隊列同步器 ,再也不持有 資源、鎖池
    • 不一樣的鎖實現了不一樣的 隊列同步器, 定義了不一樣的資源同步(acquire/release)邏輯

複製代碼


reentrantLock 源碼梳理

鎖的本質

  • 同步的方式:獨享鎖-單個隊列窗口,共享鎖-多個隊列窗口
  • 搶鎖的方式:插隊搶(不公平鎖)、先來後到搶鎖(公平鎖)
    • 不公平鎖:會先嚐試搶鎖,搶不到纔會進入等待隊列
  • 沒搶到鎖的處理方式:快速嘗試屢次(CAS自旋鎖)、阻塞等待
    • 阻塞等待,必定會有一個等待隊列。由於須要被喚醒
  • 喚醒阻塞線程的方式(叫號器):所有通知、通知下一個

抽象隊列同步器 AQS

簡介

只有鎖和鎖池(waiters),定義了 鎖(線程) 的獲取和釋放後的處理邏輯。

抽象了 獲取和釋放 鎖(資源)的方法,須要根據具體的業務場景去實現。例如:同步鎖、非同步鎖;獨享鎖,共享鎖。

等待/喚醒邏輯,都由 AQS 去實現了。由於不管什麼樣的鎖,都須要去等待。image.png

  • acquire、acquireShared
    • 定義了資源爭用的邏輯,若是沒拿到,則等待。
  • tryAcquire、tryAcquireShared
    • 實際執行佔用資源的操做,如何斷定一個由使用者具體去實現。
  • release、releaseShared
    • 定義釋放資源的邏輯,釋放以後,通知後續節點進行爭搶。
  • tryRelease、tryReleaseShared
    • 實際執行資源釋放的操做,具體的AQS使用者去實現。
  • state/exclusive owner/queue
    • state 不一樣場景下含義不一樣
    • 獨享鎖的 lock/unlock 記錄了當前線程 lock 的次數
    • 共享鎖的 acquireShare/releaseShare 表示當前已使用的資源數


Tips
  • 當前線程屢次加鎖
  • 公平與非公平只是區別因而否剛到的時候搶一下
  • 讀寫鎖
    • 操做字段不一樣
  • AQS核心實際上是 八大方法、三大主體
    • 經過八大方法去操做三大主體

讀寫鎖流程

image.png


AQS 資源佔用流程

image.png

Future Task

源碼來一波

版權聲明

image.png

  • 本文做者: 留夕
  • 本文連接: www.yuque.com/diamond/ndv…
  • 版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY-SA 4.0 許可協議。轉載請註明出處!
  • 首發日期: 2019年7月2日23:05:51
相關文章
相關標籤/搜索