11_張孝祥_多線程_線程鎖技術

轉載

Java併發編程:Lock

locks相關類

鎖相關的類都在包java.util.concurrent.locks下,有以下類和接口:

|---AbstractOwnableSynchronizer
|---AbstractQueuedLongSynchronizer
|---AbstractQueuedSynchronizer
|---Condition
|---Lock
|---LockSupport
|---ReadWriteLock
|---ReentrantLock
|---ReentrantReadWriteLock

接口摘要:

接口 摘要
Condition Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意 Lock 實現組合使用,爲每個對象提供多個等待 set(wait-set)。
Lock Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。
ReadWriteLock ReadWriteLock 維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。

類摘要:

摘要
AbstractOwnableSynchronizer 可以由線程以獨佔方式擁有的同步器。
AbstractQueuedLongSynchronizer 以 long 形式維護同步狀態的一個 AbstractQueuedSynchronizer 版本。
AbstractQueuedSynchronizer 爲實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。
LockSupport 用來創建鎖和其他同步類的基本線程阻塞原語。
ReentrantLock 一個可重入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。
ReentrantReadWriteLock 支持與 ReentrantLock 類似語義的 ReadWriteLock 實現。
ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.readLock() 方法返回的鎖。
ReentrantReadWriteLock.WriteLock ReentrantReadWriteLock.writeLock() 方法返回的鎖。

synchronized與lock

synchronized對比lock:
1、synchronized是Java語言的關鍵字屬於內置特性,Lock是一個類
2、使用synchronized不需要用戶去手動釋放鎖,使用Lock需要在finally手動釋放鎖,不然容易造成線程死鎖

詳細對比見下面的表格:

類別 synchronized Lock
存在層次 Java的關鍵字,在jvm層面上 是一個類
鎖的釋放 1、以獲取鎖的線程執行完同步代碼,釋放鎖 2、線程執行發生異常,jvm會讓線程釋放鎖 在finally中必須釋放鎖,不然容易造成線程死鎖
鎖的獲取 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待 分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,線程可以不用一直等待
鎖狀態 無法判斷 可以判斷
鎖類型 可重入 不可中斷 非公平 可重入 可判斷 可公平(兩者皆可)
性能 少量同步 大量同步

常用類

Lock

Lock是一個接口:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

下面來逐個講述Lock接口中每個方法的使用,lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。unLock()方法是用來釋放鎖的。

lock()

  lock()方法是平常使用得最多的一個方法,就是用來獲取鎖。如果鎖已被其他線程獲取,則進行等待。
  由於在前面講到如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。通常使用Lock來進行同步的話,是以下面這種形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //處理任務
}catch(Exception ex){

}finally{
    lock.unlock();   //釋放鎖
}

tryLock()、tryLock(long time, TimeUnit unit)

  tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他線程獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。

  tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。

  所以,一般情況下通過tryLock來獲取鎖時是這樣使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //處理任務
     }catch(Exception ex){

     }finally{
         lock.unlock();   //釋放鎖
     } 
}else {
    //如果不能獲取鎖,則直接做其他事情
}

lockInterruptibly()

  lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應中斷,即中斷線程的等待狀態。也就是說,當兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。

  因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
    }
    finally {
        lock.unlock();
    }  
}

  注意,當一個線程獲取了鎖之後,是不會被interrupt()方法中斷的。因爲本身在前面的文章中講過單獨調用interrupt()方法不能中斷正在運行過程中的線程,只能中斷阻塞過程中的線程。
  因此當通過lockInterruptibly()方法獲取某個鎖時,如果不能獲取到,只有進行等待的情況下,是可以響應中斷的。
  而用synchronized修飾的話,當一個線程處於等待某個鎖的狀態,是無法被中斷的,只有一直等待下去。

鎖類型

Java中存在以下幾種鎖:

  • 可重入鎖:在執行對象中所有同步方法不用再次獲得鎖(可看一個使用示例)

  • 可中斷鎖:在等待獲取鎖過程中可中斷

  • 公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具有優先獲取鎖權利

  • 讀寫鎖:對資源讀取和寫入的時候拆分爲2部分處理,讀的時候可以多線程一起讀,寫的時候必須同步地寫

