目錄

  • 1. ReentrantLock簡介
  • 2.ReentrantLock和synchronized的比較
  • 2.1 相同點
  • 2.2不同點
  • 3.ReentrantLock相比synchronized的額外功能
  • 3.1 ReentrantLock可以實現公平鎖。
  • 3.2 .ReentrantLock可響應中斷
  • 3.3 獲取鎖時限時等待
  • 4.結合Condition實現等待通知機制
  • 4.1 Condition使用簡介
  • 4.2 使用Condition實現簡單的阻塞隊列
  • 5. 總結

一.ReentrantLock簡介

jdk中獨佔鎖的實現除了使用關鍵字synchronized外,還可以使用ReentrantLock。雖然在性能上ReentrantLock和synchronized沒有什麼區別,但ReentrantLock相比synchronized而言功能更加豐富,使用起來更為靈活,也更適合複雜的併發場景。

接口:

Modifier and Type

Method

Description

void

lock()

獲取鎖

void

lockInterruptibly()

除非當前線程被中斷,否則獲取鎖定

Condition

newCondition()

返回綁定到此Lock實例的新Condition實例

boolean

tryLock()

只有在調用時它是空閒的才能獲取鎖

boolean

tryLock(long time, TimeUnit unit)

如果在給定的等待時間內空閒並且當前線程未被中斷,則獲取鎖

void

unlock()

釋放鎖

二.ReentrantLock和synchronized的比較 ↑top

1.相同點 ↑top

  • a.它們都是加鎖方式同步;
  • b.都是重入鎖;
  • c.阻塞式的同步;也就是説當如果一個線程獲得了對象鎖,進入了同步塊,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進行線程阻塞和喚醒的代價是比較高的

可重入性

public class ReentrantLockTest {

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

        ReentrantLock lock = new ReentrantLock();

        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }

        for(int i=1;i<=3;i++){
            try {

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

上面的代碼通過lock()方法先獲取鎖三次,然後通過unlock()方法釋放鎖3次,程序可以正常退出。

從上面的例子可以看出,ReentrantLock是可以重入的鎖,當一個線程獲取鎖時,還可以接着重複獲取多次。在加上ReentrantLock的的獨佔性,我們可以得出以下ReentrantLock和synchronized的相同點。

  • 1.ReentrantLock和synchronized都是獨佔鎖,只允許線程互斥的訪問臨界區。但是實現上兩者不同:synchronized加鎖解鎖的過程是隱式的,用户不用手動操作,優點是操作簡單,但顯得不夠靈活。一般併發場景使用synchronized的就夠了;ReentrantLock需要手動加鎖和解鎖,且解鎖的操作儘量要放在finally代碼塊中,保證線程正確釋放鎖。ReentrantLock操作較為複雜,但是因為可以手動控制加鎖和解鎖過程,在複雜的併發場景中能派上用場。
  • 2.ReentrantLock和synchronized都是可重入的。synchronized因為可重入因此可以放在被遞歸執行的方法上,且不用擔心線程最後能否正確釋放鎖;而ReentrantLock在重入時要卻確保重複獲取鎖的次數必須和重複釋放鎖的次數一樣,否則可能導致其他線程無法獲得該鎖。

2.不同點 ↑top

比較方面

SynChronized

ReentrantLock(實現了 Lock接口)

原始構成

它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現

它是JDK 1.5之後提供的API層面的互斥鎖類

代碼編寫

採用synchronized不需要用户去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之後,系統會自動讓線程釋放對鎖的佔用,更安全,

而ReentrantLock則必須要用户去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。需要lock()和unlock()方法配合try/finally語句塊來完成,

靈活性

鎖的範圍是整個方法或synchronized塊部分

Lock因為是方法調用,可以跨方法,靈活性更大

等待可中斷

不可中斷,除非拋出異常(釋放鎖方式:

1.代碼執行完,正常釋放鎖;

2.拋出異常,由JVM退出等待

持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,(方法:


1.設置超時方法 tryLock(long timeout, TimeUnit unit),時間過了就放棄等待;


2.lockInterruptibly()放代碼塊中,調用interrupt()方法可中斷,而synchronized不行)

是否公平鎖

非公平鎖

兩者都可以,默認公平鎖,構造器可以傳入boolean值,true為公平鎖,false為非公平鎖,

條件Condition

通過多次newCondition可以獲得多個Condition對象,可以簡單的實現比較複雜的線程同步的功能.

提供的高級功能

提供很多方法用來監聽當前鎖的信息,如:

getHoldCount() getQueueLength() isFair() isHeldByCurrentThread() isLocked()

三. ReentrantLock相比synchronized的額外功能 ↑top

3.1 ReentrantLock可以實現公平鎖。 ↑top

公平鎖是指當鎖可用時,在鎖上等待時間最長的線程將獲得鎖的使用權。而非公平鎖則隨機分配這種使用權。和synchronized一樣,默認的ReentrantLock實現是非公平鎖,因為相比公平鎖,非公平鎖性能更好。當然公平鎖能防止飢餓,某些情況下也很有用。在創建ReentrantLock的時候通過傳進參數true創建公平鎖,如果傳入的是false或沒傳參數則創建的是非公平鎖

ReentrantLock lock = new ReentrantLock(true);

繼續跟進看下源碼

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

可以看到公平鎖和非公平鎖的實現關鍵在於成員變量sync的實現不同,這是鎖實現互斥同步的核心。

公平鎖例子:

public class ReentrantLockTest {

    static Lock lock = new ReentrantLock(true);

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

        for(int i=0;i<5;i++){
            new Thread(new ThreadDemo(i)).start();
        }

    }

    static class ThreadDemo implements Runnable {
        Integer id;

        public ThreadDemo(Integer id) {
            this.id = id;
        }

        @Override

      public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<2;i++){
                lock.lock();
                System.out.println("獲得鎖的線程:"+id);
                lock.unlock();
            }
        }
    }
}

