java 併發多線程顯式鎖概念簡介 什麼是顯式鎖 多線程下篇(一)

目前對於同步,僅僅介紹了一個關鍵字synchronized,能夠用於保證線程同步的原子性、可見性、有序性
對於synchronized關鍵字,對於靜態方法默認是以該類的class對象做爲鎖,對於實例方法默認是當前對象this,對於同步代碼塊,須要指定鎖對象
對於整個同步方法或者代碼塊,再也不須要顯式的進行加鎖,默認這一整個範圍都是在鎖範圍內
能夠理解爲,隱含的在代碼開始和結尾處,進行了隱式的加鎖和解鎖
因此synchronized又被稱爲隱式鎖
對於synchronized關鍵字的隱式鎖,不須要顯式的加鎖和釋放,即便出現了問題,仍舊可以對鎖進行釋放
synchronized是一種阻塞式的,在前面也提到過,對於synchronized修飾的同步,若是沒法進入監視器則是BLOCKED狀態,無疑,性能方面可想而知
並且,這種隱式鎖,在同一個代碼片斷內只有一個監視器,靈活性不夠
 
爲了優化synchronized的一些不便,Java又提出來了顯式鎖的概念Lock
顧名思義,顯式,是相對隱式來講的,也就是對於加鎖和解鎖,須要明確的給出,而不會自動的進行處理

示例回顧

