在上一篇文章中咱們講到了如何使用關鍵字synchronized來實現同步訪問。本文咱們繼續來探討這個問題,從Java 5以後,在java.util.concurrent.locks包下提供了另一種方式來實現同步訪問,那就是Lock。html
也許有朋友會問,既然均可以經過synchronized來實現同步訪問了,那麼爲何還須要提供Lock?這個問題將在下面進行闡述。本文先從synchronized的缺陷講起,而後再講述java.util.concurrent.locks包下經常使用的有哪些類和接口,最後討論如下一些關於鎖的概念方面的東西java
前面咱們說過synchronized的線程釋放鎖的狀況有兩種:安全
- 代碼塊或者同步方法執行完畢
- 代碼塊或者同步方法出現異常有jvm自動釋放鎖
從上面的synchronized釋放鎖能夠看出,只有synchronized代碼塊執行完畢或者異常纔會釋放,若是代碼塊中的程序由於IO緣由阻塞了,那麼線程將永遠不會釋放鎖,可是此時另外的線程還要執行其餘的程序,極大的影響了程序的執行效率,如今咱們須要一種機制可以讓線程不會一直無限的等待下去,可以響應中斷,這個經過lock就能夠辦到dom
另外若是有一個程序,包含多個讀線程和一個寫線程,咱們能夠知道synchronized只能一個一個線程的執行,可是咱們須要多個讀線程同時進行讀,那麼使用synchronized確定是不行的,可是咱們使用lock一樣能夠辦到jvm
查看API可知,Lock是一個接口,所以是不能夠直接建立對象的,可是咱們能夠利用其實現的類來建立對象,這個先不着急,咱們先看看Lock類到底實現了什麼方法,具體的實現咱們將會在介紹其實現的類的時候再詳細的講解ide
lock()
獲取鎖,若是沒有得到就會一直等待unlock()
釋放鎖tryLock()
嘗試得到鎖,若是成功得到鎖就執行,若是沒有成功得到鎖,那麼就不會等待了lockInterruptibly()
若是當前線程未被中斷,則獲取鎖。
ReentrantLock是可重入鎖,是實現Lock接口的一個類,可重入是一種線程的分配機制,可重入的意思就是老是分配給最近得到鎖的線程,這是一種不公平的分配機制,將會出現飢餓現象,固然爲了解決這種現象,ReentrantLock的構造方法還提供了一個fair參數,若是fair爲true表示使用公平分配機制,將會有等待時間最長的線程得到鎖測試
ReentrantLock()
建立一個對象,默認使用的時可重入的機制ReentrantLock(boolean fair)
若是fair爲true那麼使用的是公平分配機制
lock()
獲取鎖,若是沒有獲取到將會一直阻塞
下面使用一段程序演示如下lock方法的使用,代碼以下:this
//實現接口的線程類 public class MyThread implements Runnable { public ReentrantLock rLock = null; //注意這裏的鎖必定要是全局變量,不然每個線程都建立一把鎖,那麼將會毫無心義 public MyThread() { this.rLock = new ReentrantLock(); // 建立默認的可重入鎖 } // 將unlock方法放在finally中確保執行中代碼出現異常仍然可以釋放鎖,不然將會形成其它的線程阻塞 public void display() { this.rLock.lock(); // 獲取鎖 try { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "正在輸出" + i); } } finally { this.rLock.unlock(); // 釋放鎖,注意這步是必定須要的 } } @Override public void run() { this.display(); // 調用display方法 } } //線程的測試類,主要是建立對象啓動線程 public class Test { public static void main(String[] args) { final MyThread thread = new MyThread(); // 建立對象 // 下面建立兩個線程,而且直接啓動, new Thread(thread).start(); new Thread(thread).start(); } }
執行上面的代碼獲得下圖的結果:線程
從上面的結果看出,線程是一個一個輸出的,而且只有等待一個線程輸出完畢才能執行下一個線程,這裏的僅僅是針對lock和unlock之間的代碼,以外的代碼並非受到控制code
注意: 這裏的建立的可重入鎖的對象必須對於每個線程來講是全局的變量,是能夠共享的一個對象,若是你在display方法中建立這個對象,那麼是毫無心義的,由於每個線程用的根本不是同一把鎖
boolean tryLock()
首先嚐試獲取鎖,若是獲取鎖了就執行,不然就不會一直等待
下面使用一段代碼嘗試如下這個方法,代碼以下:
//實現接口的線程類 public class MyThread implements Runnable { public ReentrantLock rLock = null; // 注意這裏的鎖必定要是全局變量,不然每個線程都建立一把鎖,那麼將會毫無心義 public MyThread() { this.rLock = new ReentrantLock(); // 建立默認的可重入鎖 } // 將unlock方法放在finally中確保執行中代碼出現異常仍然可以釋放鎖,不然將會形成其它的線程阻塞 public void display() { if (this.rLock.tryLock()) // 若是獲取了鎖 { try { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "正在輸出" + i); } } finally { this.rLock.unlock(); // 釋放鎖,注意這步是必定須要的 } } else { System.out.println(Thread.currentThread().getName() + "獲取鎖失敗,我將不會一直等待........"); } } @Override public void run() { this.display(); // 調用display方法 } } //線程的測試類,主要是建立對象啓動線程 public class Test { public static void main(String[] args) { final MyThread thread = new MyThread(); // 建立對象 // 下面建立兩個線程,而且直接啓動, new Thread(thread).start(); new Thread(thread).start(); } }
執行後的結果以下圖:
從上面的結果咱們知道線程0獲取了鎖開始執行,可是線程1並無獲取鎖,可是使用的是tryLock並非lock,所以不會一直等待下去,因此直接程序向下運行,直接跳過上鎖的代碼段,所以就輸出了上面的那句話後直接結
從API中能夠知道,這個也是一個接口,用於實現讀寫線程,他有兩個方法:Lock readLock(),Lock writeLock() 分別用於得到讀鎖和寫鎖,指定特定的鎖能夠實現特定的功能,好比讀鎖能夠在寫線程在執行的狀況下能夠實現多個讀線程進行操做,下面咱們來介紹它的具體的實現的類ReentrantReadWriteLock
這個類也是一個可重入分配的類,固然前面已經說過了什麼是可重入,如今咱們來講說說這個類的詳細的用法
ReentrantReadWriteLock()
使用默認(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock。ReentrantReadWriteLock(boolean fair)
使用給定的公平策略建立一個新的ReentrantReadWriteLock。
ReentrantReadWriteLock.ReadLock readLock()
用於返回讀取操做的鎖
前面已經說過讀取操做的鎖是用來實現多個線程共同執行的,代碼以下:
//實現接口的線程類 public class MyThread implements Runnable { public ReentrantReadWriteLock rwlock = null; public Lock rLock = null; public MyThread() { this.rwlock = new ReentrantReadWriteLock(); // 建立對象,使用的是非公平的 this.rLock = this.rwlock.readLock(); // 獲取讀取鎖對象 } // 將unlock方法放在finally中確保執行中代碼出現異常仍然可以釋放鎖,不然將會形成其它的線程阻塞 public void display() { this.rLock.lock(); // 獲取讀取鎖 try { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "正在輸出" + i); } } finally { this.rLock.unlock(); // 釋放鎖,注意這步是必定須要的 } } @Override public void run() { this.display(); // 調用display方法 } } //線程的測試類,主要是建立對象啓動線程 public class Test { public static void main(String[] args) { final MyThread thread = new MyThread(); // 建立對象 // 下面建立兩個線程,而且直接啓動, for(int i=0;i<5;i++) { new Thread(thread).start(); } } }
執行上面的程序結果以下:
從上面的結果能夠知道,其實使用讀取操做是多個線程同時進行讀取的操做,所以必定要當心謹慎的使用,根據本身的需求,通常不能在裏面進行修改了,由於出現結果不許確的結果,這個就很少說了,相信你們都明白,總之要當心使用
ReentrantReadWriteLock.WriteLock writeLock()
返回用於寫入操做的鎖
寫入操做的鎖和讀取操做的鎖不同了,由於一次只能容許一個線程執行寫入操做。
而且若是一個線程已經佔用了讀鎖,另一個線程申請寫鎖將會一直等待線程釋放讀鎖。
若是一個線程已經佔用了寫鎖,另一個線程申請讀鎖,那麼這個線程將會一直等待線程釋放寫鎖才能執行。
總之意思就是寫線程和讀線程不能同時執行,可是多個讀線程能夠同時執行
下面將使用一個程序詳細的體會如下讀寫鎖的綜合使用,代碼以下:
//實現接口的線程類 public class MyThread { public ReentrantReadWriteLock rwlock = null; public Lock rLock = null; public Lock wLock = null; public ArrayList<Integer> arrayList = null; public MyThread() { this.rwlock = new ReentrantReadWriteLock(); // 建立對象,使用的是非公平的 this.rLock = this.rwlock.readLock(); // 獲取讀取鎖對象 arrayList = new ArrayList<>(); // 實例化 this.wLock = this.rwlock.writeLock(); // 獲取寫入鎖對象 } // 將unlock方法放在finally中確保執行中代碼出現異常仍然可以釋放鎖,不然將會形成其它的線程阻塞 // //向arraylist中寫入數據 public void put() { this.wLock.lock(); // 獲取寫入鎖 try { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "正在執行寫入操做,寫入" + i); this.arrayList.add(i); } } finally { this.wLock.unlock(); } } // 從arraylist中讀取數據,這裏只是隨機讀取使用的是get,並無作什麼修改,由於這僅僅是讀取操做,若是進行了修改必須實現同步 public void get() { this.rLock.lock(); // 獲取讀取操做的鎖 Random random = new Random(); if (!arrayList.isEmpty()) { try { for (int i = 0; i < 10; i++) { int index = random.nextInt(this.arrayList.size() - 1); int data = this.arrayList.get(index); System.out.println(Thread.currentThread().getName() + "正在讀取數據 " + data); } } finally { this.rLock.unlock(); } } else { System.out.println("ArrayList爲空"); } } } //線程的測試類,主要是建立對象啓動線程 public class Test { public static void main(String[] args) { final MyThread thread = new MyThread(); // 建立對象 ArrayList<Thread> arrayList = new ArrayList<>(); /* * 建立8個讀線程,2個寫線程 */ for (int i = 0; i < 2; i++) { arrayList.add(new Thread() { @Override public void run() { thread.put(); } }); } for(int i=0;i<8;i++) { arrayList.add(new Thread(){ @Override public void run() { thread.get(); } }); } for (Thread t : arrayList) { t.start(); } } }
結果以下圖:
從上面能夠看出寫入線程都是一個一個執行的,讀取線程是一塊兒執行的
注意: 全部的鎖對象對於線程來講必須是全局變量,不然毫無心義。讀線程只能進行不影響線程安全性的操做,好比不能進行對數據的修改插入,若是想要進行修改的話必須還要使用鎖對必要的代碼實現同步操做