標籤:java 多線程
轉載請註明出處:http://blog.csdn.net/xingjiarong/article/details/47679007
在上一篇部落格中,我們討論了Race Condition現象以及它產生的原因,現在我們知道它是不好的一種現象了,那麼我們有什麼方法避免它呢。最直接有效方式就是放棄多線程,直接改為使用單線程但操作資料,但是這是不優雅的,因為我們知道有時候,多線程有它自己的優勢。在這裡我們討論兩種其他的方法——鎖對象和條件對象。
鎖對象
java SE5.0之後為實現多線程的互斥引入了ReentrantLock類。ReentrantLock類一個可重新進入的互斥鎖 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行為和語義,但功能更強大。
ReentrantLock類有兩種構造方法:
構造方法
一、不帶公平參數的構造方法
private ReentrantLock lock = new ReentrantLock();
預設的是非公平鎖,這種鎖不會根據線程等待時間的長短來優先調度線程。
這樣就構造了一個鎖對象lock。
二、帶公平參數的鎖對象
private ReentrantLock lock = new ReentrantLock(true);
此類的構造方法接受一個可選的公平 參數。當設定為 true 時,在多個線程的爭用下,這些鎖傾向於將訪問權授予等待時間最長的線程。否則此鎖將無法保證任何特定訪問順序。
公平鎖和非公平鎖的區別:
與採用預設設定(使用不公平鎖)相比,使用公平鎖的程式在許多線程訪問時表現為很低的總體輸送量(即速度很慢,常常極其慢),但是在獲得鎖和保證鎖分配的均衡性時差異較小。不過要注意的是,公平鎖不能保證線程調度的公平性。因此,使用公平鎖的眾多線程中的一員可能獲得多倍的成功機會,這種情況發生在其他活動線程沒有被處理並且目前並未持有鎖時。
使用方法
class X { private final ReentrantLock lock = new ReentrantLock(); // 其他變數的定義 public void m() { lock.lock(); // 當試圖獲得鎖時,如果鎖已經被別的線程佔有,那麼該線程會一直被阻塞,直到獲得鎖 try { // 處理資料 } finally { lock.unlock(); //釋放鎖 } }}
首先為大家介紹一下,ReentrantLock類的兩個最常用的方法:
lock()
獲得鎖對象,如果該鎖對象沒有被其他線程佔有,那麼可以立刻獲得鎖,並執行接下來的處理;如果鎖對象已經被其他對象佔有,那麼該線程就會被阻塞在請求鎖的對象的操作上,直到其他的線程釋放鎖,該線程得到鎖,才能繼續的向下執行。
unlock()
釋放鎖,已經獲得鎖對象的線程在操作完資料後要釋放鎖,以便其他的線程重新獲得鎖來執行自己的操作,否則所有的試圖獲得鎖的線程都不能繼續向下執行。
使用方法簡單明了,就是在執行各個線程都要操作相同資料的代碼之前請求鎖,在finally語句中釋放鎖,為什麼要在finally中釋放鎖呢,這是因為如果try語句塊中有語句發生異常,則會直接跳過try中所有的剩餘程式碼封裝括unlock(),所以鎖對象就不能得到釋放,其他的線程也不能繼續向下執行,導致程式不能繼續執行,我們在finally中釋放鎖,這樣就能保證一定可以將鎖釋放掉,不管獲得鎖的線程是不是正確的執行結束。
現在來使用這個方法修改一下我們上一篇部落格中的代碼:
import java.util.concurrent.locks.ReentrantLock;class MyThread implements Runnable { /** * 計算類型,1表示減法,其他的表示加法 */ private int type; /** * 鎖對象 */ private static ReentrantLock lock = new ReentrantLock(); public MyThread(int type) { this.type = type; } public void run() { if (type == 1) for (int i = 0; i < 10000; i++) { lock.lock(); Test.num--; lock.unlock(); } else for (int i = 0; i < 10000; i++) { lock.lock(); Test.num++; lock.unlock(); } }}public class Test { public static int num = 1000000; public static void main(String[] args) { Thread a = new Thread(new MyThread(1)); Thread b = new Thread(new MyThread(2)); a.start(); b.start(); /* * 主線程等待子線程完成,然後再列印數值 */ try { a.join(); b.join(); } catch (Exception e) { e.printStackTrace(); } System.out.println(num); }}
再多運行幾次,是不是結果都是正確的呢。
現在,我們來解釋一下,什麼是可重新進入的鎖。ReentrantLock類中有一個計數器,用來表示一個線程擷取鎖的數量,初始值為0,當一個線程獲得鎖時,該值被置為1,可重新進入的意思就是,已經獲得鎖的線程還可以繼續調用同一個鎖所保護的方法,也就是再一次獲得鎖,當再一次獲得鎖時,ReentrantLock中的計數器就加1,每釋放一次鎖,計數器就減1,當計數器減為0的時候,這個線程才是真正的釋放了這個鎖。
我們接著討論另外一個非常重要的問題。ReentrantLock類是依賴於建立它的類的對象。什麼意思呢,就是說如果兩個線程同時訪問同一個ReentrantLock對象的lock()方法保護的方法時,OK,這是沒有問題的,鎖對象會成功的保護資料操作不會出錯。但是如果兩個線程同時訪問ReentrantLock類的不同對象的被lock()保護的方法,那麼這兩個線程是不會相互影響的,也就是說lock()方法這時不能保證資料的正確性。
我們來看一下上邊那個代碼的這一部分:
Thread a = new Thread(new MyThread(1));Thread b = new Thread(new MyThread(2));
這裡建立了兩個對象a和b,所以他們每個類都有自己的ReentrantLock對象,這就是我們上邊所說的ReentrantLock類的不同對象,這樣如果兩個線程分別操作a和b的資料,lock方法是不會有效。
不信我們試試看這個代碼:
import java.util.concurrent.locks.ReentrantLock;class MyThread implements Runnable { /** * 計算類型,1表示減法,其他的表示加法 */ private int type; /** * 鎖對象 */ private ReentrantLock lock = new ReentrantLock(); public MyThread(int type) { this.type = type; } public void run() { if (type == 1) for (int i = 0; i < 10000; i++) { lock.lock(); Test.num--; lock.unlock(); } else for (int i = 0; i < 10000; i++) { lock.lock(); Test.num++; lock.unlock(); } }}public class Test { public static int num = 1000000; public static void main(String[] args) { Thread a = new Thread(new MyThread(1)); Thread b = new Thread(new MyThread(2)); a.start(); b.start(); /* * 主線程等待子線程完成,然後再列印數值 */ try { a.join(); b.join(); } catch (Exception e) { e.printStackTrace(); } System.out.println(num); }}
注意這裡的代碼和上邊的代碼並不一樣,唯一的區別就在於聲明ReentrantLock對象時前邊是否加了static,這裡是沒有static修飾,再運行幾次,是不是結果是不正確的呢。
為什麼會這樣呢?因為如果被static修飾,那麼兩個線程就是共用的同一個lock對象,如果不被static修飾,那麼每個線程就是使用的它自己的lock對象,所以不會static修飾就會出現錯誤。
在下一篇部落格裡,我會為大家介紹java條件對象,希望與大家一起學習一起進步,請大家繼續關注我的部落格,如果大家支援我的話,就頂我一下吧。
著作權聲明:本文為博主原創文章,轉載請註明出處,查看原文章,請訪問:http://blog.csdn.net/xingjiarong
java多線程(二)鎖對象