1. 程式人生 > >Java併發ReadWriteLock讀寫鎖的與Synchronized

Java併發ReadWriteLock讀寫鎖的與Synchronized

說到Java併發程式設計,很多開發第一個想到同時也是經常常用的肯定是Synchronized,但是小編這裡提出一個問題,Synchronized存在明顯的一個性能問題就是讀與讀之間互斥,簡言之就是,我們程式設計想要實現的最好效果是,可以做到讀和讀互不影響,讀和寫互斥,寫和寫互斥,提高讀寫的效率,如何實現呢?

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讀寫鎖的實現中,需要注意的,當有讀鎖時,寫鎖就不能獲得;而當有寫鎖時,除了獲得寫鎖的這個執行緒可以獲得讀鎖外,其他執行緒不能獲得讀鎖

作者:itbird01 連結:https://www.jianshu.com/p/9cd5212c8841 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。