Java-ReadWriteLock讀寫鎖的使用

Java併發包中ReadWriteLock是一個接口,主要有兩個方法,如下:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReadWriteLock管理一組鎖,一個是隻讀的鎖,一個是寫鎖。
Java併發庫中ReetrantReadWriteLock實現了ReadWriteLock接口並添加了可重入的特性。
在具體講解ReetrantReadWriteLock的使用方法前,我們有必要先對其幾個特性進行一些深入學習瞭解。

1. ReetrantReadWriteLock特性說明

1.1 獲取鎖順序

  • 非公平模式(默認)
    當以非公平初始化時,讀鎖和寫鎖的獲取的順序是不確定的。非公平鎖主張競爭獲取,可能會延緩一個或多個讀或寫線程,但是會比公平鎖有更高的吞吐量。
  • 公平模式
    當以公平模式初始化時,線程將會以隊列的順序獲取鎖。噹噹前線程釋放鎖後,等待時間最長的寫鎖線程就會被分配寫鎖;或者有一組讀線程組等待時間比寫線程長,那麼這組讀線程組將會被分配讀鎖。

1.2 可重入

什麼是可重入鎖,不可重入鎖呢?"重入"字面意思已經很明顯了,就是可以重新進入。可重入鎖,就是說一個線程在獲取某個鎖後,還可以繼續獲取該鎖,即允許一個線程多次獲取同一個鎖。比如synchronized內置鎖就是可重入的,如果A類有2個synchornized方法method1和method2,那麼method1調用method2是允許的。顯然重入鎖給編程帶來了極大的方便。假如內置鎖不是可重入的,那麼導致的問題是:1個類的synchornized方法不能調用本類其他synchornized方法,也不能調用父類中的synchornized方法。與內置鎖對應,JDK提供的顯示鎖ReentrantLock也是可以重入的,這裏通過一個例子着重說下可重入鎖的釋放需要的事兒。

package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final ReentrantReadWriteLock  lock = new ReentrantReadWriteLock ();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                System.out.println("Thread real execute");
                lock.writeLock().unlock();
            }
        });

        lock.writeLock().lock();
        lock.writeLock().lock();
        t.start();
        Thread.sleep(200);
        
        System.out.println("realse one once");
        lock.writeLock().unlock();
    }

}

 

運行結果.png


從運行結果中,可以看到,程序並未執行線程的run方法,由此我們可知,上面的代碼會出現死鎖,因爲主線程2次獲取了鎖,但是卻只釋放1次鎖,導致線程t永遠也不能獲取鎖。一個線程獲取多少次鎖,就必須釋放多少次鎖。這對於內置鎖也是適用的,每一次進入和離開synchornized方法(代碼塊),就是一次完整的鎖獲取和釋放。

再次添加一次unlock之後的運行結果.png

 

1.3 鎖降級

要實現一個讀寫鎖,需要考慮很多細節,其中之一就是鎖升級和鎖降級的問題。什麼是升級和降級呢?ReadWriteLock的javadoc有一段話:

Can the write lock be downgraded to a read lock without allowing an intervening writer? Can a read lock be upgraded to a write lock, in preference to other waiting readers or writers?

翻譯過來的結果是:在不允許中間寫入的情況下,寫入鎖可以降級爲讀鎖嗎?讀鎖是否可以升級爲寫鎖,優先於其他等待的讀取或寫入操作?簡言之就是說,鎖降級:從寫鎖變成讀鎖;鎖升級:從讀鎖變成寫鎖,ReadWriteLock是否支持呢?讓我們帶着疑問,進行一些Demo 測試代碼驗證。

Test Code 1

/**
 *Test Code 1
 **/
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.readLock().lock();
        System.out.println("get readLock.");
        rtLock.writeLock().lock();
        System.out.println("blocking");
    }
}

Test Code 1 Result

TestCode1 Result.png

結論:上面的測試代碼會產生死鎖,因爲同一個線程中,在沒有釋放讀鎖的情況下,就去申請寫鎖,這屬於鎖升級,ReentrantReadWriteLock是不支持的

Test Code 2

/**
 *Test Code 2
 **/
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();  
        rtLock.writeLock().lock();  
        System.out.println("writeLock");  
          
        rtLock.readLock().lock();  
        System.out.println("get read lock");  
    }
}

Test Code 2 Result

 

TestCode2 Result.png


結論:ReentrantReadWriteLock支持鎖降級,上面代碼不會產生死鎖。這段代碼雖然不會導致死鎖,但沒有正確的釋放鎖。從寫鎖降級成讀鎖,並不會自動釋放當前線程獲取的寫鎖,仍然需要顯示的釋放,否則別的線程永遠也獲取不到寫鎖。

 

2. ReetrantReadWriteLock對比使用

2.1 Synchronized實現

在使用ReetrantReadWriteLock實現鎖機制前,我們先看一下,多線程同時讀取文件時,用synchronized實現的效果

package test;

/**
 * 
 * synchronized實現
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public synchronized static void get(Thread thread) {
        System.out.println("start time:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在進行讀操作……");
        }
        System.out.println(thread.getName() + ":讀操作完畢!");
        System.out.println("end time:" + System.currentTimeMillis());
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();
    }

}

讓我們看一下運行結果:

synchronized實現的效果結果.png


從運行結果可以看出,兩個線程的讀操作是順序執行的,整個過程大概耗時200ms。

 

2.2 ReetrantReadWriteLock實現

package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * ReetrantReadWriteLock實現
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public static void get(Thread thread) {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        lock.readLock().lock();
        System.out.println("start time:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在進行讀操作……");
        }
        System.out.println(thread.getName() + ":讀操作完畢!");
        System.out.println("end time:" + System.currentTimeMillis());
        lock.readLock().unlock();
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();
    }

}

讓我們看一下運行結果:

ReetrantReadWriteLock實現.png


從運行結果可以看出,兩個線程的讀操作是同時執行的,整個過程大概耗時100ms。
通過兩次實驗的對比,我們可以看出來,ReetrantReadWriteLock的效率明顯高於Synchronized關鍵字。

 

3. ReetrantReadWriteLock讀寫鎖互斥關係

通過上面的測試代碼,我們也可以延伸得出一個結論,ReetrantReadWriteLock讀鎖使用共享模式,即:同時可以有多個線程併發地讀數據。但是另一個問題來了,寫鎖之間是共享模式還是互斥模式?讀寫鎖之間是共享模式還是互斥模式呢?下面讓我們通過Demo進行一一驗證吧。

3.1 ReetrantReadWriteLock讀寫鎖關係

package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * ReetrantReadWriteLock實現
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //同時讀、寫
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                readFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }

    // 讀操作
    public static void readFile(Thread thread) {
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
            System.out.println("當前爲讀鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行讀操作……");
            }
            System.out.println(thread.getName() + ":讀操作完畢!");
        } finally {
            System.out.println("釋放讀鎖!");
            lock.readLock().unlock();
        }
    }

    // 寫操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("當前爲寫鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行寫操作……");
            }
            System.out.println(thread.getName() + ":寫操作完畢!");
        } finally {
            System.out.println("釋放寫鎖!");
            lock.writeLock().unlock();
        }
    }
}

運行結果:

運行結果.png


結論:讀寫鎖的實現必須確保寫操作對讀操作的內存影響。換句話說,一個獲得了讀鎖的線程必須能看到前一個釋放的寫鎖所更新的內容,讀寫鎖之間爲互斥。

 

3.2 ReetrantReadWriteLock寫鎖關係

package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * ReetrantReadWriteLock實現
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //同時寫
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }

    // 讀操作
    public static void readFile(Thread thread) {
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
            System.out.println("當前爲讀鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行讀操作……");
            }
            System.out.println(thread.getName() + ":讀操作完畢!");
        } finally {
            System.out.println("釋放讀鎖!");
            lock.readLock().unlock();
        }
    }

    // 寫操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("當前爲寫鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行寫操作……");
            }
            System.out.println(thread.getName() + ":寫操作完畢!");
        } finally {
            System.out.println("釋放寫鎖!");
            lock.writeLock().unlock();
        }
    }
}

運行結果:

 

運行結果.png

4. 總結

1.Java併發庫中ReetrantReadWriteLock實現了ReadWriteLock接口並添加了可重入的特性
2.ReetrantReadWriteLock讀寫鎖的效率明顯高於synchronized關鍵字
3.ReetrantReadWriteLock讀寫鎖的實現中,讀鎖使用共享模式;寫鎖使用獨佔模式,換句話說,讀鎖可以在沒有寫鎖的時候被多個線程同時持有,寫鎖是獨佔的
4.ReetrantReadWriteLock讀寫鎖的實現中,需要注意的,當有讀鎖時,寫鎖就不能獲得;而當有寫鎖時,除了獲得寫鎖的這個線程可以獲得讀鎖外,其他線程不能獲得讀鎖

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章