Lock 鎖機制詳解 | Lock 與的 Synchronized 區別


本章內容涵蓋Lock的使用講解,可重入鎖、讀寫鎖。Lock和Synchronized的對比等。
多線程一直Java開發中的難點,也是面試中的常客,趁着還有時間,打算鞏固一下JUC方面知識,我想機會隨處可見,但始終都是留給有准備的人的,希望我們都能加油!!!

沉下去,再浮上來,我想我們會變的不一樣的。

一、什么是 Lock

Lock 鎖實現提供了比使用同步方法和語句可以獲得的更廣泛的鎖操作。

二、鎖類型

可重入鎖:在執行對象中所有同步方法不用再次獲得鎖

可中斷鎖:在等待獲取鎖過程中可中斷

公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具有優先獲取鎖權利

讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫

三、Lock接口

public interface Lock {

    void lock(); //獲得鎖。

    /**
    除非當前線程被中斷,否則獲取鎖。
    
	如果可用,則獲取鎖並立即返回。
	如果鎖不可用,則當前線程將出於線程調度目的而被禁用並處於休眠狀態,直到發生以下兩種情況之一:
		鎖被當前線程獲取; 
		要么其他一些線程中斷當前線程,支持中斷獲取鎖。
	如果當前線程:
		在進入此方法時設置其中斷狀態; 
		要么獲取鎖時中斷,支持中斷獲取鎖,
    */
    void lockInterruptibly() throws InterruptedException; 

    /**
    僅在調用時空閑時才獲取鎖。
	如果可用,則獲取鎖並立即返回值為true 。 如果鎖不可用,則此方法將立即返回false值。
	*/
    boolean tryLock();
    
    //比上面多一個等待時間 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  

   	// 解鎖
    void unlock(); 
    
    //返回綁定到此Lock實例的新Condition實例。
    Condition newCondition();  。
}

下面講幾個常用方法的使用。

3.1、lock()、unlock()

lock()是最常用的方法之一,作用就是獲取鎖,如果鎖已經被其他線程獲得,則當前線程將被禁用以進行線程調度,並處於休眠狀態,等待,直到獲取鎖。

如果使用到了lock的話,那么必須去主動釋放鎖,就算發生了異常,也需要我們主動釋放鎖,因為lock並不會像synchronized一樣被自動釋放。所以使用lock的話,必須是在try{}catch(){}中進行,並將釋放鎖的代碼放在finally{}中,以確保鎖一定會被釋放,以防止死鎖現象的發生。

unlock()的作用就是主動釋放鎖。

lock接口的類型有好幾個實現類,這里是隨便找了個哈。

Lock lock = new ReentrantLock();
try {
    lock.lock();
    System.out.println("上鎖了");
}catch (Exception e){
    e.printStackTrace();
}finally {
    lock.unlock();
    System.out.println("解鎖了");
}

3.2、newCondition

關鍵字 synchronized 與 wait()/notify()這兩個方法一起使用可以實現等待/通知模式, Lock 鎖的 newContition()方法返回 Condition 對象,Condition 類 也可以實現等待/通知模式。 用 notify()通知時,JVM 會隨機喚醒某個等待的線程, 使用 Condition 類可以 進行選擇性通知, Condition 比較常用的兩個方法:

  • await():會使當前線程等待,同時會釋放鎖,當等到其他線程調用signal()方法時,此時這個沉睡線程會重新獲得鎖並繼續執行代碼(在哪里沉睡就在哪里喚醒)。
  • signal():用於喚醒一個等待的線程。

注意:在調用 Condition 的 await()/signal()方法前,也需要線程持有相關 的 Lock 鎖,調用 await()后線程會釋放這個鎖,在調用singal()方法后會從當前 Condition對象的等待隊列中,喚醒一個線程,后被喚醒的線程開始嘗試去獲得鎖, 一旦成功獲得鎖就繼續往下執行。

在這個地方我們舉個例子來用代碼寫一下哈:

這里就不舉例synchronized 實現了,道理都差不多。

例子:我們有兩個線程,實現對一個初始值是0的number變量,一個線程當number = =0時 對number值+1,另外一個線程當number = = 1時對number-1。

class Share {

    private Integer number = 0;

    private ReentrantLock lock = new ReentrantLock();

    private Condition newCondition = lock.newCondition();

    // +1 的方法
    public void incr() {
        try {
            lock.lock(); // 加鎖
            while (number != 0) {
                newCondition.await();//沉睡
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            newCondition.signal(); //喚醒另一個沉睡的線程 
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // -1 的方法
    public void decr() {
        try {
            lock.lock();
            while (number != 1) {
                newCondition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            newCondition.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class LockDemo2 {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(()->{
            for (int i=0;i<=10;i++){
                share.incr();
            }
        },"AA").start();

        new Thread(()->{
            for (int i=0;i<=10;i++){
                share.decr();
            }
        },"BB").start();
        /**
         * AA::1
         * BB::0
         * AA::1
         * BB::0
         * .....
         */     
    }
}

四、ReentrantLock (可重入鎖)

ReentrantLock,意思是“可重入鎖”。ReentrantLock 是唯一實現了 Lock 接口的類,並且 ReentrantLock 提供了更 多的方法。

可重入鎖:什么是 “可重入”,可重入就是說某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖。

package com.crush.juc02;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println("第1次獲取鎖,這個鎖是:" + lock);
                    for (int i = 2;i<=11;i++){
                        try {
                            lock.lock();
                            System.out.println("第" + i + "次獲取鎖,這個鎖是:" + lock);
                            try {
                                Thread.sleep(new Random().nextInt(200));
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        } finally {
                           lock.unlock();// 如果把這里注釋掉的話,那么程序就會陷入死鎖當中。
                        }
                    }

                } finally {
                    lock.unlock();
                }
            }
        }).start();

		new Thread(new Runnable() {

			@Override
			public void run() {
				try {
					lock.lock();
                    System.out.println("這里是為了測試死鎖而多寫一個的線程");
				} finally {
					lock.unlock();
				}
			}
		}).start();
    }
}
/**
 * 第1次獲取鎖,這個鎖是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * 第2次獲取鎖,這個鎖是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * 第3次獲取鎖,這個鎖是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
 * ...
 */

死鎖的話,程序就無法停止,直到資源耗盡或主動終止。

在這里插入圖片描述

代碼中也稍微提了一下死鎖的概念,在使用Lock中必須手動解鎖,不然就會可能造成死鎖的現象。

五、ReadWriteLock (讀寫鎖)

ReadWriteLock 也是一個接口,在它里面只定義了兩個方法:

public interface ReadWriteLock {
	
    // 獲取讀鎖
    Lock readLock();

	// 獲取寫鎖
    Lock writeLock();
}

分為一個讀鎖一個寫鎖,將讀寫進行了分離,使可以多個線程進行讀操作,從而提高了效率。

ReentrantReadWriteLock 實現了 ReadWriteLock 接口。里面提供了更豐富的方法,當然最主要的還是獲取寫鎖(writeLock)和讀鎖(readLock)。

5.1、案例

假如多個線程要進行讀的操作,我們用Synchronized 來實現的話。

public class SynchronizedDemo2 {

    public static void main(String[] args) {
        final SynchronizedDemo2 test = new SynchronizedDemo2();
        new Thread(()->{
            test.get(Thread.currentThread());
        }).start();

        new Thread(()->{
            test.get(Thread.currentThread());
        }).start();
    }

    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while(System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName()+"正在進行讀操作");
        }
        System.out.println(thread.getName()+"讀操作完畢");
    }
}
/**
 * 輸出
 * Thread-0正在進行讀操作
 * Thread-0讀操作完畢
 * Thread-1正在進行讀操作
 * Thread-1正在進行讀操作
 * Thread-1正在進行讀操作
 * ....
 * Thread-1讀操作完畢
 */

改成讀寫鎖之后

public class SynchronizedDemo2 {

    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final SynchronizedDemo2 test = new SynchronizedDemo2();
        new Thread(()->{
            test.get2(Thread.currentThread());
        }).start();

        new Thread(()->{
            test.get2(Thread.currentThread());
        }).start();
    }

    public void get2(Thread thread) {
        rwl.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while(System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName()+"正在進行讀操作");
            }
            System.out.println(thread.getName()+"讀操作完畢");
        } finally {
            rwl.readLock().unlock();
        }
    }
}
/**
 * 輸出
 * Thread-0正在進行讀操作
 * Thread-0讀操作完畢
 * Thread-1正在進行讀操作
 * Thread-1讀操作完畢
 */

結論:改用讀寫鎖后 線程1和線程2 同時在讀,可以感受到效率的明顯提升。

注意:

  1. 若此時已經有一個線程占用了讀鎖,此時其他線程申請讀鎖是可以的,但是若此時其他線程申請寫鎖,則只有等待讀鎖釋放,才能成功獲得。
  2. 若此時已經有一個線程占用了寫鎖,那么此時其他線程申請寫鎖或讀鎖,都只有持有寫鎖的線程釋放寫鎖,才能成功獲得。

六、Lock 與的 Synchronized 區別

類別 synchronized Lock
存在層次 Java的關鍵字,在jvm層面上 是一個接口
鎖的獲取 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,線程可以不用一直等待
鎖的釋放 1、當 synchronized 方法或者 synchronized 代碼塊執行完之后, 系統會自動讓線程釋放對鎖的占用 (不需要手動釋放鎖)2、若線程執行發生異常,jvm會讓線程釋放鎖 在finally中必須釋放鎖,不然容易造成線程死鎖現象 (需要手動釋放鎖)
鎖狀態 無法判斷 可以判斷
鎖類型 鎖類型 可重入 可判斷 可公平(兩者皆可)
性能 前提:大量線程情況下 同步效率較低 前提:大量線程情況下 同步效率比synchronized高的多

Lock可以提高多個線程進行讀操作的效率。


七、自言自語

最近又開始了JUC的學習,感覺Java內容真的很多,但是為了能夠走的更遠,還是覺得應該需要打牢一下基礎。

正在持續更新中,如果你覺得對你有所幫助,也感興趣的話,關注我吧,讓我們一起學習,一起討論吧。

你好,我是博主寧在春,Java學習路上的一顆小小的種子,也希望有一天能扎根長成蒼天大樹。

希望與君共勉😁

待我們,別時相見時,都已有所成

博客園 | 寧在春

簡書 | 寧在春

CSDN | 寧在春

掘金 | 寧在春

知乎 | 寧在春


免責聲明!

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



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