教學筆記:多線程之鎖的區分與應用

併發編程中最常出現的情形就是多個線程共享一個資源,這些共享的資源很可能導致錯誤或者數據不一致的情形,需要想辦法來解決這種問題。

教學筆記:多線程之鎖的區分與應用

臨界區(critical section):最多隻能有一個線程執行的代碼塊來訪問某些共享資源。

一般鎖能夠防止多個線程同時訪問某些資源,但是有些鎖可以允許多個線程併發的讀取共享資源,比如讀寫鎖。

兩個基本的鎖機制:

  • synchronized關鍵字,之前也提到過
  • Lock接口

鎖的注意事項

鎖是最常用的同步方法之一。但是在高併發情況下鎖的競爭會導致程序的性能下降。為了降低這種副作用,這裡有一些使用鎖的建議。

  • 減少鎖的持有時間。
  • 減小鎖的粒度,即縮小鎖作用的對象範圍。
  • 鎖分離,如讀多寫少的場合,可以使用讀寫鎖。其餘需要使用獨佔鎖的時候,嘗試根據功能,分離鎖。
  • 鎖粗化:這個和減少鎖的持有時間相反,根據具體場景來衡量,假如某個線程不斷的請求,同步和釋放鎖,也會浪費性能,根據實際情況進行權衡。

在學習或者使用Java的過程中進程會遇到各種各樣的鎖的概念:公平鎖、非公平鎖、自旋鎖、可重入鎖、偏向鎖、輕量級鎖、重量級鎖、讀寫鎖、互斥鎖等等。

這篇文章整理了各種常見的Java鎖:http://www.importnew.com/19472.html

Lock與synchronized的區別

在Lock接口出現前,Java都是依靠synchronized關鍵字的,在JavaSE5之後,新增了Lock接口以及相關類來實現類似功能。

  • Lock接口在使用時需要顯示的獲取和釋放鎖,synchronized關鍵字是隱式獲取和釋放鎖
  • Lock接口更加靈活,在獲取鎖的時候可以指定更多的操作,可中斷鎖,超時獲取鎖,指定時間釋放鎖,非阻塞的獲取鎖。
  • Lock接口可以允許讀寫分離,多個讀,但是隻有一個寫

Lock可以說是synchronzed的增強版。

  • 超時獲取鎖:在指定時間未獲取到鎖,則返回
  • 非阻塞獲取鎖:當前線程嘗試獲取鎖,如果未獲取到,立刻返回,如果成功則獲取到鎖。
  • 能被中斷的獲取鎖:獲取到鎖的線程可以響應中斷。
教學筆記:多線程之鎖的區分與應用

重入鎖-ReenterantLock

可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數獲得鎖之後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。

Java中重入鎖使用java.util.concurrent.locks.ReentrantLock實現。syncrhonzed也是可重入鎖。

// 一個簡單的案例public class ReentrantLockTest implements Runnable{ private ReentrantLock lock = new ReentrantLock(); public void get() { // get兩次加鎖,set又加鎖。 lock.lock(); lock.lock(); System.out.println("線程當前ID:" + Thread.currentThread().getId()); set(); lock.unlock(); lock.unlock(); } public void set() { lock.lock(); System.out.println("線程當前ID:" + Thread.currentThread().getId()); lock.unlock(); } @Override public void run() { get(); } public static void main(String[] args) { ReentrantLockTest test = new ReentrantLockTest(); new Thread(test,"A").start(); new Thread(test,"B").start(); new Thread(test,"C").start(); }}

ReentrantLock有幾個重要方法:

  • lock() : 獲得鎖,如果鎖已經被佔用,則等待
  • lockInterruptibly() : 獲得鎖,但優先響應中斷
  • tryLock() : 無阻塞鎖,如果成功返回true,失敗返回false,該方法不等的,立刻返回
  • tryLock(long time,TimeUnit unit) : 在給定時間內嘗試獲得鎖
  • unlock(): 釋放鎖

這幾個方法都比較簡單,可以自行嘗試。

讀寫鎖-ReadWriteLock

讀寫分離鎖可以有效的減少鎖競爭,以提升系統性能。

  • 讀 寫 讀 非阻塞 阻塞 寫 阻塞 阻塞 讀讀不互斥
  • 讀寫互斥
  • 寫寫互斥

如果在系統中,讀操作的次數遠遠大於寫操作,則讀寫鎖就可以發揮最大的功效。JDK併發包中提供讀寫鎖的實現是ReentrantReadWriteLock。

// 這個demo,用來驗證讀寫鎖的性能比一般的鎖要好。// 直接運行此demo,程序幾秒鐘就可以運行完畢// 如果註釋掉讀寫所,讓sreadLock = sLock, sWriteLock = sLock。那麼程序要運行20多秒才結束public class ReadWriteLockDemo { private static Lock sLock = new ReentrantLock(); private static ReadWriteLock sReadWriteLock = new ReentrantReadWriteLock(); // 分別獲取讀寫鎖 private static Lock sReadLock = sReadWriteLock.readLock();// private static Lock sReadLock = sLock; private static Lock sWriteLock = sReadWriteLock.writeLock();// private static Lock sWriteLock = sLock; private int value; // 模擬讀操作 public int read() throws InterruptedException { try { sReadLock.lock(); Thread.sleep(1000); return value; }finally { sReadLock.unlock(); } } // 模擬寫操作 public void write(int index) throws InterruptedException { try { sWriteLock.lock(); Thread.sleep(1000); value = index; }finally { sWriteLock.unlock(); } } static class ReadRunnable implements Runnable{ private ReadWriteLockDemo mDemo; public ReadRunnable(ReadWriteLockDemo demo) { mDemo = demo; } @Override public void run() { try { mDemo.read(); } catch (InterruptedException e) { e.printStackTrace(); } } } static class WriteRunnable implements Runnable{ private ReadWriteLockDemo mDemo; public WriteRunnable(ReadWriteLockDemo demo) { mDemo = demo; } @Override public void run() { try { mDemo.write(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { ReadWriteLockDemo demo = new ReadWriteLockDemo(); ReadRunnable readRunnable = new ReadRunnable(demo); WriteRunnable writeRunnable = new WriteRunnable(demo); for (int i = 0; i < 20; i++) { if (i < 18){ new Thread(readRunnable).start(); }else { new Thread(writeRunnable).start(); } } }}

鎖的公平性

重入鎖和讀寫鎖,構造器都提供了一個參數fair,允許你控制鎖的公平性,默認情況下是不公平的。

非公平鎖:在多個線程搶佔鎖的時候,系統隨機挑選一個線程持有鎖。

公平鎖:多個線程搶佔鎖的時候,系統挑選等待時間最長的線程持有鎖。

我們只需要在構造鎖對象的時候,傳入true參數即可獲得公平鎖對象。

new ReentrantLock(true);new ReentrantReadWriteLock(true);

Condition

在JDK內部,重入鎖和Condition對象經常一起用到。之前我們在併發基礎中提到過wait與notify要與synchronized關鍵字配合使用。Condition與重入鎖一起使用,功能與wait和notify類似。

Lock接口的newCondition()方法可以生成一個與當前重入鎖綁定的Condition實例,利用Condition我們可以讓線程在特定的時間等待,在特定的時刻受到通知。

以ArrayBlockingQueue為例:

public class ArrayBlockingQueue extends AbstractQueue implements BlockingQueue, java.io.Serializable { //存儲數據元素 final Object[] items; /** 主要的鎖 */ final ReentrantLock lock; /** 等待taoke的條件 */ private final Condition notEmpty; /** 等待put的條件 */ private final Condition notFull;  public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; //構造鎖與condition lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); }     public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) //如果隊列為空,等待隊列非空的信號 notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }  // 如果入隊成功,發出不空的信號 private void enqueue(E x) { // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = x; if (++putIndex == items.length) putIndex = 0; count++;  //發信號 notEmpty.signal(); }   public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == items.length) //如果隊列已滿,等待隊列有足夠的空間 notFull.await(); enqueue(e); } finally { lock.unlock(); } }   //如果出隊成功,發出 不滿 的信號 private E dequeue() { // assert lock.getHoldCount() == 1; // assert items[takeIndex] != null; final Object[] items = this.items; @SuppressWarnings("unchecked") E x = (E) items[takeIndex]; items[takeIndex] = null; if (++takeIndex == items.length) takeIndex = 0; count--; if (itrs != null) itrs.elementDequeued(); //發信號  notFull.signal(); return x; } }

最後

這篇文章主要說了下使用鎖的時候需要注意的幾點,然後提了重入鎖與讀寫鎖。公平鎖,最後說了重入鎖的搭檔Condition。

篇幅有限,沒辦法面面俱到,感興趣的還望自己再摸索。希望能幫助大家。

參考

  • Java中synchronized的實現原理與應用
  • Java鎖的種類以及辨析(四):可重入鎖
  • 《Java高併發程序設計》
  • 《併發編程的藝術》


分享到:


相關文章: