前面幾篇文章分析了java.util.concurrent.atomic包下的原子類和synchronized同步鎖,這篇分析JUC的locks包下的鎖類。java.util.concurrent.locks下的類不是很多,但是比較復(fù)雜,定義了基本的鎖Lock,對(duì)線程進(jìn)行park和unpark的LockSupport和核心的AQS框架(AbstractQueuedSynchronizer)。
先看下Lock的源碼:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
是只有六個(gè)方法的接口。Lock和synchronized具有同樣的含義和功能,synchronized 鎖在退出塊時(shí)自動(dòng)釋放,而Lock 需要手動(dòng)釋放,Lock更加靈活。synchronized 是系統(tǒng)關(guān)鍵字,Lock則是jdk1.5以來提供的一個(gè)接口。
synchronized缺點(diǎn)很明顯:一個(gè)正在等候獲得synchronized鎖的線程無法被中斷;也無法通過投票得到鎖,如果想要得到鎖那么就必須得等下去直到釋放鎖;synchronized還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進(jìn)行。
而Lock(如ReentrantLock )除了與Synchronized 具有相同的語義外,還支持鎖投票、定時(shí)鎖等候和可中斷鎖等候(就是說在等待鎖的過程中,可以被中斷)的一些特性。調(diào)用lockInterruptibly后,或者獲得鎖,或者被中斷后拋出異常。優(yōu)先響應(yīng)異常。
Lock 接口有 3 個(gè)實(shí)現(xiàn)它的類:ReentrantLock、ReetrantReadWriteLock.ReadLock 和 ReetrantReadWriteLock.WriteLock,即重入鎖、讀鎖和寫鎖,下面介紹下ReentrantLock。
ReentrantLock,意思是“可重入鎖”,ReentrantLock實(shí)現(xiàn)了Lock接口,并且ReentrantLock提供了更多的方法。ReentrantLock的鎖內(nèi)部實(shí)現(xiàn)通過NonfairSync和FairSync實(shí)現(xiàn),它提供了兩個(gè)構(gòu)造器:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
無參數(shù)構(gòu)造器采用默認(rèn)的NonfairSync機(jī)制,第二個(gè)構(gòu)造器根據(jù)參數(shù)來決定使用公平鎖還是非公平鎖。
synchronized 采用的同步策略稱為阻塞同步,它屬于一種悲觀的并發(fā)策略,即線程獲得的是獨(dú)占鎖。獨(dú)占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。而在 CPU 轉(zhuǎn)換線程阻塞時(shí)會(huì)引起線程上下文切換,當(dāng)有很多線程競爭鎖的時(shí)候,會(huì)引起 CPU 頻繁的上下文切換導(dǎo)致效率很低。
隨著指令集的發(fā)展,我們有了另一種選擇:基于沖突檢測的樂觀并發(fā)策略,通俗地講就是先進(jìn)性操作,如果沒有其他線程爭用共享數(shù)據(jù),那操作就成功了,如果共享數(shù)據(jù)被爭用,產(chǎn)生了沖突,那就再進(jìn)行其他的補(bǔ)償措施(最常見的補(bǔ)償措施就是不斷地自旋重拾,直到試成功為止),這種樂觀的并發(fā)策略的許多實(shí)現(xiàn)都不需要把線程掛起,因此這種同步被稱為非阻塞同步。ReetrantLock 采用的便是這種并發(fā)策略加上LockSupport提供的park/unPark操作。
“欲知天道,察其數(shù)”,想知其原理,先看其表現(xiàn),那么我們先通過例子看下如何正確使用ReentrantLock,然后再探究其原理。測試代碼如下:
public class Fs {
private int cnt = 0;
public static void main(String args[]) {
Fs fs = new Fs();
int threadCnt = 10;
Thread[] threads = new Thread[threadCnt];
CountDownLatch cdt = new CountDownLatch(threadCnt);
for (int i = 0; i < threadCnt; i++) {
threads[i] = new Thread(() -> {
Lock lock = new ReentrantLock();
lock.lock();
try {
for (int j = 0; j < 10000; j++) {
fs.add();
}
} catch (Exception e) {
} finally {
lock.unlock();
}
cdt.countDown();
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
// for (Thread i : threads) {
// try {
// i.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
// try {
// cdt.await();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(fs.cnt + " ");
}
private void add() {
++cnt;
}
}
Java并發(fā)-synchronized從入門到精通這篇文章結(jié)尾介紹了JVM對(duì)鎖優(yōu)化的手段包括鎖清除,根據(jù)鎖清除和代碼逃逸原理,各位朋友猜猜看上面的代碼能輸出我們期望的值10,000嗎?
答案是不能。lock屬于一個(gè)局部變量不會(huì)從當(dāng)前線程中逃逸出去,因此也不會(huì)被其他線程所使用,因此不可能存在共享資源競爭的情景,JVM會(huì)自動(dòng)將其鎖消除。所以輸出結(jié)果總是小于10,000。正確的代碼如下:
public class Fs {
private int cnt = 0;
private Lock lock = new ReentrantLock();
public static void main(String args[]) {
Fs fs = new Fs();
int threadCnt = 10;
Thread[] threads = new Thread[threadCnt];
CountDownLatch cdt = new CountDownLatch(threadCnt);
for (int i = 0; i < threadCnt; i++) {
threads[i] = new Thread(() -> {
fs.lock.lock();
try {
for (int j = 0; j < 10000; j++) {
fs.add();
}
} catch (Exception e) {
} finally {
fs.lock.unlock();
}
cdt.countDown();
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(fs.cnt + " ");
}
private void add() {
++cnt;
}
}
關(guān)于lock()方法的使用,需要注意以下幾點(diǎn):
-
lock()和unlock()必須成對(duì)出現(xiàn),如果只出現(xiàn)lock()會(huì)導(dǎo)鎖不會(huì)被釋放,如果只出現(xiàn)unlock()會(huì)拋出異常java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457) at Fs.lambda$main$0(Fs.java:25) at java.lang.Thread.run(Thread.java:745)。 -
lock()和unlock()之間的同步代碼塊一定要用try-catch處理,就算你百分之一萬的確定同步代碼塊不會(huì)發(fā)生異常,也最好加上try-catch包裹起來,如果不用try-catch,那么萬一出現(xiàn)異常的話,unlock就不會(huì)執(zhí)行而導(dǎo)致鎖無法被釋放,而且unlock要放到finally語句里。 -
lock()不會(huì)響應(yīng)中斷,如果想要響應(yīng)中斷,需要使用lockInterruptibly()方法。
關(guān)于synchronized和Lock,需要再提一下:
- 等待synchronized鎖的線程無法被中斷(中斷標(biāo)記位無法被設(shè)置為true),獲得synchronized鎖的線程可以被中斷;
- 等待Lock鎖的線程可以被中斷(這里是指中斷標(biāo)記位被設(shè)置為true),但是
lock()方法無法響應(yīng)中斷,lockInterruptibly()可以響應(yīng)中斷。
關(guān)于線程的中斷,你需要理解這三個(gè)方法才能繼續(xù)往下閱讀:
//中斷線程(實(shí)例方法)
public void Thread.interrupt();
//判斷線程是否被中斷(實(shí)例方法)
public boolean Thread.isInterrupted();
//判斷是否被中斷并清除當(dāng)前中斷狀態(tài)(靜態(tài)方法)
public static boolean Thread.interrupted();
為了證明等待Lock鎖的線程可以被中斷,我寫了個(gè)程序?qū)iT測試下:
public class Fg implements Runnable {
@Override
public void run() {
lock.lock();
f();
//中斷判斷
System.out.println("等待鎖線程執(zhí)行完畢" + Thread.currentThread().isInterrupted());
lock.unlock();
}
Fg() {
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
f();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("獲得了鎖的線程執(zhí)行完畢");
lock.unlock();
}
}, "獲得了鎖的線程開始執(zhí)行").start();
}
private void f() {
System.out.println(Thread.currentThread().getName() + " " + Math.random());
}
private int cnt = 0;
private Lock lock = new ReentrantLock();
public static void main(String args[]) {
Fg fg = new Fg();
Thread t = new Thread(fg, "等待鎖線程開始執(zhí)行");
t.start();
t.interrupt();
System.out.println(t.isInterrupted());
}
}
代碼輸出是:
true
獲得了鎖的線程開始執(zhí)行 0.40285651579626425
獲得了鎖的線程執(zhí)行完畢
等待鎖線程開始執(zhí)行 0.8003535914600582
等待鎖線程執(zhí)行完畢true
上述代碼中,線程t啟動(dòng)前,它的內(nèi)部已經(jīng)啟動(dòng)了一個(gè)內(nèi)部線程并且獲得了lock鎖,t啟動(dòng)的時(shí)候也企圖獲得lock鎖,因此會(huì)被阻塞,然后調(diào)用t.interrupt();中斷t,日志顯示中斷標(biāo)記為已經(jīng)是true說明中斷成功,內(nèi)部線程3秒后釋放鎖,線程t獲得鎖后執(zhí)行后面的代碼??梢钥吹?,等待鎖的線程t能被中斷,但是如果調(diào)用lock()方法獲取鎖失敗,也是會(huì)自旋重試的(外觀效果和阻塞是一樣的,但是CAS的自旋比阻塞高效很多),直到獲取鎖成功,如果想要響應(yīng)中斷而不被自旋等待,需要使用lockInterruptibly方法。
為了證明獲得Lock鎖的線程也能被中斷(lock()方法同樣無法響應(yīng)中斷,lockInterruptibly()可以響應(yīng)中斷,我這里的被中斷意思是說中斷標(biāo)記位被成功設(shè)置為true),我又寫了個(gè)程序?qū)iT測試下:
public class Fs {
private int cnt = 0;
private Lock lock = new ReentrantLock();
public static void main(String args[]) {
Fs fs = new Fs();
int threadCnt = 1;
Thread[] threads = new Thread[threadCnt];
CountDownLatch cdt = new CountDownLatch(threadCnt);
for (int i = 0; i < threadCnt; i++) {
threads[i] = new Thread(() -> {
fs.lock.lock();
try {
while(true) {
if (Thread.currentThread().isInterrupted()) {//這個(gè)if不會(huì)重置中斷標(biāo)記位,想要重置中斷標(biāo)記位需要使用Thread.interrupted()
System.out.println("中斷線程!! " + Thread.currentThread().isInterrupted() + " " + threads[0].isInterrupted());
break;
} else {
System.out.println(Thread.currentThread().getName() + Math.random());
}
}
} catch (Exception e) {
System.out.println("異常了:" + e.toString());
} finally {
fs.lock.unlock();
}
cdt.countDown();
});
threads[i].start();
}
threads[0].interrupt();
System.out.println("threads[0].isInterrupted(): " + threads[0].isInterrupted());
System.out.println("fs.cnt " + fs.cnt);
}
}
輸出結(jié)果是(輸出順序不固定,因?yàn)槿齻€(gè)輸出的代碼是運(yùn)行在兩個(gè)線程中的)
threads[0].isInterrupted(): true
中斷線程!! true true
fs.cnt 0
輸出結(jié)果給我們兩點(diǎn)提示
- threads[0]獲得了鎖,而且中斷標(biāo)記位能被設(shè)置為true;
- 可以看到被中斷的線程threads[0]的中斷標(biāo)記位一直是true,因此我們知道interrupt不會(huì)重置中斷標(biāo)記位,如果想要重置中斷標(biāo)記位,那么需要if條件需要使用
Thread.interrupted()而不是Thread.currentThread().isInterrupted(),在while(true)后面的if條件語句你可以使用Thread.interrupted()代替,看下輸出結(jié)果回是這樣的:
threads[0].isInterrupted(): true
中斷線程!! false false
fs.cnt 0
即中斷標(biāo)記位被重置(為啥第一行輸出的標(biāo)記位是true,我猜測是Thread.interrupted()重置標(biāo)記位需要一定的時(shí)間,即還沒有重置為false第一行日志就打印出來了,如果在最后延遲1秒后再打印threads[0]的中斷標(biāo)記位,就是false了,這個(gè)猜測我是親自試過的,是對(duì)的)。
tryLock()方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失?。存i已被其他線程獲?。?,則返回false,也就說這個(gè)方法無論如何都會(huì)立即返回。在拿不到鎖時(shí)不會(huì)一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會(huì)等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。
條件變量實(shí)現(xiàn)線程間協(xié)作
synchronized可以配合使用 Object 對(duì)象的 wait()和 notify()或 notifyAll()方法來實(shí)現(xiàn)線程間協(xié)作。Java 5 之后,我們可以用 Reentrantlock 鎖配合 Condition 對(duì)象上的 await()和 signal()或 signalAll()方法來實(shí)現(xiàn)線程間協(xié)作。在 ReentrantLock 對(duì)象上 newCondition()可以得到一個(gè) Condition 對(duì)象,可以通過在 Condition 上調(diào)用 await()方法來掛起一個(gè)任務(wù)(線程),通過在 Condition 上調(diào)用 signal()來通知任務(wù),從而喚醒一個(gè)任務(wù),或者調(diào)用 signalAll()來喚醒所有在這個(gè) Condition 上被其自身掛起的任務(wù)。另外,如果使用了公平鎖,signalAll()的與 Condition 關(guān)聯(lián)的所有任務(wù)將以 FIFO 隊(duì)列的形式獲取鎖,如果沒有使用公平鎖,則獲取鎖的任務(wù)是隨機(jī)的,這樣我們便可以更好地控制處在 await 狀態(tài)的任務(wù)獲取鎖的順序。與 notifyAll()相比,signalAll()是更安全的方式。另外,它可以指定喚醒與自身 Condition 對(duì)象綁定在一起的任務(wù)。
下面是生產(chǎn)者——消費(fèi)者模型的代碼:
class Info { // 定義信息類
private String name = "name";//定義name屬性,為了與下面set的name屬性區(qū)別開
private String content = "content";// 定義content屬性,為了與下面set的content屬性區(qū)別開
private boolean flag = true; // 設(shè)置標(biāo)志位,初始時(shí)先生產(chǎn)
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition(); //產(chǎn)生一個(gè)Condition對(duì)象
public void set(String name, String content) {
lock.lock();
try {
while (!flag) {
condition.await();
}
this.name = name; // 設(shè)置名稱
this.content = content; // 設(shè)置內(nèi)容
Thread.sleep(300);
System.out.println("生產(chǎn) " + this.name + " --> " + this.content);
flag = false; // 改變標(biāo)志位,表示可以取走
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void get() {
lock.lock();
try {
while (flag) {
condition.await();
}
System.out.println("消費(fèi) " + this.name + " --> " + this.content);
Thread.sleep(300);
flag = true; // 改變標(biāo)志位,表示可以生產(chǎn)
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class Producer implements Runnable { // 通過Runnable實(shí)現(xiàn)多線程
private Info info = null; // 保存Info引用
public Producer(Info info) {
this.info = info;
}
public void run() {
for (int i = 0; i < 10; i++) {
this.info.set("姓名--" + i, "內(nèi)容--" + +i); // 設(shè)置名稱
}
}
}
class Consumer implements Runnable {
private Info info = null;
public Consumer(Info info) {
this.info = info;
}
public void run() {
for (int i = 0; i < 10; i++) {
this.info.get();
}
}
}
public class ThreadCaseDemo {
public static void main(String args[]) {
Info info = new Info(); // 實(shí)例化Info對(duì)象
Producer pro = new Producer(info); // 生產(chǎn)者
Consumer con = new Consumer(info); // 消費(fèi)者
new Thread(pro).start();
//啟動(dòng)了生產(chǎn)者線程后,再啟動(dòng)消費(fèi)者線程
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(con).start();
}
}
輸出:
生產(chǎn) 姓名--0 --> 內(nèi)容--0
消費(fèi) 姓名--0 --> 內(nèi)容--0
生產(chǎn) 姓名--1 --> 內(nèi)容--1
消費(fèi) 姓名--1 --> 內(nèi)容--1
生產(chǎn) 姓名--2 --> 內(nèi)容--2
消費(fèi) 姓名--2 --> 內(nèi)容--2
生產(chǎn) 姓名--3 --> 內(nèi)容--3
消費(fèi) 姓名--3 --> 內(nèi)容--3
生產(chǎn) 姓名--4 --> 內(nèi)容--4
消費(fèi) 姓名--4 --> 內(nèi)容--4
生產(chǎn) 姓名--5 --> 內(nèi)容--5
消費(fèi) 姓名--5 --> 內(nèi)容--5
生產(chǎn) 姓名--6 --> 內(nèi)容--6
消費(fèi) 姓名--6 --> 內(nèi)容--6
生產(chǎn) 姓名--7 --> 內(nèi)容--7
消費(fèi) 姓名--7 --> 內(nèi)容--7
生產(chǎn) 姓名--8 --> 內(nèi)容--8
消費(fèi) 姓名--8 --> 內(nèi)容--8
生產(chǎn) 姓名--9 --> 內(nèi)容--9
消費(fèi) 姓名--9 --> 內(nèi)容--9
上面代碼通過條件變量condition的await()和signal()達(dá)到生產(chǎn)者線程和消費(fèi)者線程間的同步與協(xié)作。
與 synchronized 性能比較
在 JDK1.5 中,synchronized 是性能低效的。因?yàn)檫@是一個(gè)重量級(jí)操作,它對(duì)性能最大的影響是阻塞的是實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力。相比之下使用Java 提供的 Lock 對(duì)象,性能更高一些。Brian Goetz 對(duì)這兩種鎖在 JDK1.5、單核處理器及雙 Xeon 處理器環(huán)境下做了一組吞吐量對(duì)比的實(shí)驗(yàn),發(fā)現(xiàn)多線程環(huán)境下,synchronized的吞吐量下降的非常嚴(yán)重,而ReentrankLock 則能基本保持在同一個(gè)比較穩(wěn)定的水平上。但與其說 ReetrantLock 性能好,倒不如說 synchronized 還有非常大的優(yōu)化余地,于是到了 JDK1.6,發(fā)生了變化,對(duì) synchronize 加入了很多優(yōu)化措施,有自適應(yīng)自旋,鎖消除,鎖粗化,輕量級(jí)鎖,偏向鎖等等。導(dǎo)致在 JDK1.6 上 synchronize 的性能并不比 Lock 差。官方也表示,他們也更支持 synchronize,在未來的版本中還有優(yōu)化余地,所以還是提倡在 synchronized 能實(shí)現(xiàn)需求的情況下,優(yōu)先考慮使用 synchronized 來進(jìn)行同步。
前面講了很多,但是并沒有解釋ReentrantLock的鎖機(jī)制,關(guān)于ReentrantLock的鎖機(jī)制,我打算放到下一篇文章中分析,不然文章太長,讀起來會(huì)不會(huì)覺得有點(diǎn)累。
參考文獻(xiàn)