可重入鎖ReentrantLock

ReentrantLock是唯一實現了Lock接口的類,並且ReentrantLock提供了更多的方法。下面通過一些實例看具體看一下如何使用ReentrantLock。

例子1:lock()的使用
可以類似於Synchronized的用法,定義一個類,新建一個該類的對象用於線程間同步,在類裏面定義鎖的對象。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class LockTest {

    public static void main(String[] args) {
        new LockTest().init();
    }

    private void init(){
        final Outputer outputer = new Outputer();
        new Thread(new Runnable(){
            public void run() {
                while(true){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    outputer.output("zhangxiaoxiang");
                }

            }
        }).start();

        new Thread(new Runnable(){
            public void run() {
                while(true){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    outputer.output("lihuoming");
                }

            }
        }).start();

    }

    static class Outputer{
        Lock lock = new ReentrantLock();
        public void output(String name){
            lock.lock();
            try{
                System.out.println(name);
            }finally{
                lock.unlock();
            }
        }
    }
}

輸出:

zhangxiaoxiang
lihuoming
lihuoming
zhangxiaoxiang
zhangxiaoxiang
lihuoming
...

注意:輸出的字符串順序不定,個數也不定。

例子2:tryLock()的使用
這裏相比例子1只修改了Outputer類,main方法一樣。

static class Outputer{
    Lock lock = new ReentrantLock();
    public void output(String name){
        if (lock.tryLock()) {
            try{
                System.out.println(name + "得到鎖");
            }finally{
                lock.unlock();
                System.out.println(name + "釋放鎖");
            }
        } else {
            System.out.println(name + "獲取鎖失敗");
        }
    }
}

輸出:

lihuoming得到鎖
zhangxiaoxiang獲取鎖失敗
lihuoming釋放鎖
zhangxiaoxiang得到鎖
zhangxiaoxiang釋放鎖
lihuoming得到鎖
lihuoming釋放鎖
...

注意:輸出的字符串順序不定,個數也不定。

例子3:lockInterruptibly()的使用
執行lockInterruptibly()方法的方法中,需要將異常InterruptedException拋出,在等待鎖的線程可調用interrupt()方法中斷,即可觸發異常InterruptedException,然後可以在catch中執行相應的操作。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {

    public static void main(String[] args) {
        new LockTest().init();
    }

    private void init(){
        final Outputer outputer = new Outputer();
        Thread thread1 = new Thread(new Runnable(){
            public void run() {
                String name = "zhangxiaoxiang";
                try {
                    Thread.sleep(10);
                    outputer.output(name);
                } catch (InterruptedException e) {
                    System.out.println(name + "被中斷");
                }


            }
        });
        thread1.start();

        Thread thread2 = new Thread(new Runnable(){
            public void run() {
                String name = "lihuoming";
                try {
                    Thread.sleep(10);
                    outputer.output(name);
                } catch (InterruptedException e) {
                    System.out.println(name + "被中斷");
                }

            }
        });
        thread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.interrupt();

    }

    static class Outputer{
        Lock lock = new ReentrantLock();
        //將InterruptedException拋出
        public void output(String name) throws InterruptedException {
            System.out.println(name + "試圖執行output方法");
            lock.lockInterruptibly();
            try{
                System.out.println(name + "得到鎖");
                long startTime = System.currentTimeMillis();
                for(    ;     ;) {
                    if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
                        break;
                }
            }finally{
                System.out.println(name + "執行了finally");
                lock.unlock();
                System.out.println(name + "釋放鎖");

            }
        }
    }
}

輸出:

zhangxiaoxiang試圖執行output方法
zhangxiaoxiang得到鎖
lihuoming試圖執行output方法
lihuoming被中斷

運行之後,發現thread2能夠被正確中斷

在jdk源碼中的一個運用就是類ArrayBlockingQueue的方法。該方法中有以下幾點注意:
1、使用lock.lockInterruptibly()需拋出異常InterruptedException
2、使用了Condition
3、在finally中關閉鎖

 /**
  * Inserts the specified element at the tail of this queue, waiting
  * up to the specified wait time for space to become available if
  * the queue is full.
  *
  * @throws InterruptedException {@inheritDoc}
  * @throws NullPointerException {@inheritDoc}
  */
 public boolean offer(E e, long timeout, TimeUnit unit)
     throws InterruptedException {
     checkNotNull(e);
     long nanos = unit.toNanos(timeout);
     final ReentrantLock lock = this.lock;
     lock.lockInterruptibly();
     try {
         while (count == items.length) {
             if (nanos <= 0)
                 return false;
              /** notFull是一個Condition對象,
              ** Condition for waiting puts 
              ** private final Condition notFull;
              */
             nanos = notFull.awaitNanos(nanos);
         }
         insert(e);
         return true;
     } finally {
         lock.unlock();
     }
 }

讀寫鎖ReadWriteLock

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();
}

一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進行讀操作。ReentrantReadWriteLock實現了ReadWriteLock接口。

ReentrantReadWriteLock裏面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。

讀寫鎖,分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥,由JVM控制。

注意:此鎖最多支持 65535 個遞歸寫入鎖和 65535 個讀取鎖。試圖超出這些限制將導致鎖方法拋出 Error。

下面給出構造函數和常用方法的簡要說明:

類ReentrantReadWriteLock

  • ReentrantReadWriteLock(boolean fair): 使用給定的公平策略創建一個新的 ReentrantReadWriteLock。
  • ReentrantReadWriteLock():使用默認(非公平)的排序屬性創建一個新的 ReentrantReadWriteLock

類ReentrantReadWriteLock的方法

返回類型 方法
ReentrantReadWriteLock.ReadLock readLock() 返回用於讀取操作的鎖
ReentrantReadWriteLock.WriteLock writeLock() 返回用於寫入操作的鎖

下面給出示例代碼:

import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {
    public static void main(String[] args) {
        final Queue3 q3 = new Queue3();
        for(int i=0;i<3;i++)
        {
            final Thread readThread = new Thread() {
                public void run() {
                    while (true) {
                        q3.get();
                    }
                }
            };
            readThread.setName("read-"+i);
            readThread.start();


            final Thread writeThread = new Thread(){
                public void run(){
                    while(true){
                        q3.put(new Random().nextInt(10000));
                    }
                }           

            };
            writeThread.setName("write-"+i);
            writeThread.start();
        }

    }
}

class Queue3{
    private Object data = null;
    ReadWriteLock rwl = new ReentrantReadWriteLock();
    public void get(){
        rwl.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to read data!");
            Thread.sleep((long)(Math.random()*1000));
            System.out.println(Thread.currentThread().getName() + " have read data :" + data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            rwl.readLock().unlock();
        }
    }

    public void put(Object data){

        rwl.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");                  
            Thread.sleep((long)(Math.random()*1000));
            this.data = data;       
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);                 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            rwl.writeLock().unlock();
        }
    }
}

輸出:

read-0 be ready to read data!
read-1 be ready to read data!
read-0 have read data :null
read-1 have read data :null
write-1 be ready to write data!
write-1 have write data: 3713
write-1 be ready to write data!
write-1 have write data: 3420
...

對輸出結果進行分析:be ready to read data!have read data並不是先後出現的,中間可以夾着be ready to read data!說明讀鎖之間不互斥。

