synchronized可以用于控制一個線程是否可以訪問臨界區(qū)資源,Object.wait()和Object.notify()方法可以實現(xiàn)線程等待和通知。這些工具都很簡單可靠,但是想要實現(xiàn)更復雜和高級的功能,就要用到Java中的鎖。
鎖
1.重入鎖(ReentrantLock)
-
lock.lock()簡單的上鎖
重入鎖完全可以替代synchronized關鍵字,在Java早期版本中重入鎖的性能遠遠優(yōu)于synchronized,而從JDK1.6開始,synchronized進行了大量的優(yōu)化,兩者性能不相上下。
public class Main {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lock.lock();
lock.lock();
try {
System.out.println(LocalDateTime.now());
} finally {
lock.unlock();
lock.unlock();
}
}).start();
}
}
}
重入鎖如它的名字一樣,一個線程可以連續(xù)兩次獲得同一把鎖,(否則線程會在第二次請求鎖時和自己產(chǎn)生死鎖),同樣,多次獲得鎖之后也要多次釋放鎖,否則其他線程將無法獲取鎖。
-
lock.lockInterruptibly()響應中斷
public class Main {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
t2.interrupt();
}
static class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
lock.lockInterruptibly();
System.out.println(LocalDateTime.now());
} catch (InterruptedException e) {
System.out.println("break");
break;
} finally {
lock.unlock();
}
}
}
}
}
使用lock.lockInterruptibly()可以使線程在等待鎖時響應中斷,此時線程會拋出一個InterruptedException異常并放棄鎖的競爭。
-
lock.tryLock()超時結束
public boolean tryLock()
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
無參數(shù)的lock.tryLock()方法會在調(diào)用后嘗試獲得鎖,如果成功立刻返回true,失敗立刻返回false。而有參數(shù)的方法可以持續(xù)請求一段時間后自動退出請求并返回false,同時有參數(shù)的lock.tryLock()同樣可以響應中斷。
-
new ReentrantLock(true)公平鎖
使用synchronized關鍵字進行鎖控制,產(chǎn)生的鎖就是非公平鎖,即在分配鎖時不會管線程請求鎖的時間先后,所有線程都有可能分配到鎖。而公平鎖則按照請求鎖的時間先后分配鎖,這保證了不會出現(xiàn)饑餓現(xiàn)象,但公平鎖需要維護一個有序的請求隊列,因此開銷更大,性能較低。
public ReentrantLock(boolean fair)
- 重入鎖的實現(xiàn)
第一,原子狀態(tài)。原子狀態(tài)使用CAS操作來存儲當前鎖的狀態(tài),判斷鎖是否已經(jīng)被其他線程持有。
第二,等待隊列。所有沒有請求到鎖的線程,會進入等待隊列中進行等待。待有線程釋放鎖之后,系統(tǒng)就能從等待隊列中喚醒一個線程,繼續(xù)工作。
第三,阻塞原語(park)和(unpark),用來掛起和恢復線程。沒有得到鎖的線程將被掛起。
2.Condition接口
Condition的作用類似于Object.wait()和Object.notify(),不過Condition是用于和ReentrantLock合作。它有以下方法。
// await()方法會使當前線程等待,同時釋放當前鎖,當其他線程使用
// signal()或signalAll()方法時,線程會重新獲得鎖并繼續(xù)執(zhí)行?;蛘?// 當線程被中斷時,也能跳出等待。功能上類似于Object.wait()方法。
void await() throws InterruptedException;
// 與await()方法類似,但是并不響應中斷
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 用于喚醒一個在等待中的線程,類似于Object.notify()
void signal();
void signalAll();
類似于Object.wait()和Object.notify(),執(zhí)行前必須用synchronized獲得Object對象的鎖。Condition對象由鎖的newCondition()方法生成,使用await()和signal()方法前線程必須獲得鎖對象,而使用后要釋放鎖對象。
public Condition newCondition() { return sync.newCondition(); }
3.信號量(Semaphore)
信號量是對鎖的拓展,可以允許多個線程同時訪問臨界區(qū)資源。
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
構造信號量時,必須要指定信號量的準入數(shù),還可以指定是否公平分配信號量。
// 嘗試獲取信號量,獲取時可以響應中斷
public void acquire() throws InterruptedException
// 嘗試獲取信號量,獲取時不響應中斷
public void acquireUninterruptibly()
// 嘗試獲取信號量,立刻返回結果,成功為true,失敗為false
public boolean tryAcquire()
// 嘗試獲取信號量一段時間,可以響應中斷
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException
// 釋放占有的信號量
public void release()
信號量的方法與重入鎖基本類似,區(qū)別只有同時進入臨界區(qū)的線程數(shù)量。
4. 讀寫鎖(ReadWriteLock)
如果使用synchronized關鍵字或者重入鎖,則所有讀與讀之間、讀與寫之間、寫與寫之間都是串行操作,而讀寫鎖允許多個線程同時讀,寫寫和讀寫之間依然相互排斥。如果系統(tǒng)中的讀遠大于寫,則讀寫鎖可以很好的提升系統(tǒng)性能。
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
使用時,從ReadWriteLock上分別獲得讀鎖和寫鎖,讀寫操作時分別請求對應的鎖。
5.倒計時器(CountDownLatch)
倒計時器可以讓某個線程等待直到計數(shù)器歸零。
public class Main {
private static CountDownLatch count = new CountDownLatch(10);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(new Random().nextInt());
count.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// todo
}
}
countDownLatch.await()方法同樣可以響應中斷。
6.循環(huán)柵欄(CyclicBarrier)
循環(huán)柵欄類似于倒計時器但功能更多一些,首先循環(huán)柵欄可以重復觸發(fā),另外可以接受一個Runnable對象,作為一次計數(shù)完成后系統(tǒng)會觸發(fā)的動作。
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
public int await() throws InterruptedException, BrokenBarrierException
在使用時,每有一個線程執(zhí)行CyclicBarrier.await(),程序計數(shù)加一,到達指定數(shù)后就會觸發(fā)barrierAction。CyclicBarrier.await()會返回線程到達的名次,最后一個到達的將會返回0。CyclicBarrier.await()可以響應中斷,而當線程已經(jīng)不可能滿足程序計數(shù)時(比如某個線程被中斷),則其余線程會拋出BrokenBarrierException異常。
7.線程阻塞工具類(LockSupport)
LockSupport可以在線程中的任意位置讓線程阻塞,與Thread.suspend()方法相比,彌補了由于resume()方法發(fā)生導致線程無法繼續(xù)執(zhí)行的情況;與Object.wait()方法相比,不需要先獲得某個對象的鎖,也不會拋出InterruptedException異常。
public static void park(Object blocker)
public static void unpark(Thread thread)
即使unpark()方法發(fā)生在park()方法之前,它也能使下一次的park()方法立即返回,同時,處于park()方法掛起狀態(tài)的線程不會像Thread.suspend()方法一樣顯示Runnable狀態(tài),而是明確的Waiting狀態(tài),并且還會標注是由park()方法引起的。
8.限流(RateLimiter)
限流算法的思路:
- 最簡單的限流算法就是給出一個單位時間,然后使用一個計數(shù)器count統(tǒng)計單位時間內(nèi)收到的請求數(shù)量,當請求數(shù)量超過門限時,余下的請求丟棄或等待。
- 漏桶算法:利用一個緩沖區(qū),無論請求的速率如何,都先進入緩沖區(qū)等待,然后以固定的流速離開緩沖區(qū)。
- 令牌桶算法:系統(tǒng)以一定的速率生成令牌并存入令牌桶中,桶中只能存放一定時限內(nèi)的令牌。當有請求到來時,拿走桶中的一個令牌,如果桶中沒有令牌,則等待或丟棄請求。
RateLimiter就是是Google旗下一個庫Guava中的一個工具,采用了令牌桶算法來控制流量??梢苑乐惯^量的請求創(chuàng)建的線程壓垮服務器。