回憶下是以前《多線程協做wait、notify、notifyAll方法簡介理解使用 》一文中使用的例子
ps:下面的例子是優化過的,其中if判斷換成了while 循環檢測,notify換成了notifyAll
package test1;
import java.util.LinkedList;
/**
* 消息隊列MessageQueue 測試
*/
public class T14 {
public static void main(String[] args) {
final RefactorMessageQueue mq = new RefactorMessageQueue(5);
System.out.println("***************task begin***************");
//建立生產者線程並啓動
for (int i = 0; i < 20; i++) {
new Thread(() -> {
while (true) {
mq.set(new Message());
}
}, "producer"+i).start();
}
//建立消費者線程並啓動
new Thread(() -> {
while (true) {
mq.get();
}
}, "consumer").start();
}
}
/**
* 消息隊列
*/
class RefactorMessageQueue {
/**
* 隊列最大值
*/
private final int max;
/*
* 鎖
* */
private final byte[] lock = new byte[1];
/**
* final確保發佈安全
*/
final LinkedList<Message> messageQueue = new LinkedList<>();
/**
* 構造函數默認隊列大小爲10
*/
public RefactorMessageQueue() {
max = 10;
}
/**
* 構造函數設置隊列大小
*/
public RefactorMessageQueue(int x) {
max = x;
}
public void set(Message message) {
synchronized (lock) {
//若是已經大於隊列個數,隊列滿,進入等待
while (messageQueue.size() > max) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若是隊列未滿,生產消息,隨後通知lock上的等待線程
//每一次的消息生產,都會通知消費者
System.out.println(Thread.currentThread().getName() + " : add a message");
messageQueue.addLast(message);
lock.notifyAll();
}
}
public void get() {
synchronized (lock) {
//若是隊列爲空,進入等待,沒法獲取消息
while (messageQueue.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//隊列非空時,讀取消息,隨後通知lock上的等待線程
//每一次的消息讀取,都會通知生產者
System.out.println(Thread.currentThread().getName() + " : get a message");
messageQueue.removeFirst();
lock.notifyAll();
}
}
}
分析下這個示例中的一些概念
使用了synchronized用做同步,鎖對象爲  private final byte[] lock = new byte[1];
有多個生產者和一個消費者,爲了進行通訊使用了監視器(也就是鎖對象)的wait和notifyAll方法進行通訊
ps:前文也說過爲什麼要用notifyAll而不是notify
簡單說兩個點:
  • synchronized關鍵字
  • 監視器方法
藉助於這兩個點,能夠完成多線程之間的協做與通訊(多個生產者一個消費者)
監視器方法的調用須要在監視器內,也就是同步方法內
並且上面的例子中的監視器都是同一個就是鎖對象,wait是當前線程在監視器上wait,notifyAll方法則是喚醒全部在此監視器上等待的線程
很顯然,其實生產者應該喚醒生產者,消費者應該喚醒消費者
但是,多線程協做使用的是同一個隊列,因此須要使用同一把鎖
又由於監視器方法必須在同步方法內並且也必須是持有監視器才能調用相應的監視器方法,因此只能使用同一個監視器了
也就是隻能將這些線程組織在同一個監視器中,就很差作到「其實生產者應該喚醒生產者,消費者應該喚醒消費者」

顯式鎖邏輯

再回過頭看顯式鎖,他是如何作到各方面靈活的呢?
從上面的分析來看主要就是由於隱式鎖與監視器之間的比較強的關聯關係
synchronized修飾的代碼片斷使用的是同一把鎖,同步方法內的監視器方法也只能調用這個鎖的,也就是說在使用上來看,用什麼鎖,就要用這個鎖的監視器,強關聯
問題的一種解題思路就是解耦,顯式鎖就是這種思路 
Lock就比如是synchronized關鍵字,只不過你須要顯式的進行加鎖和解鎖
慣用套路以下
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
原本使用synchronized隱式的加鎖和解鎖,換成了Lock的lock和unlock方法調用
那麼監視器呢?
與鎖關聯的監視器又是什麼,又如何調用監視器的方法呢?
Lock提供了Condition newCondition();方法
返回類型爲Condition,被稱之爲條件變量,能夠認爲是鎖關聯的監視器
藉助於Condition,就能夠達到原來監視器方法調用的效果,Condition方法列表以下,看得出來,是否是很像wait和notify、notifyAll?目標是一致的
image_5c7dcac8_2722
因此能夠說,顯式鎖的邏輯就是藉助於Lock接口以及Condition接口,實現了對synchronized關鍵字以及鎖對應的監視器的另外的一種實現
從而提供了更大的靈活性
仍是以前的示例,嘗試試用一下顯式鎖
package test2;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T26 {
public static void main(String[] args) {
final RefactorMessageQueue mq = new RefactorMessageQueue(5);
System.out.println("***************task begin***************");
//建立生產者線程並啓動
for (int i = 0; i < 20; i++) {
new Thread(() -> {
while (true) {
mq.set(new Message());
}
}, "producer" + i).start();
}
//建立消費者線程並啓動
new Thread(() -> {
while (true) {
mq.get();
}
}, "consumer").start();
}
/**
* 消息隊列中存儲的消息
*/
static class Message {
}
/**
* 消息隊列
*/
static class RefactorMessageQueue {
/**
* 隊列最大值
*/
private final int max;
/*
* 鎖
* */
private final Lock lock = new ReentrantLock();
/**
* 條件變量
*/
private final Condition condition = lock.newCondition();
/**
* final確保發佈安全
*/
final LinkedList<Message> messageQueue = new LinkedList<>();
/**
* 構造函數默認隊列大小爲10
*/
public RefactorMessageQueue() {
max = 10;
}
/**
* 構造函數設置隊列大小
*/
public RefactorMessageQueue(int x) {
max = x;
}
public void set(Message message) {
lock.lock();
try {
//若是已經大於隊列個數,隊列滿,進入等待
while (messageQueue.size() > max) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若是隊列未滿,生產消息,隨後通知lock上的等待線程
//每一次的消息生產,都會通知消費者
System.out.println(Thread.currentThread().getName() + " : add a message");
messageQueue.addLast(message);
condition.signalAll();
} finally {
}
lock.unlock();
}
public void get() {
lock.lock();
try {
//若是隊列爲空,進入等待,沒法獲取消息
while (messageQueue.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//隊列非空時,讀取消息,隨後通知lock上的等待線程
//每一次的消息讀取,都會通知生產者
System.out.println(Thread.currentThread().getName() + " : get a message");
messageQueue.removeFirst();
condition.signalAll();
} finally {
lock.unlock();
}
}
}
}
改變的核心邏輯就是鎖和條件變量
/*
* 鎖
* */
private final Lock lock = new ReentrantLock();
/**
* 條件變量
*/
private final Condition condition = lock.newCondition();
  • 使用lock.lock();以及lock.unlock(); 替代了synchronized(lock)
  • 使用condition的await和signalAll方法替代了lock.wait()和   lock.notifyAll
看起來與使用synchronized關鍵字好像差很少,這沒什麼毛病
顯式鎖的設計原本就是爲了彌補隱式鎖的,雖然說不是說做爲一種替代品,可是功能邏輯的類似性是必然的
注意到,使用條件變量,與隱式鎖中都是隻有一個監視器,全部的線程仍舊都是被喚醒
前面提到過,其實生產者應該喚醒消費者,消費者才應該喚醒生產者
是否是能夠兩個變量?
對於生產者來講,只要非滿便可,若是滿了等待,非滿生產而後喚醒消費者
對於消費者來講,只要非空便可,若是空了等待,非空消費而後喚醒生產者
 
能夠定義兩個條件變量,以下所示完整代碼
其實只是定義了兩個監視器
package test2;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class T27 {
  public static void main(String[] args) {
    final RefactorMessageQueue mq = new RefactorMessageQueue(5);
    System.out.println("***************task begin***************");
    //建立生產者線程並啓動
    for (int i = 0; i < 20; i++) {
      new Thread(() -> {
        while (true) {
          mq.set(new Message());
        }
      }, "producer" + i).start();
    }
    //建立消費者線程並啓動
    new Thread(() -> {
      while (true) {
        mq.get();
      }
    }, "consumer").start();
  }
  /**
  * 消息隊列中存儲的消息
  */
  static class Message {
  }
  /**
  * 消息隊列
  */
  static class RefactorMessageQueue {
    /**
    * 隊列最大值
    */
    private final int max;
    /*
    * 鎖
    * */
    private final Lock lock = new ReentrantLock();
    /**
    * 條件變量,用於消費者,非空便可消費
    */
    private final Condition notEmptyCondition = lock.newCondition();
    /**
    * 條件變量,用於生產者,非滿便可生產
    */
    private final Condition notFullCondition = lock.newCondition();
    /**
    * final確保發佈安全
    */
    final LinkedList<Message> messageQueue = new LinkedList<>();
    /**
    * 構造函數默認隊列大小爲10
    */
    public RefactorMessageQueue() {
      max = 10;
    }
    /**
    * 構造函數設置隊列大小
    */
    public RefactorMessageQueue(int x) {
      max = x;
    }
    public void set(Message message) {
      lock.lock();
      try {
        //若是已經大於隊列個數,隊列滿,進入等待
        while (messageQueue.size() > max) {
          try {
            System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
            //若是滿了,生產者在「非滿」這個條件上等待
            notFullCondition.await();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        //若是隊列未滿,生產消息,隨後通知lock上的等待線程
        //每一次的消息生產,都會通知消費者
        System.out.println(Thread.currentThread().getName() + " : add a message");
        messageQueue.addLast(message);
        //生產後,增長了消息,非空條件知足,須要喚醒消費者
        notEmptyCondition.signalAll();
      } finally {
      }
      lock.unlock();
    }
    public void get() {
      lock.lock();
      try {
        //若是隊列爲空,進入等待,沒法獲取消息
        while (messageQueue.isEmpty()) {
          try {
            System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
            //若是空了,消費者須要在「非空」條件上等待
            notEmptyCondition.await();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        //隊列非空時,讀取消息,隨後通知lock上的等待線程
        //每一次的消息讀取,都會通知生產者
        System.out.println(Thread.currentThread().getName() + " : get a message");
        messageQueue.removeFirst();
        //消費後,減小了消息,因此非滿條件知足,須要喚醒生產者
        notFullCondition.signalAll();
      } finally {
        lock.unlock();
      }
    }
  }
}

總結

經過上面的示例,應該能夠理解顯式鎖的思路
他與隱式鎖並無像名稱上看起來這麼對立(一個隱 一個顯),他們的核心仍舊是爲了解決線程的同步與線程間的通訊協做
線程同步與通訊的在Java中的底層核心概念爲鎖和監視器
不論是synchronized仍是Lock,不論是Object提供的通訊方法仍是Condition中的方法,都仍是圍繞着鎖和監視器的概念展開的
如同平時寫代碼,一樣的功能,可能會有多種實現方式,顯式鎖和隱式鎖也是相似的,他們的實現有着不少的不一樣,也都有各類利弊
因此纔會有隱式鎖和顯式鎖,在程序中很難找到「放之四海而皆準」的實現代碼,因此纔會有各類各樣的解決方案
儘管早期synchronized關鍵字性能比較低,可是隨着版本的升級,性能也有了很大的改善
因此官方也是建議若是場景知足,仍是儘量使用synchronized關鍵字而不是顯式鎖
顯式鎖是爲了解決隱式鎖而很差解決的一些場景而存在的,儘管本文並無體現出來他們之間的差別(本文偏偏相反,對相同點進行了介紹)
可是顯式鎖有不少隱式鎖不存在的優勢,後續慢慢介紹,經過本文但願理解,顯式鎖也只是線程同步與協做通訊的一種實現途徑而已
相關文章
相關標籤/搜索