面試題:
緩存系統:取數據,需調用public Object getData(String key)方法,先檢查緩存有沒有這個數據,如果有就直接返回,如果沒有,就從數據庫中查找這個數,然後寫入緩存。
如果使用synchronized對getData加鎖,那麼getData方法只能被一個讀線程執行,其他讀操作就得等待,這裏可以使用一個讀寫鎖,只有在寫的時候才需要互斥


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheDemo {

    private Map<String, Object> cache = new HashMap<String, Object>();
    public static void main(String[] args) {
        // TODO Auto-generated method stub

    }

    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    public  Object getData(String key){
        rwl.readLock().lock();
        Object value = null;
        try{
            value = cache.get(key);
            if(value == null){
                rwl.readLock().unlock();
                rwl.writeLock().lock();
                try{
                    //再次進行判斷,防止多個寫線程堵在這個地方重複寫
                    if(value==null){
                        value = "aaaa"; //設置新值
                    }
                }finally{
                    rwl.writeLock().unlock();
                }
                //設置完成 釋放寫鎖,恢復讀寫狀態
                rwl.readLock().lock();
            }
        }finally{
            rwl.readLock().unlock();
        }
        return value;
    }
}

其他更多有關ReentrantReadWriteLock後面補充。

Condition

Condition是在java 1.5中才出現的,它用來替代傳統的Object的wait()、notify()實現線程間的協作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()這種方式實現線程間協作更加安全和高效。因此通常來說比較推薦使用Condition,,阻塞隊列實際上是使用了Condition來模擬線程間協作。

synchronized常與wait、notify等方法使用,Condition常與await、signal等方法使用。

  • Condition是個接口,基本的方法就是await()和signal()方法;
  • Condition依賴於Lock接口,生成一個Condition的基本代碼是lock.newCondition()
  • 調用Condition的await()和signal()方法,都必須在lock保護之內,就是說必須在lock.lock()和lock.unlock之間纔可以使用

Conditon中的await()對應Object的wait();
Condition中的signal()對應Object的notify();
Condition中的signalAll()對應Object的notifyAll()。

Condition中的long awaitNanos(long nanosTimeout) throws InterruptedException方法傳入一個等待的微秒時間,該方法返回了所剩毫微秒數的一個估計值,以等待所提供的 nanosTimeout 值的時間,如果超時,則返回一個小於等於 0 的值。可以用此值來確定在等待返回但某一等待條件仍不具備的情況下,是否要再次等待,以及再次等待的時間。此方法的典型用法採用以下形式(上面講ArrayBlockingQueue的public E poll(long timeout, TimeUnit unit)方法中就用到這個方法):

 synchronized boolean aMethod(long timeout, TimeUnit unit) {
   long nanosTimeout = unit.toNanos(timeout);
   while (!conditionBeingWaitedFor) {
     if (nanosTimeout > 0)
         nanosTimeout = theCondition.awaitNanos(nanosTimeout);
      else
        return false;
   }
   // ... 
 }

代碼示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionCommunication {

    public static void main(String[] args) {
        final Business business = new Business();
        new Thread(
                new Runnable() {
                    public void run() {
                        for (int i = 1; i <= 50; i++) {
                            business.sub(i);
                        }
                    }
                }
        ).start();

        for (int i = 1; i <= 50; i++) {
            business.main(i);
        }
    }

    static class Business {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        private boolean bShouldSub = true;

        public void sub(int i) {
            lock.lock();
            try {
                while (!bShouldSub) {
                    try {
                        condition.await();
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                for (int j = 1; j <= 10; j++) {
                    System.out.println("sub thread sequence of " + j + ",loop of " + i);
                }
                bShouldSub = false;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }

        public void main(int i) {
            lock.lock();
            try {
                while (bShouldSub) {
                    try {
                        condition.await();
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                for (int j = 1; j <= 100; j++) {
                    System.out.println("main thread sequence of " + j + ",loop of " + i);
                }
                bShouldSub = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }

    }
}

同樣:await()方法需要放在while循環中。
更多參考:
對比synchronized+notify的使用可以參考:04_張孝祥Java多線程傳統線程同步通信技術
與傳統的同步對比可參考:線程間協作的兩種方式:wait、notify、notifyAll和Condition

參考

詳解synchronized與Lock的區別與使用
張孝祥_Java多線程與併發庫高級應用04
jdk api
線程間協作的兩種方式:wait、notify、notifyAll和Condition

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章