Java多執行緒13:讀寫鎖和兩種同步方式的對比

五月的倉頡發表於2015-10-05

讀寫鎖ReentrantReadWriteLock概述

大型網站中很重要的一塊內容就是資料的讀寫,ReentrantLock雖然具有完全互斥排他的效果(即同一時間只有一個執行緒正在執行lock後面的任務),但是效率非常低。所以在JDK中提供了一種讀寫鎖ReentrantReadWriteLock,使用它可以加快執行效率。

讀寫鎖表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;另一個是寫操作相關的鎖,稱為排他鎖。我把這兩個操作理解為三句話:

1、讀和讀之間不互斥,因為讀操作不會有執行緒安全問題

2、寫和寫之間互斥,避免一個寫操作影響另外一個寫操作,引發執行緒安全問題

3、讀和寫之間互斥,避免讀操作的時候寫操作修改了內容,引發執行緒安全問題

總結起來就是,多個Thread可以同時進行讀取操作,但是同一時刻只允許一個Thread進行寫入操作

 

讀和讀共享

先證明一下第一句話"讀和讀之間不互斥",舉一個簡單的例子:

public class ThreadDomain48 extends ReentrantReadWriteLock
{        
    public void read()
    {
        try
        {
            readLock().lock();
            System.out.println(Thread.currentThread().getName() + "獲得了讀鎖, 時間為" + 
                    System.currentTimeMillis());
            Thread.sleep(10000);
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            readLock().unlock();
        }
    }
}
public static void main(String[] args)
{
    final ThreadDomain48 td = new ThreadDomain48();
    Runnable readRunnable = new Runnable()
    {
        public void run()
        {
            td.read();
        }
    };
    Thread t0 = new Thread(readRunnable);
    Thread t1 = new Thread(readRunnable);
    t0.start();
    t1.start();
}

看一下執行結果:

Thread-0獲得了讀鎖, 時間為1444019668424
Thread-1獲得了讀鎖, 時間為1444019668424

儘管方法加了鎖,還休眠了10秒,但是兩個執行緒還是幾乎同時執行lock()方法後面的程式碼,看時間就知道了。說明lock.readLock()讀鎖可以提高程式執行效率,允許多個執行緒同時執行lock()方法後面的程式碼

 

寫和寫互斥

再證明一下第二句話"寫和寫之間互斥",類似的證明方法:

public class ThreadDomain48 extends ReentrantReadWriteLock
{        
    public void write()
    {
        try
        {
            writeLock().lock();
            System.out.println(Thread.currentThread().getName() + "獲得了寫鎖, 時間為" + 
                    System.currentTimeMillis());
            Thread.sleep(10000);
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            writeLock().unlock();
        }
    }
}
public static void main(String[] args)
{
    final ThreadDomain48 td = new ThreadDomain48();
    Runnable readRunnable = new Runnable()
    {
        public void run()
        {
            td.write();
        }
    };
    Thread t0 = new Thread(readRunnable);
    Thread t1 = new Thread(readRunnable);
    t0.start();
    t1.start();
}

看一下執行結果:

Thread-0獲得了寫鎖, 時間為1444021393325
Thread-1獲得了寫鎖, 時間為1444021403325

從時間上就可以看出來,10000ms即10s,和程式碼裡一致,證明了讀和讀之間是互斥的

 

讀和寫互斥

最後證明一下第三句話"讀和寫之間互斥",證明方法無非是把上面二者結合起來而已,看一下:

public class ThreadDomain48 extends ReentrantReadWriteLock
{        
    public void write()
    {
        try
        {
            writeLock().lock();
            System.out.println(Thread.currentThread().getName() + "獲得了寫鎖, 時間為" + 
                    System.currentTimeMillis());
            Thread.sleep(10000);
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            writeLock().unlock();
        }
    }
    
    public void read()
    {
        try
        {
            readLock().lock();
            System.out.println(Thread.currentThread().getName() + "獲得了讀鎖, 時間為" + 
                    System.currentTimeMillis());
            Thread.sleep(10000);
        } 
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            readLock().unlock();
        }
    }
}
public static void main(String[] args)
    {
        final ThreadDomain48 td = new ThreadDomain48();
        Runnable readRunnable = new Runnable()
        {
            public void run()
            {
                td.read();
            }
        };
        Runnable writeRunnable = new Runnable()
        {
            public void run()
            {
                td.write();
            }
        };
        Thread t0 = new Thread(readRunnable);
        Thread t1 = new Thread(writeRunnable);
        t0.start();
        t1.start();
    }

看一下執行結果:

Thread-0獲得了讀鎖, 時間為1444021679203
Thread-1獲得了寫鎖, 時間為1444021689204

從時間上看,也是10000ms即10s,和程式碼裡面是一致的,證明了讀和寫之間是互斥的。注意一下,"讀和寫互斥"和"寫和讀互斥"是兩種不同的場景,但是證明方式和結論是一致的,所以就不證明了。

 

synchronized和ReentrantLock的對比

到現在,看到多執行緒中,鎖定的方式有2種:synchronized和ReentrantLock。兩種鎖定方式各有優劣,下面簡單對比一下:

1、synchronized是關鍵字,就和if...else...一樣,是語法層面的實現,因此synchronized獲取鎖以及釋放鎖都是Java虛擬機器幫助使用者完成的;ReentrantLock是類層面的實現,因此鎖的獲取以及鎖的釋放都需要使用者自己去操作。特別再次提醒,ReentrantLock在lock()完了,一定要手動unlock()

2、synchronized簡單,簡單意味著不靈活,而ReentrantLock的鎖機制給使用者的使用提供了極大的靈活性。這點在Hashtable和ConcurrentHashMap中體現得淋漓盡致。synchronized一鎖就鎖整個Hash表,而ConcurrentHashMap則利用ReentrantLock實現了鎖分離,鎖的只是segment而不是整個Hash表

3、synchronized是不公平鎖,而ReentrantLock可以指定鎖是公平的還是非公平的

4、synchronized實現等待/通知機制通知的執行緒是隨機的,ReentrantLock實現等待/通知機制可以有選擇性地通知

5、和synchronized相比,ReentrantLock提供給使用者多種方法用於鎖資訊的獲取,比如可以知道lock是否被當前執行緒獲取、lock被同一個執行緒呼叫了幾次、lock是否被任意執行緒獲取等等

總結起來,我認為如果只需要鎖定簡單的方法、簡單的程式碼塊,那麼考慮使用synchronized,複雜的多執行緒處理場景下可以考慮使用ReentrantLock。當然這只是建議性地,還是要具體場景具體分析的。

最後,檢視了很多資料,JDK1.5版本只有由於對synchronized做了諸多優化,效率上synchronized和ReentrantLock應該是差不多。

相關文章