Java多線程13:讀寫鎖和兩種同步方式的對比


讀寫鎖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應該是差不多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM