1. 程式人生 > >(四)Lock,ReentrantLock,ReentrantReadWriteLock類的使用以及相關api---synchronized進階

(四)Lock,ReentrantLock,ReentrantReadWriteLock類的使用以及相關api---synchronized進階

這篇部落格記錄了Lock,ReentrantLock,ReentrantReadWriteLock類的使用以及其一些api:

碼字不易~~另外《java多執行緒程式設計核心技術》這本書讀著很爽

前言說明:之前為了解決多執行緒時的非執行緒安全問題,使用的是synchronized。接下來記錄的是他的升級版本ReentrantLock,更加靈活,可控性更高,而ReentrantReadWriteLock類是對ReentrantLock類的補充,能夠在某些條件之間之下提交效率

下面先來看下都有哪些api,以及和synchronized之間是怎樣對應的吧。

 

以前使用鎖完成同步是將同步程式碼塊寫在synchronized之內,現在我們使用  

Lock lock = new ReentrantLock();

來宣告一個鎖,他有這兩個方法

lock.lock();  和 lock.unlock();   這兩個是配套的,在其之間的程式碼就是同步程式碼塊。

和之前一樣,lock()方法會讓當前執行緒持有物件監聽器,具體規則之類的和synchronized也一樣,

比如下面的例子,MyService有一段程式碼上鎖,自定義執行緒類呼叫它

MyService.java

package 第四章;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyService {
    private Lock lock = new ReentrantLock();
    public void testMethod(){
        lock.lock();
        for(int i=0;i<5;i++){
            System.out.println(i+"執行緒:"+Thread.currentThread().getName());
        }
        lock.unlock();
    }
}
View Code

MyThread.java

package 第四章;

public class MyThread extends Thread {
    private MyService myService;

    public MyThread(MyService myService) {
        super();
        this.myService = myService;
    }

    public void run(){
        myService.testMethod();
    }
}
View Code

test.java

package 第四章;

public class test {
    public static void main(String[] args){
        MyThread[] threads = new MyThread[5];
        MyService myService = new MyService();
        for(int i=0;i<5;i++){
            threads[i] = new MyThread(myService);
            threads[i].start();
        }
    }
}
View Code

執行結果:

 

 可以看到執行緒之間是同步執行的,當然前提是同一個MyService物件。

之前的wait/notify,用Condition物件來替換:

效率提高的地方以及原因:

Condition物件可以對同一個鎖宣告多個,相當於每當讓執行緒等待時,他都有自己的喚醒condition,換句話說,每一個執行緒都可以註冊一個Condition,這樣當我們喚醒執行緒的時候,就可以喚醒指定的執行緒,比如之前的生產者消費者模型之中的假死現象,我們使用過notifyAll()來解決的,但是這種方法喚醒了所有的執行緒,讓所有執行緒都去爭搶cpu,但是我們事實上指向喚醒異類執行緒,並不想喚醒同類,全部喚醒的話,效率是一個問題。那麼現在,給每一個執行緒都註冊

一個Condition,這樣子喚醒時候,我們就可以喚醒指定的執行緒,提高了效率,也更加靈活。

下面的是一個簡單的await/signal例子,展示了基本的使用:await類似之前的wait,signal類似於notify:signalAll()喚醒全部

更改之前的MyService.java

condition.await()讓執行緒阻塞,condition.signal()隨機喚醒一個由當前condition註冊的執行緒

 

package 第四章;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class MyService {
    private Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();
    public void testMethod(){
        try{
        lock.lock();
        System.out.println("即將開始迴圈");
        condition.await();
        for(int i=0;i<2;i++){
            System.out.println(i+"執行緒:"+Thread.currentThread().getName());
        }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void signal(){
        try{
            lock.lock();
            this.condition.signal();
            System.out.println("喚醒了一個執行緒");
        }finally {
            lock.unlock();
        }
    }
}
View Code

 

MyThread.java不變

test.java:先讓執行緒全部阻塞,然後呼叫自定義的signal方法喚醒執行緒,

package 第四章;

public class test {
    public static void main(String[] args){
        MyThread[] threads = new MyThread[5];
        MyService myService = new MyService();
        for(int i=0;i<5;i++){
            threads[i] = new MyThread(myService);
            threads[i].start();
        }
        try{
            Thread.sleep(1000);
            myService.signal();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
View Code

執行結果如下:

可以看到,我們成功喚醒了一個執行緒。

下面的例子喚醒了一個指定的執行緒

MyService.java:根據當前執行緒的名字讓指定的Condition物件等待,並書寫兩個喚醒不同的Condition物件註冊的執行緒

package 第四章;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class MyService {
    private Lock lock = new ReentrantLock();
    public Condition conditionA = lock.newCondition();
    public Condition conditionB = lock.newCondition();
    public void testMethod(){
        try{
            lock.lock();
            System.out.println("執行緒"+Thread.currentThread().getName()+"等待中...");
            if(Thread.currentThread().getName().equals("A"))
                conditionA.await();
            else
                conditionB.await();
            for(int i=0;i<2;i++){
                System.out.println(i+"執行緒:"+Thread.currentThread().getName());
        }
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void signalA(){
        try{
            lock.lock();
            this.conditionA.signal();
            System.out.println("喚醒了A執行緒");
        }finally {
            lock.unlock();
        }
    }
    public void signalB(){
        try{
            lock.lock();
            this.conditionB.signal();
            System.out.println("喚醒了B執行緒");
        }finally {
            lock.unlock();
        }
    }
}
View Code

test.java,啟動A,B兩個執行緒,只喚醒A執行緒

package 第四章;

public class test {
    public static void main(String[] args){
        MyService myService = new MyService();
        MyThread myThreadA = new MyThread(myService);
        myThreadA.setName("A");
        MyThread myThreadB = new MyThread(myService);
        myThreadB.setName("B");
        myThreadA.start();
        myThreadB.start();
        try{
            Thread.sleep(1000);
            myService.signalA();
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
View Code

執行結果:

根據程式碼,我們可以看到可以通過不同Condition物件來喚醒指定的執行緒。

用處:

1.可以想到,如果用Lock來解決之前的多消費多生產者時的假死問題,我們可以將生產者統一註冊一個Condition,消費者統一註冊一個Condition,每一次喚醒對方的Condition,這樣子就不會出現連續喚醒同類導致假死的情況了,並且可以避免喚醒所有執行緒,導致效率低下。

2.我們也可以按照我們想要的順序進行喚醒,只要你註冊了正確的Condition物件

 

公平鎖和非公平鎖:

比較好理解,公平鎖相當於一個佇列,先進先出,先執行的執行緒先拿到鎖,後執行的後拿到鎖,按照順序來,非公平鎖就是鎖的搶佔是隨機的,沒有順序。

預設是非公平鎖,建立Lock時加上true引數即為公平鎖:

Lock lock =new ReentrantLock(true);

 

下面介紹一些ReentrantLock的api,

一般在一些定製化的情況可能會用到,emmm,這塊先了解一下,知道有這些就行,emmm,說實話目前我感覺這個沒啥用,有個印象,不過注意使用這些API使,必須以下面這種方式new物件

ReentrantLock lock = new ReentrantLock();

(lock.)GetHoldCount():查詢當前執行緒有保持著幾個lock鎖,簡單來講就是當前執行緒呼叫了幾次lock()方法

GetQueueLength():有多少個執行緒在等待獲取當前鎖,可以理解為有多少個沒有拿到當前鎖,

getWaitQueueLength(Condition condition):有多少個執行緒處於阻塞狀態,並且是執行了引數Condition物件所對應的await()方法導致阻塞的。

hasQueuedThread(Thread thread):查詢指定的執行緒是否正在等待獲取當前鎖

hasQueuedThreads():查詢是否有執行緒正在等待獲取當前鎖

hasWaiters(Condition):查詢是否有執行緒是由於呼叫了引數Condition.await()導致阻塞的。

isHeldByCurrentThread():查詢當前執行緒是否持有當前鎖

isLocked():當前鎖是否被某個執行緒持有

awaitUninterruptibly():這也是一種讓當前執行緒阻塞的方法,不過await呼叫之後如果再使用Interrupt等程式碼阻塞當前程序會報異常,但是這個不會,相當於讓當前執行緒變成可以阻塞的執行緒,,,,不懂有撒用

awaitUntil(Date):阻塞當前執行緒,如果在指定時間之前還沒有被喚醒,則喚醒他。引數也可以傳Calendar.getTime(),Calendar類用於處理時間

 

ReentrantReadWriteLock類

之前的ReentrantLock相當於同一時間只有一個執行緒在執行程式碼。但是在不涉及更改例項變數的程式碼之中,我們可以允許非同步執行來加快效率, 而一些涉及到更改例項變數的程式碼,這時候同步執行(這時候非同步可能出現非執行緒安全),這樣可以在一定程度上加快效率,這就是這個類的作用。

簡單來說,我們一般有讀寫兩個操作,如果多個執行緒執行讀操作,ok,非同步執行,如果多個執行緒有的執行讀,有的寫,ok,同步執行,這個類就是自動完成這個事情,你只需要在鎖時使用不同型別的鎖就行。

下面是一個例子,讀讀非同步(其他全部同步):
ReadAndWrite.java  代表具體的操作,讀,寫,輸出當前操作以及時間,sleep()模擬操作耗費的時間

package 第四章;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadAndWrite {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void read(){
        try{
            lock.readLock().lock();
            System.out.println("讀操作"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.readLock().unlock();
        }
    }
    public void write(){
        try{
            lock.writeLock().lock();
            System.out.println("寫操作"+System.currentTimeMillis());
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }finally {
            lock.writeLock().unlock();
        }
    }
}
View Code

MyThread2.java:裡面有兩個java類,一個執行讀操作,一個寫操作

package 第四章;

class MyThreadRead extends Thread{
    private ReadAndWrite readAndWrite;

    public MyThreadRead(ReadAndWrite readAndWrite) {
        this.readAndWrite = readAndWrite;
    }

    public void run(){
        this.readAndWrite.read();
    }
}
class MyThreadWrite extends Thread{
    private ReadAndWrite readAndWrite;

    public MyThreadWrite(ReadAndWrite readAndWrite) {
        this.readAndWrite = readAndWrite;
    }

    public void run(){
        this.readAndWrite.write();
    }
}
View Code

test.java:  建立三個讀執行緒

package 第四章;

public class test {
    public static void main(String[] args){
           ReadAndWrite readAndWrite = new ReadAndWrite();
           MyThreadRead reads[] = new MyThreadRead[3];
           for(int i=0;i<3;i++) {
               reads[i] = new MyThreadRead(readAndWrite);
               reads[i].start();
           }
        }
    }
View Code

執行結果:

可以看到,三個讀操作時同時執行的。

下面更改test.java,建立三個讀執行緒,三個寫執行緒:

test.java

package 第四章;

public class test {
    public static void main(String[] args){

           ReadAndWrite readAndWrite = new ReadAndWrite();
        MyThreadWrite writes[] = new MyThreadWrite[3];
        for(int i=0;i<3;i++) {
            writes[i] = new MyThreadWrite(readAndWrite);
            writes[i].start();
        }
           MyThreadRead reads[] = new MyThreadRead[3];
           for(int i=0;i<3;i++) {
               reads[i] = new MyThreadRead(readAndWrite);
               reads[i].start();
           }

        }
    }
View Code

執行:

 

可以看到,寫操作之間是互斥的,相當於同步,一個一個執行的,讀的時候就是非同步的,

 ,,好嘞,就演示這幾個,其他的都同理,只有讀讀是非同步的,讀寫同步,你可以交替著start看一下,如下:

 

 好滴,第四章就這些暫時。。

&n