多線程系列(十) -ReadWriteLock用法詳解

一、摘要

在上篇文章中,我們講到ReentrantLock可以保證了只有一個線程能執行加鎖的代碼。

但是有些時候,這種保護顯的有點過頭,比如下面這個方法,它僅僅就是隻讀取數據,不修改數據,它實際上允許多個線程同時調用的。

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;

    public int get() {
        // 加鎖
        lock.lock();
        try {
            return count;
        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }
}

站在程序性能的角度,實際上我們想要的是這樣的效果。

  • 1.讀和讀之間不互斥,因爲只讀操作不會有數據安全問題
  • 2.寫和寫之間互斥,避免一個寫操作影響另外一個寫操作,引發數據計算錯誤問題
  • 3.讀和寫之間互斥,避免讀操作的時候寫操作修改了內容,引發數據髒讀問題

總結起來就是,允許多個線程同時讀,但只要有一個線程在寫,其他線程就必須排隊等待。

在 JDK 中有一個讀寫鎖ReadWriteLock,使用它就可以解決這個問題,它可以保證以下兩點:

  • 1.只允許一個線程寫入,其他線程既不能寫入也不能讀取
  • 2.沒有寫入時,多個線程允許同時讀,可以提高程序併發性能

實際上,讀寫鎖ReadWriteLock裏面有兩個鎖實現,一個是讀操作相關的鎖,稱爲共享鎖,當多個線程同時操作時,不會讓多個線程進行排隊等待,大大的提升了程序併發讀的執行效率;另一個是寫操作相關的鎖,稱爲排他鎖,當多個線程同時操作時,只允許一個線程寫入,其他線程進入排隊等待;兩者進行組合操作,就可以實現上面的預期效果。

下面我們一起來看看它的基本用法!

二、ReadWriteLock 基本用法

2.1、讀和讀共享

讀和讀之間不互斥,當多個線程進行讀的時候,不會讓多個線程進行排隊等待。

我們可以看一個簡單的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void read() {
        // 加讀鎖
        lock.readLock().lock();
        try {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了讀鎖,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放讀鎖
            lock.readLock().unlock();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        });

        threadA.start();
        threadB.start();
    }
}

看一下運行結果:

2023-10-23 16:12:28:119 當前線程:Thread-0獲得了讀鎖,count:0
2023-10-23 16:12:28:119 當前線程:Thread-1獲得了讀鎖,count:0

從日誌時間上可以很清晰的看到,儘管加鎖了,並且休眠了 5 秒,但是兩個線程還是幾乎同時執行try()方法裏面的代碼,證明了讀和讀之間是不互斥的,可以顯著提高程序的運行效率。

2.2、寫和寫之間互斥

寫和寫之間互斥,當多個線程進行寫的時候,只允許一個線程寫入,其他線程進入排隊等待。

我們可以看一個簡單的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void write() {
        // 加寫鎖
        lock.writeLock().lock();
        try {
            count++;
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了寫鎖,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放寫鎖
            lock.writeLock().unlock();
        }
    }
}
public class MyThreadTest {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        });

        threadA.start();
        threadB.start();
    }
}

看一下運行結果:

2023-10-23 16:29:59:103 當前線程:Thread-0獲得了寫鎖,count:1
2023-10-23 16:30:04:108 當前線程:Thread-1獲得了寫鎖,count:2

從日誌時間上可以很清晰的看到,兩個線程進行串行執行,證明了寫和寫之間是互斥的。

2.3、讀和寫之間互斥

讀和寫之間互斥,當多個線程交替進行讀寫的時候,操作上互斥,只有一個線程能進入,其他線程進入排隊等待。

我們可以看一個簡單的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void read() {
        // 加讀鎖
        lock.readLock().lock();
        try {
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了讀鎖,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放讀鎖
            lock.readLock().unlock();
        }
    }

    public void write() {
        // 加寫鎖
        lock.writeLock().lock();
        try {
            count++;
            String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
            System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了寫鎖,count:" + count);
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 釋放寫鎖
            lock.writeLock().unlock();
        }
    }
}

public class MyThreadTest {

    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        });

        threadA.start();
        threadB.start();
    }
}

看一下運行結果:

2023-10-23 16:36:08:786 當前線程:Thread-0獲得了讀鎖,count:0
2023-10-23 16:36:13:791 當前線程:Thread-1獲得了寫鎖,count:1

從日誌時間上可以很清晰的看到,兩個線程進行串行執行,證明了讀和寫之間是互斥的。

三、小結

總結下來,ReadWriteLock有以下特點:

  • 允許多個線程在沒有寫入時同時讀取,可以提高讀取效率
  • 當存在寫入情況時,只允許一個線程寫入,其他線程進入排隊等待
  • 適合讀多寫少的場景

對於同一個數據,有大量線程讀取,但僅有少數線程修改,使用ReadWriteLock可以顯著的提升程序併發執行效率。

例如,一個論壇的帖子,瀏覽可以看做讀取操作,是非常頻繁的,而回復可以看做寫入操作,它是不頻繁的,這種情況就可以使用ReadWriteLock來實現。

本文主要圍繞ReadWriteLock的基本使用做了一次知識總結,如果有不正之處,請多多諒解,並歡迎批評指出。

四、參考

1、https://www.cnblogs.com/xrq730/p/4855631.html

2、https://www.liaoxuefeng.com/wiki/1252599548343744/1306581002092578

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