Java多執行緒11:ReentrantLock的使用和Condition

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

ReentrantLock

ReentrantLock,一個可重入的互斥鎖,它具有與使用synchronized方法和語句所訪問的隱式監視器鎖相同的一些基本行為和語義,但功能更強大。

 

ReentrantLock基本用法

先來看一下ReentrantLock的基本用法:

public class ThreadDomain38
{
    private Lock lock = new ReentrantLock();
    
    public void testMethod()
    {
        try
        {
            lock.lock();
            for (int i = 0; i < 2; i++)
            {
                System.out.println("ThreadName = " + Thread.currentThread().getName() + 
                        ", i  = " + i);
            }
        }
        finally
        {
            lock.unlock();
        }
    }
}
public class MyThread38 extends Thread
{
    private ThreadDomain38 td;
    
    public MyThread38(ThreadDomain38 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.testMethod();
    }
}
public static void main(String[] args)
{
    ThreadDomain38 td = new ThreadDomain38();
    MyThread38 mt0 = new MyThread38(td);
    MyThread38 mt1 = new MyThread38(td);
    MyThread38 mt2 = new MyThread38(td);
    mt0.start();
    mt1.start();
    mt2.start();
}

看一下執行結果:

ThreadName = Thread-1, i  = 0
ThreadName = Thread-1, i  = 1
ThreadName = Thread-0, i  = 0
ThreadName = Thread-0, i  = 1
ThreadName = Thread-2, i  = 0
ThreadName = Thread-2, i  = 1

沒有任何的交替,資料都是分組列印的,說明了一個執行緒列印完畢之後下一個執行緒才可以獲得鎖去列印資料,這也證明了ReentrantLock具有加鎖的功能

 

ReentrantLock持有的是物件監視器

前面已經證明了ReentrantLock具有加鎖功能,但我們還不知道ReentrantLock持有的是什麼鎖,因此寫個例子看一下:

public class ThreadDomain39
{
    private Lock lock = new ReentrantLock();
    
    public void methodA()
    {
        try
        {
            lock.lock();
            System.out.println("MethodA begin ThreadName = " + Thread.currentThread().getName());
            Thread.sleep(5000);
            System.out.println("MethodA end ThreadName = " + Thread.currentThread().getName());
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            lock.unlock();
        }
        
    }
    
    public void methodB()
    {
        lock.lock();
        System.out.println("MethodB begin ThreadName = " + Thread.currentThread().getName());
        System.out.println("MethodB begin ThreadName = " + Thread.currentThread().getName());
        lock.unlock();
    }
}

寫兩個執行緒分別呼叫methodA()和methodB()方法:

public class MyThread39_0 extends Thread
{
    private ThreadDomain39 td;
    
    public MyThread39_0(ThreadDomain39 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodA();
    }
}
public class MyThread39_1 extends Thread
{
    private ThreadDomain39 td;
    
    public MyThread39_1(ThreadDomain39 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.methodB();
    }
}

寫一個main函式啟動這兩個執行緒:

public static void main(String[] args)
{
    ThreadDomain39 td = new ThreadDomain39();
    MyThread39_0 mt0 = new MyThread39_0(td);
    MyThread39_1 mt1 = new MyThread39_1(td);
    mt0.start();
    mt1.start();
}

看一下執行結果:

MethodB begin ThreadName = Thread-1
MethodB begin ThreadName = Thread-1
MethodA begin ThreadName = Thread-0
MethodA end ThreadName = Thread-0

看不見時間,不過第四確實是格了5秒左右才列印出來的。從結果來看,已經證明了ReentrantLock持有的是物件監視器,可以寫一段程式碼進一步證明這一結論,即去掉methodB()內部和鎖相關的程式碼,只留下兩句列印語句:

MethodA begin ThreadName = Thread-0
MethodB begin ThreadName = Thread-1
MethodB begin ThreadName = Thread-1
MethodA end ThreadName = Thread-0

看到交替列印了,進一步證明了ReentrantLock持有的是"物件監視器"的結論。

不過注意一點,ReentrantLock雖然持有物件監視器,但是和synchronized持有的物件監視器不是一個意思,雖然我也不清楚兩個持有的物件監視器有什麼區別,不過把methodB()方法用synchronized修飾,methodA()不變,兩個方法還是非同步執行的,所以就記一個結論吧----ReentrantLock和synchronized持有的物件監視器不同

另外,千萬別忘了,ReentrantLock持有的鎖是需要手動去unlock()的

 

Condition

synchronized與wait()和nitofy()/notifyAll()方法相結合可以實現等待/通知模型,ReentrantLock同樣可以,但是需要藉助Condition,且Condition有更好的靈活性,具體體現在:

1、一個Lock裡面可以建立多個Condition例項,實現多路通知

2、notify()方法進行通知時,被通知的執行緒時Java虛擬機器隨機選擇的,但是ReentrantLock結合Condition可以實現有選擇性地通知,這是非常重要的

看一下利用Condition實現等待/通知模型的最簡單用法,下面的程式碼注意一下,await()和signal()之前,必須要先lock()獲得鎖,使用完畢在finally中unlock()釋放鎖,這和wait()/notify()/notifyAll()使用前必須先獲得物件鎖是一樣的:

public class ThreadDomain40
{
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    
    public void await()
    {
        try
        {
            lock.lock();
            System.out.println("await時間為:" + System.currentTimeMillis());
            condition.await();
            System.out.println("await等待結束");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            lock.unlock();
        }
    }
    
    public void signal()
    {
        try
        {
            lock.lock();
            System.out.println("signal時間為:" + System.currentTimeMillis());
            condition.signal();
        }
        finally
        {
            lock.unlock();
        }
    }
}
public class MyThread40 extends Thread
{
    private ThreadDomain40 td;
    
    public MyThread40(ThreadDomain40 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.await();
    }
}
public static void main(String[] args) throws Exception
{
    ThreadDomain40 td = new ThreadDomain40();
    MyThread40 mt = new MyThread40(td);
    mt.start();
    Thread.sleep(3000);
    td.signal();
}

看一下執行結果:

await時間為:1443970329524
signal時間為:1443970332524
await等待結束

差值是3000毫秒也就是3秒,符合程式碼預期,成功利用ReentrantLock的Condition實現了等待/通知模型。其實這個例子還證明了一點,Condition的await()方法是釋放鎖的,原因也很簡單,要是await()方法不釋放鎖,那麼signal()方法又怎麼能呼叫到Condition的signal()方法呢?

注意要是用一個Condition的話,那麼多個執行緒被該Condition給await()後,呼叫Condition的signalAll()方法喚醒的是所有的執行緒。如果想單獨喚醒部分執行緒該怎麼辦呢?new出多個Condition就可以了,這樣也有助於提升程式執行的效率。使用多個Condition的場景是很常見的,像ArrayBlockingQueue裡就有。

相關文章