公平鎖結果:

獲得鎖的線程:3
獲得鎖的線程:2
獲得鎖的線程:4
獲得鎖的線程:0
獲得鎖的線程:1
獲得鎖的線程:3
獲得鎖的線程:2
獲得鎖的線程:4
獲得鎖的線程:0
獲得鎖的線程:1

我們開啓5個線程,讓每個線程都獲取釋放鎖兩次。為了能更好的觀察到結果,在每次獲取鎖前讓線程休眠100毫秒。可以看到線程幾乎是輪流的獲取到了鎖。如果我們改成非公平鎖,再看下結果:

獲得鎖的線程:2
獲得鎖的線程:2
獲得鎖的線程:4
獲得鎖的線程:4
獲得鎖的線程:3
獲得鎖的線程:3
獲得鎖的線程:1
獲得鎖的線程:1
獲得鎖的線程:0
獲得鎖的線程:0

線程會重複獲取鎖。如果申請獲取鎖的線程足夠多,那麼可能會造成某些線程長時間得不到鎖。這就是非公平鎖的“飢餓”問題。

  • 公平鎖和非公平鎖該如何選擇
    大部分情況下我們使用非公平鎖,因為其性能比公平鎖好很多。但是公平鎖能夠避免線程飢餓,某些情況下也很有用。

3.2ReentrantLock可響應中斷 ↑top

當使用synchronized實現鎖時,阻塞在鎖上的線程除非獲得鎖否則將一直等待下去,也就是説這種無限等待獲取鎖的行為無法被中斷。而ReentrantLock給我們提供了一個可以響應中斷的獲取鎖的方法lockInterruptibly()。該方法可以用來解決死鎖問題。

public class ReentrantLockDemo02 implements Runnable {

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            lock.lockInterruptibly();
            System.out.println(Thread.currentThread().getName() + " running");
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " finished");
            lock.unlock();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + " interrupted");
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo02 reentrantLockDemo = new ReentrantLockDemo02();
        Thread thread01 = new Thread(reentrantLockDemo, "thread01");
        Thread thread02 = new Thread(reentrantLockDemo, "thread02");
        thread01.start();
        thread02.start();
        thread02.interrupt();
    }
}

結果:

thread01 running
thread02 interrupted
thread01 finished

3.3ReentrantLock獲取鎖限時等待 ↑top

ReentrantLock還給我們提供了獲取鎖限時等待的方法tryLock(),可以選擇傳入時間參數,表示等待指定的時間,無參則表示立即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。我們可以使用該方法配合失敗重試機制來更好的解決死鎖問題。

public class ReentrantLockDemo03 implements Runnable {

    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        try {
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                System.out.println(Thread.currentThread().getName() + " 獲取當前lock鎖");
                TimeUnit.SECONDS.sleep(4);
            } else {
                System.out.println(Thread.currentThread().getName()+ " 獲取鎖失敗");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }


    public static void main(String[] args) {
        ReentrantLockDemo03 reentrantLockDemo = new ReentrantLockDemo03();
        Thread thread01 = new Thread(reentrantLockDemo, "thread01");
        Thread thread02 = new Thread(reentrantLockDemo, "thread02");
        thread01.start();
        thread02.start();
    }
}

運行結果:

thread02 獲取當前lock鎖
thread01 獲取鎖失敗

線程2運行,首先獲得鎖,休眠4秒鐘後釋放所,線程1如果在2秒內如果沒有獲取到鎖,則會輸出獲取鎖失敗

如果將線程2的休眠時間小於1秒或者線程1的等待時間大於4秒此時兩者都會成功

thread02 獲取當前lock鎖
thread01 獲取當前lock鎖

四. 結合Condition實現等待通知機制 ↑top

4.1 Condition使用簡介 ↑top

Condition由ReentrantLock對象創建,並且可以同時創建多個

static Condition notEmpty = lock.newCondition();

static Condition notFull = lock.newCondition();

Condition接口在使用前必須先調用ReentrantLock的lock()方法獲得鎖。之後調用Condition接口的await()將釋放鎖,並且在該Condition上等待,直到有其他線程調用Condition的signal()方法喚醒線程。使用方式和wait,notify類似。

ublic class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    public static void main(String[] args) throws InterruptedException {

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("主線程等待通知");
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println("主線程恢復運行");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("子線程通知");
            } finally {
                lock.unlock();
            }
        }
    }
}

運行結果:

主線程等待通知
子線程通知
主線程恢復運行

4.2 使用Condition實現簡單的阻塞隊列 ↑top

阻塞隊列是一種特殊的先進先出隊列,它有以下幾個特點
1.入隊和出隊線程安全
2.當隊列滿時,入隊線程會被阻塞;當隊列為空時,出隊線程會被阻塞。

public class MyBlockingQueue<E> {

    int size;//阻塞隊列最大容量

    ReentrantLock lock = new ReentrantLock();

    LinkedList<E> list=new LinkedList<>();//隊列底層實現

    Condition notFull = lock.newCondition();//隊列滿時的等待條件
    Condition notEmpty = lock.newCondition();//隊列空時的等待條件

    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() ==size)//隊列已滿,在notFull條件上等待
                notFull.await();
            list.add(e);//入隊:加入鏈表末尾
            System.out.println("入隊:" +e);
            notEmpty.signal(); //通知在notEmpty條件上等待的線程
        } finally {
            lock.unlock();
        }
    }

    public E dequeue() throws InterruptedException {
        E e;
        lock.lock();
        try {
            while (list.size() == 0)//隊列為空,在notEmpty條件上等待
                notEmpty.await();
            e = list.removeFirst();//出隊:移除鏈表首元素
            System.out.println("出隊:"+e);
            notFull.signal();//通知在notFull條件上等待的線程
            return e;
        } finally {
            lock.unlock();
        }
    }
}

測試代碼:

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

    MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
    for (int i = 0; i < 10; i++) {
        int data = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    queue.enqueue(data);
                } catch (InterruptedException e) {

                }
            }
        }).start();

    }
    for(int i=0;i<10;i++){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Integer data = queue.dequeue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

運行結果:

入隊:3
出隊:3
入隊:4
入隊:8
出隊:4
入隊:1
出隊:8
入隊:0
出隊:1
出隊:0
入隊:9
出隊:9
入隊:2
入隊:5
出隊:2
出隊:5
入隊:7
入隊:6
出隊:7
出隊:6

五.總結 ↑top

ReentrantLock是可重入的獨佔鎖。比起synchronized功能更加豐富,支持公平鎖實現,支持中斷響應以及限時等待等等。可以配合一個或多個Condition條件方便的實現等待通知機制。

什麼情況下使用ReenTrantLock:

答案是,如果你需要實現ReenTrantLock的三個獨有功能時。

參考:

  • https://www.jianshu.com/p/c021f144a565