使用Lock
本文主要講述如何解決非線程安全問題,感謝java多線程核心編程一書,爲本系列文章提供參考借鑑
一、使用ReentrantLock
1.使用ReentrantLock方法:
- lock():鎖定
- unlock():解除鎖定
- int getHoldCount():產訊當前線程保持此鎖定的個數,也就是調用lock()方法的次數
- int getQueueLength():返回正在等待獲取此鎖定的線程估計數,例如:8個線程,3個線程調用了await()方法吧,那麼在調用此方法後返回的值是5,那麼就說嘛有5個線程同時在等待lock的釋放。
- int getWaitQueueLength():顧名思義,其作用是返回等待與此鎖定相關的給定條件Condition的線程估計數。例如:8個線程,每個線程都調用了同一個Condition的await()方法,則調用此方法返回的值是8.
- boolean hasQueuedThread():查詢指定線程是否正在等待獲取此鎖定。ReentrantLock.hasQueuedThread(thread);
- boolean hasWaiters():查詢是否有線程正在等待與此鎖定有關的Condition條件。
- boolean isFair():判斷是否是公平鎖
- boolean isHeldByCurrentThread():查詢當前線程是否保持此鎖定。
- boolean isLocked():查詢此鎖定是否由任意線程保持。
- void lockInterruptibly():如果當前線程未被中斷,則獲取鎖定,如果已經被中斷則出現異常。
- boolean tryLock():僅在調用時鎖定未被另一個線程保持的情況下,才獲取該鎖定。
- boolean tryLock(long timeout,TimeUnit unit):如果鎖定在給定等待時間內沒有被另一個線程保持,且當前線程未被中斷,則獲取該鎖定。
2.使用ReentrantLock實現同步效果:
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
public void printReentrantLock() {
try {
lock.lock();//獲取鎖
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" + Thread.currentThread().getName() + ",i=" + i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();//釋放鎖
}
}
public static void main(String[] args) {
final MyServer myServer = new MyServer();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
myServer.printReentrantLock();
}
});
thread.setName(String.valueOf(i));
thread.start();
}
}
}
運行結果爲:
由結果分析知:當前線程獲取鎖對象後其他線程呈阻塞狀態,直到當前線程打印完畢釋放鎖,其他線程才能獲取到鎖進行打印。
這個主要進行單個方法同一個鎖多線程調用的同步效果,下面來看一下多個線程交叉調用不同方法同一個鎖的例子:
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
public void methodA() {
try {
lock.lock();//獲取鎖
System.out.println("method A begin ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
methodB();//調用methodB
Thread.sleep(5000);
System.out.println("method A end ThreadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void methodB() {
try {
lock.lock();//獲取鎖
System.out.println("method B begin ThreadName="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("method B end ThreadName="+Thread.currentThread().getName()+" time="+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
String[] names = {"A", "AA", "B", "BB"};
final MyServer myServer = new MyServer();
Thread[] threads = new Thread[4];
for (int i = 0; i < 2; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
myServer.methodA();
}
});
}
for (int i = 2; i < 4; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
myServer.methodB();
}
});
}
for (int i = 0; i < 4; i++) {
threads[i].setName(names[i]);
threads[i].start();
}
}
結果如下:
分析結果:在線程A獲取鎖時其他線程呈阻塞狀態直至線程A執行完畢且釋放鎖,其他線程纔可以獲取到鎖,同時在線程A獲取鎖執行過程中又成功調用了methodB,這些說明reentrantlock和synchronized關鍵字一樣線程之間是同步順序執行的,也都具有鎖重入特性。
二、使用Condition實現等待/通知
關鍵字synchronized於wait()和notify()/notifyAll()方法相結合可以實現等待/通知模式,類ReentrantLock也可以試想同樣的功能,但需要藉助於Condition對象。相對於notify和notifyAll,Condition具有跟高的靈活性,在使用notify和notifyAll時,被通知的線程是由JVM隨機選擇的,而ReentrantLock結合Condition類是可以“選擇性通知的”。
1.Condition中通知/等待方法
- await():相當於Object類中的wait()方法
- await(long time,TimeUtil unit):相當於Object類中的wait(long timeout)方法
- signal():相當於Object類中的notify()方法
- signalAll():相當於Object類中的notifyAll()方法
2.簡單使用:
注意:調用condition.await()之前需要執行lock.lock()來獲取同步監視器。
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void await() {
try {
lock.lock();
System.out.println(" await 時間爲 "+ System.currentTimeMillis());
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() {
try {
lock.lock();
System.out.println(" signal 時間爲 "+System.currentTimeMillis());
condition.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
new Thread(new Runnable() {
public void run() {
myServer.await();
}
}).start();
Thread.sleep(3000);
myServer.signal();
}
}
運行結果爲:
3.使用多個Condition實現”選擇性“通知線程
代碼如下:
public class MyServer {
private ReentrantLock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
@SuppressWarnings("all")
public void awaitA() {
try {
lock.lock();
System.out.println("begin await A 時間爲 "+ System.currentTimeMillis());
conditionA.await();
System.out.println("end await A 時間爲 "+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@SuppressWarnings("all")
public void awaitB() {
try {
lock.lock();
System.out.println("begin await B 時間爲 "+ System.currentTimeMillis());
conditionB.await();
System.out.println("end await B 時間爲 "+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@SuppressWarnings("all")
public void signalAll_A() {
try {
lock.lock();
System.out.println(" signalall B 時間爲 "+System.currentTimeMillis());
conditionA.signalAll();
} finally {
lock.unlock();
}
}
@SuppressWarnings("all")
public void signalAll_B() {
try {
lock.lock();
System.out.println(" signalall B 時間爲 "+System.currentTimeMillis());
conditionB.signalAll();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.awaitA();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.awaitB();
}
});
threadB.setName("B");
threadB.start();
Thread.sleep(3000);
myServer.signalAll_B();
}
}
在上述代碼中我們定義了兩個Condition,分別用於兩個線程的等待和喚醒,而我們只喚醒線程B而線程A則繼續等待。結果如下:
三、公平鎖與非公平鎖
公平鎖就是線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進先出的順序。非公平鎖就是隨機獲取鎖的,屬於一種槍戰機制。先來不一定先得到,誰搶到屬於誰,所以是不公平的。
通過構造方法ReetrantLock(boolean isFair)來創建對應公平鎖或不公平鎖。無參默認爲非公平鎖。
四、使用ReentrantReadWriteLock類
使用ReetrantLock具有完全互斥排他的效果,即同一時間只有一個線程能夠執行Reentrantlock.lock()方法後的代碼。雖然這樣能保持實例變量的線程安全性,但是效率確實非常低的。然而使用讀寫鎖ReentrantReadWriteLock類,可以提高運行效率,在不需要操作實例變量的情況下,可以使用讀寫鎖ReentrantReadWriteLock來提高任務的執行效率。
讀寫鎖ReentrantReadWriteLock有兩個鎖,一個是讀操作的鎖即共享鎖,一個是寫操作鎖即排它鎖。在多少個讀鎖之間不互斥,讀鎖和寫鎖互斥,寫鎖和寫鎖互斥。在多個線程中,如果沒有線程進行寫操作的時,可以有多個線程同時進行讀操作;而其中一個線程正在寫操作時,其他線程的讀操作或寫操作都只能等當前的寫線程執行完畢。
1.讀寫或寫讀互斥
public class MyServer {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read() {
try {
lock.readLock().lock();
System.out.println("獲得讀鎖: "+Thread.currentThread().getName()+" "+System.currentTimeMillis());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
public void write() {
try {
lock.writeLock().lock();
System.out.println("獲得寫鎖: "+Thread.currentThread().getName()+" "+System.currentTimeMillis());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.write();
}
});
threadB.setName("B");
threadB.start();
Thread threadC = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadC.setName("C");
threadC.start();
}
}
結果如:
由結果可知,讀寫和寫讀是互斥的
2.讀讀共享
更改上面的main方法:
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.read();
}
});
threadB.setName("B");
threadB.start();
}
結果如:
由結果可知:讀和讀共享
3.寫寫互斥
更改上面的main方法:
public static void main(String[] args) throws InterruptedException {
final MyServer myServer = new MyServer();
Thread threadA = new Thread(new Runnable() {
public void run() {
myServer.write();
}
});
threadA.setName("A");
threadA.start();
Thread threadB = new Thread(new Runnable() {
public void run() {
myServer.write();
}
});
threadB.setName("B");
threadB.start();
}
結果如:
由結果知:寫寫互斥