synchronized關(guān)鍵字
首先,來(lái)看一個(gè)多線程競(jìng)爭(zhēng)臨界資源導(dǎo)致的同步不安全問(wèn)題昏苏。
package com.example.weishj.mytester.concurrency.sync;
/**
* 同步安全測(cè)試
*
* 在無(wú)任何同步措施時(shí),并發(fā)會(huì)導(dǎo)致錯(cuò)誤的結(jié)果
*/
public class SyncTest1 implements Runnable {
// 共享資源(臨界資源)
private static int race = 0;
private static final int THREADS_COUNT = 10;
public void increase() {
race++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
SyncTest1 runnable = new SyncTest1();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(runnable);
threads[i].start();
}
// 等待所有累加線程都結(jié)束
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 期待的結(jié)果應(yīng)該是(THREADS_COUNT * 10000)= 100000
System.out.println("race = " + race + ", time: " + (System.currentTimeMillis() - start));
}
}
運(yùn)行結(jié)果:
race = 69309, time: 4
synchronized實(shí)例方法
- 鎖定實(shí)例對(duì)象(this)
以開(kāi)頭的代碼為例斤彼,對(duì) increase()
做同步安全控制:
// synchronized實(shí)例方法鸦泳,安全訪問(wèn)臨界資源
public synchronized void increase() {
race++;
}
運(yùn)行結(jié)果:
race = 100000, time: 29
既然鎖定的是this對(duì)象岔乔,那么任何同步安全就必須建立在當(dāng)前對(duì)象鎖的前提之上船老,脫離了當(dāng)前對(duì)象咖熟,就不再有同步安全可言。仍然以開(kāi)頭的代碼為例:
package com.example.weishj.mytester.concurrency.sync;
/**
* 同步安全測(cè)試
*
* 脫離了"同一個(gè)對(duì)象"的前提柳畔,synchronized實(shí)例方法將不再具有同步安全性
*/
public class SyncTest3 implements Runnable {
// 共享資源(臨界資源)
private static int race = 0;
private static final int THREADS_COUNT = 10;
// synchronized實(shí)例方法馍管,安全訪問(wèn)臨界資源
public synchronized void increase() {
race++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
// SyncTest3 runnable = new SyncTest3();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 不同的對(duì)象鎖,將導(dǎo)致臨界資源不再安全
threads[i] = new Thread(new SyncTest3());
threads[i].start();
}
// 等待所有累加線程都結(jié)束
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 期待的結(jié)果應(yīng)該是(THREADS_COUNT * 10000)= 100000
System.out.println("race = " + race + ", time: " + (System.currentTimeMillis() - start));
}
}
運(yùn)行結(jié)果:
race = 72446, time: 5
因此薪韩,使用synchronized實(shí)例方法時(shí)确沸,需要格外注意實(shí)例對(duì)象是不是同一個(gè):
- 單例:安全
- 非單例:同一個(gè)實(shí)例對(duì)象上才存在同步安全
另外捌锭,既然是針對(duì)對(duì)象加鎖,那么同一個(gè)對(duì)象中的多個(gè)同步實(shí)例方法之間罗捎,也是互斥的观谦。
package com.example.weishj.mytester.concurrency.sync;
/**
* 同步安全測(cè)試
*
* 同一個(gè)對(duì)象的不同synchronized實(shí)例方法之間,也是互斥的
*/
public class SyncTest4 {
private static final int THREADS_COUNT = 2;
public synchronized void a() {
int i = 5;
while (i-- > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", method: a, running...");
}
}
public synchronized void b() {
int i = 5;
while (i-- > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", method: b, running...");
}
}
public static void main(String[] args) {
final SyncTest4 instance = new SyncTest4();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
final int finalI = i;
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
if (finalI % 2 == 0) {
// 若通過(guò)不同對(duì)象調(diào)用方法ab宛逗,則ab之間不存在互斥關(guān)系
// new SyncTest4().a();
// 在同一個(gè)對(duì)象上調(diào)用方法ab坎匿,則ab之間是互斥的
instance.a();
} else {
// 若通過(guò)不同對(duì)象調(diào)用方法ab盾剩,則ab之間不存在互斥關(guān)系
// new SyncTest4().b();
// 在同一個(gè)對(duì)象上調(diào)用方法ab雷激,則ab之間是互斥的
instance.b();
}
}
});
threads[i].start();
}
}
}
運(yùn)行結(jié)果:
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
若兩個(gè)線程分別通過(guò)不同的對(duì)象調(diào)用方法ab(上述示例中被注釋的代碼),則ab之間就不存在互斥關(guān)系告私∈合荆可以通過(guò)上述示例中被注釋的代碼來(lái)驗(yàn)證,運(yùn)行結(jié)果:
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
綜上分析驻粟,synchronized實(shí)例方法
有以下關(guān)鍵點(diǎn)需要記赘俊:
- 鎖定實(shí)例對(duì)象(this)
- 每個(gè)實(shí)例都有獨(dú)立的對(duì)象鎖,因此只有針對(duì)同一個(gè)實(shí)例,才具備互斥性
- 同一個(gè)實(shí)例中的多個(gè)synchronized實(shí)例方法之間,也是互斥的
synchronized靜態(tài)方法
- 鎖定類對(duì)象(class)
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
/**
* 同步安全測(cè)試
*
* 同步靜態(tài)方法搓幌,實(shí)現(xiàn)線程安全
*/
public class SyncStaticTest1 implements Runnable {
// 共享資源(臨界資源)
private static int race = 0;
private static final int THREADS_COUNT = 10;
public static synchronized void increase() {
race++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// 這里加this只是為了顯式地表明是通過(guò)對(duì)象來(lái)調(diào)用increase方法
this.increase();
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 每次都創(chuàng)建新的SyncStaticTest1實(shí)例
threads[i] = new Thread(new SyncStaticTest1());
threads[i].start();
}
// 等待所有累加線程都結(jié)束
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 期待的結(jié)果應(yīng)該是(THREADS_COUNT * 10000)= 100000
System.out.println("race = " + race + ", time: " + (System.currentTimeMillis() - start));
}
}
運(yùn)行結(jié)果:
race = 100000, time: 25
可見(jiàn)挤忙,就算是10個(gè)線程分別通過(guò)不同的SyncStaticTest1實(shí)例調(diào)用increase方法,仍然是線程安全的夷陋。同樣地,不同線程分別通過(guò)實(shí)例對(duì)象和類對(duì)象調(diào)用同步靜態(tài)方法,也是線程安全的母廷,這里不再做演示。
但是糊肤,同一個(gè)類的 同步靜態(tài)方法
和 同步實(shí)例方法
之間琴昆,則不存在互斥性,因?yàn)樗麄兊耐芥i不同馆揉。如下示例:
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
/**
* 同步安全測(cè)試
*
* 同步靜態(tài)方法和同步實(shí)例方法之間业舍,不存在互斥性
*/
public class SyncStaticTest2 {
private static final int THREADS_COUNT = 2;
public synchronized static void a() {
int i = 5;
while (i-- > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", method: a, running...");
}
}
public synchronized void b() {
int i = 5;
while (i-- > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", method: b, running...");
}
}
public static void main(String[] args) {
final SyncStaticTest2 instance = new SyncStaticTest2();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
final int finalI = i;
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
if (finalI % 2 == 0) {
// 靜態(tài)方法即可以通過(guò)實(shí)例調(diào)用,也可以通過(guò)類調(diào)用
instance.a();
} else {
// 實(shí)例方法則只能通過(guò)實(shí)例調(diào)用
instance.b();
}
}
});
threads[i].start();
}
}
}
運(yùn)行結(jié)果:
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-0, method: a, running...
Thread: Thread-1, method: b, running...
Thread: Thread-0, method: a, running...
Thread: Thread-1, method: b, running...
Thread: Thread-0, method: a, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
Thread: Thread-1, method: b, running...
綜上分析升酣,synchronized靜態(tài)方法
有以下關(guān)鍵點(diǎn)需要記浊诜怼:
- 鎖定類對(duì)象(class)
- 同步靜態(tài)方法在任意實(shí)例對(duì)象之間,也是互斥的
- 同個(gè)類的同步靜態(tài)方法和同步實(shí)例方法之間拗踢,不具備互斥性
synchronized代碼塊
從之前的演示示例中脚牍,我們可以發(fā)現(xiàn),方法同步后巢墅,其耗時(shí)(time)一般都在20ms以上诸狭,而不同步時(shí)券膀,time則只有3ms左右,這印證了synchronized關(guān)鍵字其實(shí)是非常低效的驯遇,不應(yīng)該隨意使用芹彬,如果必須使用,也應(yīng)該考慮盡量減少同步的范圍叉庐,尤其當(dāng)方法體比較大時(shí)舒帮,應(yīng)該盡量避免使用同步方法,此時(shí)可以考慮用同步代碼塊來(lái)代替陡叠。
synchronized(obj) {...}
- 鎖住指定的對(duì)象(可以是任意實(shí)例對(duì)象玩郊,類對(duì)象)
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
/**
* 同步安全測(cè)試
*
* 同步代碼塊,實(shí)現(xiàn)線程安全
*/
public class SyncBlockTest1 implements Runnable {
// 共享資源(臨界資源)
private static int race = 0;
private static final int THREADS_COUNT = 10;
public void increase() {
race++;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// 要注意這里鎖定的對(duì)象是誰(shuí)
synchronized (this) {
increase();
}
}
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
SyncBlockTest1 runnable = new SyncBlockTest1();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 必須使用同一個(gè)實(shí)例枉阵,才能達(dá)到同步效果
threads[i] = new Thread(runnable);
threads[i].start();
}
// 等待所有累加線程都結(jié)束
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 期待的結(jié)果應(yīng)該是(THREADS_COUNT * 10000)= 100000
System.out.println("race = " + race + ", time: " + (System.currentTimeMillis() - start));
}
}
運(yùn)行結(jié)果:
race = 100000, time: 29
上例中译红,我們鎖定了當(dāng)前對(duì)象 this
,如果類的使用情況比較復(fù)雜兴溜,無(wú)法用this做對(duì)象鎖侦厚,也可以自行創(chuàng)建任意對(duì)象充當(dāng)對(duì)象鎖,此時(shí)建議使用長(zhǎng)度為0的byte數(shù)組拙徽,因?yàn)樵谒袑?duì)象中刨沦,它的創(chuàng)建是最經(jīng)濟(jì)的(查看編譯后的字節(jié)碼:byte[] lock = new byte[0]
只需3條操作碼,而Object lock = new Object()
則需要7行操作碼)膘怕。
// 使用一個(gè)長(zhǎng)度為0的byte數(shù)組作為對(duì)象鎖
private byte[] lock = new byte[0];
synchronized (lock) {
increase();
}
使用同步代碼塊時(shí)想诅,同樣必須明確你的對(duì)象鎖是誰(shuí),這樣才能寫(xiě)出正確的使用邏輯淳蔼。以上例來(lái)說(shuō)侧蘸,無(wú)論是 this
還是 lock
,他們都是與當(dāng)前對(duì)象相關(guān)的鹉梨,所以讳癌,為了達(dá)到同步效果,必須如下使用:
SyncBlockTest1 runnable = new SyncBlockTest1();
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 必須使用同一個(gè)實(shí)例存皂,才能達(dá)到同步效果
threads[i] = new Thread(runnable);
threads[i].start();
}
可如果你的使用方法如下晌坤,就失去了線程安全性:
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 每次都創(chuàng)建新的SyncStaticTest1實(shí)例,就會(huì)失去線程安全性
threads[i] = new Thread(new SyncBlockTest1());
threads[i].start();
}
此時(shí)旦袋,運(yùn)行結(jié)果為:
race = 62629, time: 7
但如果你鎖定的是類對(duì)象 SyncStaticTest1.class
骤菠,那10個(gè)線程無(wú)論使用同一個(gè)實(shí)例還是各自使用不同的實(shí)例,都是安全的疤孕。
// 鎖定類對(duì)象
synchronized (SyncStaticTest1.class) {
increase();
}
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
// 每次都創(chuàng)建新的SyncStaticTest1實(shí)例商乎,仍然是線程安全的
threads[i] = new Thread(new SyncBlockTest1());
threads[i].start();
}
運(yùn)行結(jié)果:
race = 100000, time: 25
綜上分析,synchronized代碼塊
有以下關(guān)鍵點(diǎn)需要記准婪А:
- 鎖住指定的對(duì)象(可以是任意實(shí)例對(duì)象鹉戚,類對(duì)象)
- 需要?jiǎng)?chuàng)建對(duì)象鎖時(shí)鲜戒,建議使用
new byte[0]
,因?yàn)樵谒袑?duì)象中抹凳,它的創(chuàng)建是最經(jīng)濟(jì)的 - 必須時(shí)刻明確對(duì)象鎖是誰(shuí)遏餐,只有配合正確的使用方法,才能得到正確的同步效果
至此赢底,synchronized的三種用法就說(shuō)完了失都,可見(jiàn),使用synchronized時(shí)幸冻,明確對(duì)象鎖是非常重要的粹庞。另外,搞清楚了對(duì)象鎖的相關(guān)知識(shí)后嘁扼,就不難推斷出以下2個(gè)等式:
synchronized void method() {
// method logic
}
等價(jià)于:
void method() {
synchronized(this) {
// method logic
}
}
static synchronized void method() {
// method logic
}
等價(jià)于:
static void method() {
synchronized(TestClass.class) {
// method logic
}
}
Lock接口
除了synchronized關(guān)鍵字信粮,JDK1.5中還新增了另外一種線程同步機(jī)制:Lock接口黔攒。來(lái)看看其接口定義:
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()
獲取普通鎖趁啸,若鎖已被獲取,則只能等待督惰,效果與synchronized相同不傅。只不過(guò)lock后需要unlock。
lockInterruptibly()
獲取可中斷鎖赏胚,當(dāng)兩個(gè)線程同時(shí)通過(guò) lockInterruptibly()
想獲取某個(gè)鎖時(shí)访娶,假設(shè)A獲取到了,那么B只能等待觉阅,此時(shí)如果對(duì)B調(diào)用 interrupt()
方法崖疤,就可以中斷B的等待狀態(tài)。但是注意典勇,A是不會(huì)被 interrupt()
中斷的劫哼,也就是說(shuō),只有處于等待狀態(tài)的線程割笙,才可以響應(yīng)中斷权烧。
tryLock()
嘗試獲取鎖,如果獲取成功返回true伤溉,反之立即返回false般码。此方法不會(huì)阻塞等待獲取鎖。
tryLock(long time, TimeUnit unit)
等待time時(shí)間乱顾,如果在time時(shí)間內(nèi)獲取到鎖返回true板祝,如果阻塞等待time時(shí)間內(nèi)沒(méi)有獲取到鎖返回false。
unlock()
業(yè)務(wù)處理完畢走净,釋放鎖券时。
newCondition()
創(chuàng)建一個(gè)Condition囊嘉。Condition與Lock結(jié)合使用,可以達(dá)到synchronized與wait/notify/notifyAll結(jié)合使用時(shí)同樣的線程等待與喚醒的效果革为,而且功能更強(qiáng)大扭粱。
Lock接口與synchronized關(guān)鍵字的區(qū)別
- synchronized加解鎖是自動(dòng)的;而Lock需要手動(dòng)加解鎖震檩,操作復(fù)雜琢蛤,但更加靈活
- lock與unlock需要成對(duì)使用,否則可能造成線程長(zhǎng)期占有鎖抛虏,其他線程長(zhǎng)期等待
- unlock應(yīng)該放在
finally
中博其,以防發(fā)生異常時(shí)未能及時(shí)釋放鎖
- synchronized不可響應(yīng)中斷,一個(gè)線程獲取不到鎖就一直等待迂猴;而Lock可以響應(yīng)中斷
- 當(dāng)兩個(gè)線程同時(shí)通過(guò)
Lock.lockInterruptibly()
想獲取某個(gè)鎖時(shí)慕淡,假設(shè)A獲取到了,那么B只能等待沸毁,此時(shí)如果對(duì)B調(diào)用interrupt()
方法峰髓,就可以中斷B的等待狀態(tài)。但是注意息尺,A是不會(huì)被interrupt()
中斷的携兵,也就是說(shuō),只有處于等待狀態(tài)的線程搂誉,才可以響應(yīng)中斷徐紧。
- 當(dāng)兩個(gè)線程同時(shí)通過(guò)
- synchronized無(wú)法實(shí)現(xiàn)公平鎖;而Lock可以實(shí)現(xiàn)公平鎖
- 公平鎖與非公平鎖的概念稍后再說(shuō)
ReentrantLock可重入鎖
ReentrantLock是Lock的實(shí)現(xiàn)類炭懊。首先并级,看一個(gè)簡(jiǎn)單的售票程序:
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
/**
* 同步安全測(cè)試
*
* 一個(gè)簡(jiǎn)單的售票程序,多線程同時(shí)售票時(shí)侮腹,會(huì)出現(xiàn)線程安全問(wèn)題
*/
public class ReentrantLockTest1 {
private static final int THREADS_COUNT = 3; // 線程數(shù)
private static final int TICKETS_PER_THREAD = 5; // 每個(gè)線程分配到的票數(shù)
// 共享資源(臨界資源)
private int ticket = THREADS_COUNT * TICKETS_PER_THREAD; // 總票數(shù)
public void buyTicket() {
try {
if (ticket > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", bought ticket-" + ticket--);
// 為了更容易出現(xiàn)安全問(wèn)題嘲碧,這里加一個(gè)短暫睡眠
Thread.sleep(2);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
public void readTicket() {
System.out.println("Thread: " + Thread.currentThread().getName() + ", tickets left: " + ticket);
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
final ReentrantLockTest1 instance = new ReentrantLockTest1();
// 啟動(dòng) THREADS_COUNT 個(gè)線程
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
// 每個(gè)線程可以賣 TICKETS_PER_THREAD 張票
for (int j = 0; j < TICKETS_PER_THREAD; j++) {
instance.buyTicket();
}
}
});
threads[i].start();
}
// 等待所有累加線程都結(jié)束
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 讀取剩余票數(shù)
instance.readTicket();
// 耗時(shí)
System.out.println("time: " + (System.currentTimeMillis() - start));
}
}
庫(kù)存有15張票,同時(shí)啟動(dòng)3個(gè)線程出售凯旋,每個(gè)線程分配5張呀潭,線程安全時(shí),結(jié)果應(yīng)該是所有票正好都被賣掉至非,不多不少钠署。然而,在沒(méi)有任何同步措施的情況下荒椭,運(yùn)行結(jié)果如下:
Thread: Thread-0, bought ticket-15
Thread: Thread-2, bought ticket-13
Thread: Thread-1, bought ticket-14
Thread: Thread-1, bought ticket-12
Thread: Thread-2, bought ticket-11
Thread: Thread-0, bought ticket-12
Thread: Thread-2, bought ticket-10
Thread: Thread-1, bought ticket-10
Thread: Thread-0, bought ticket-9
Thread: Thread-2, bought ticket-8
Thread: Thread-1, bought ticket-7
Thread: Thread-0, bought ticket-6
Thread: Thread-0, bought ticket-5
Thread: Thread-2, bought ticket-5
Thread: Thread-1, bought ticket-4
Thread: main, tickets left: 3
time: 14
可見(jiàn)谐鼎,ticket-12、ticket-10趣惠、ticket-5均被售出了2次狸棍,而Ticket-1身害、Ticket-2、Ticket-3沒(méi)有售出草戈。
下面是使用Lock的實(shí)現(xiàn)類 ReentrantLock
對(duì)上例做的改造:
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
import java.util.concurrent.locks.ReentrantLock;
/**
* 同步安全測(cè)試
*
* 演示ReentrantLock實(shí)現(xiàn)同步塌鸯,以及公平鎖與非公平鎖
*/
public class ReentrantLockTest2 {
private static final int THREADS_COUNT = 3; // 線程數(shù)
private static final int TICKETS_PER_THREAD = 5; // 每個(gè)線程分配到的票數(shù)
// 共享資源(臨界資源)
private int ticket = THREADS_COUNT * TICKETS_PER_THREAD; // 總票數(shù)
private static final ReentrantLock lock;
static {
// 創(chuàng)建一個(gè)公平鎖/非公平鎖
lock = new ReentrantLock(false); // 修改參數(shù),看看公平鎖與非公平鎖的差別
}
public void buyTicket() {
try {
lock.lock();
if (ticket > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", bought ticket-" + ticket--);
// 為了演示出公平鎖與非公平鎖的效果唐片,這里加一個(gè)短暫睡眠丙猬,讓其他線程獲得一個(gè)等待時(shí)間
Thread.sleep(2);
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
// unlock應(yīng)該放在finally中,防止發(fā)生異常時(shí)來(lái)不及解鎖
lock.unlock();
}
}
public void readTicket() {
System.out.println("Thread: " + Thread.currentThread().getName() + ", tickets left: " + ticket);
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
final ReentrantLockTest2 instance = new ReentrantLockTest2();
// 啟動(dòng) THREADS_COUNT 個(gè)線程
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
// 每個(gè)線程可以賣 TICKETS_PER_THREAD 張票
for (int j = 0; j < TICKETS_PER_THREAD; j++) {
instance.buyTicket();
}
}
});
threads[i].start();
}
// 等待所有累加線程都結(jié)束
while (Thread.activeCount() > 1) {
Thread.yield();
}
// 讀取剩余票數(shù)
instance.readTicket();
// 耗時(shí)
System.out.println("time: " + (System.currentTimeMillis() - start));
}
}
運(yùn)行結(jié)果:
Thread: Thread-0, bought ticket-15
Thread: Thread-0, bought ticket-14
Thread: Thread-0, bought ticket-13
Thread: Thread-1, bought ticket-12
Thread: Thread-1, bought ticket-11
Thread: Thread-1, bought ticket-10
Thread: Thread-1, bought ticket-9
Thread: Thread-1, bought ticket-8
Thread: Thread-2, bought ticket-7
Thread: Thread-2, bought ticket-6
Thread: Thread-2, bought ticket-5
Thread: Thread-2, bought ticket-4
Thread: Thread-2, bought ticket-3
Thread: Thread-0, bought ticket-2
Thread: Thread-0, bought ticket-1
Thread: main, tickets left: 0
time: 36
可見(jiàn)费韭,從 ticket-15 到 ticket-1 都被按順序售出了茧球,只不過(guò)每張票由哪條線程售出則存在不確定性。上述運(yùn)行結(jié)果是使用 非公平鎖
得到的星持,我們?cè)偻ㄟ^(guò)修改代碼 lock = new ReentrantLock(true)
抢埋,看看公平鎖的運(yùn)行效果:
Thread: Thread-0, bought ticket-15
Thread: Thread-1, bought ticket-14
Thread: Thread-2, bought ticket-13
Thread: Thread-0, bought ticket-12
Thread: Thread-1, bought ticket-11
Thread: Thread-2, bought ticket-10
Thread: Thread-0, bought ticket-9
Thread: Thread-1, bought ticket-8
Thread: Thread-2, bought ticket-7
Thread: Thread-0, bought ticket-6
Thread: Thread-1, bought ticket-5
Thread: Thread-2, bought ticket-4
Thread: Thread-0, bought ticket-3
Thread: Thread-1, bought ticket-2
Thread: Thread-2, bought ticket-1
Thread: main, tickets left: 0
time: 47
我們看到,在公平鎖環(huán)境下督暂,不僅ticket安全性得到保證揪垄,就連線程獲得鎖的順序也得到了保證,以“Thread-0损痰、1福侈、2”的順序循環(huán)執(zhí)行酒来。這里的“公平性”體現(xiàn)在哪里呢卢未?通俗點(diǎn)說(shuō),就是先排隊(duì)等待(也就是等待時(shí)間越長(zhǎng))的線程先得到鎖堰汉,顯然辽社,這種”先到先得“的效果,用隊(duì)列”先進(jìn)先出“的特性實(shí)現(xiàn)最為合適翘鸭。
Java也確實(shí)是通過(guò)”等待隊(duì)列“來(lái)實(shí)現(xiàn)”公平鎖“的滴铅。所有等待鎖的線程都會(huì)被掛起并且進(jìn)入等待隊(duì)列,當(dāng)鎖被釋放后就乓,系統(tǒng)只允許等待隊(duì)列的頭部線程被喚醒并獲得鎖汉匙。而”非公平鎖“其實(shí)同樣有這樣一個(gè)隊(duì)列,只不過(guò)當(dāng)鎖被釋放后生蚁,系統(tǒng)并不會(huì)只從等待隊(duì)列中獲取頭部線程噩翠,而是如果發(fā)現(xiàn)此時(shí)正好有一個(gè)還沒(méi)進(jìn)入等待隊(duì)列的線程想要獲取鎖(此時(shí)該線程還未被掛起)時(shí),則直接將鎖給了它(公平性被打破)邦投,這條線程就可以直接執(zhí)行伤锚,而不用進(jìn)行狀態(tài)切換,于是就省去了切換的開(kāi)銷志衣,這也就是非公平鎖效率高于公平鎖的原因所在屯援。
有了上述理解猛们,我們就可以推斷
- 若在釋放鎖時(shí),總是沒(méi)有新的線程來(lái)打擾狞洋,則每次都必定從等待隊(duì)列中取頭部線程喚醒弯淘,此時(shí)非公平鎖等于公平鎖。
- 對(duì)于非公平鎖來(lái)說(shuō)吉懊,只要線程進(jìn)入了等待隊(duì)列耳胎,隊(duì)列里面仍然是FIFO的原則,跟公平鎖的順序是一樣的惕它。有人認(rèn)為怕午,”非公平鎖環(huán)境下,哪條線程獲得鎖完全是隨機(jī)的“淹魄,這種說(shuō)法明顯是不對(duì)的郁惜,已經(jīng)進(jìn)入等待隊(duì)列中的那些線程就不是隨機(jī)獲得鎖的。
Condition條件
在Lock接口定義中甲锡,還定義了一個(gè) newCondition()
方法兆蕉,用于返回一個(gè)Condition。
Condition與Lock結(jié)合起來(lái)使用缤沦,可以達(dá)到Object監(jiān)視器方法(wait/notify/notifyAll)與synchronized結(jié)合起來(lái)使用時(shí)同樣甚至更加強(qiáng)大的線程等待與喚醒效果虎韵。其中,Lock替代synchronized缸废,Condition替代Object監(jiān)視器方法包蓝。
在Condition中,用await()替換wait()企量,用signal()替換notify()测萎,用signalAll()替換notifyAll()。傳統(tǒng)的線程間通信方式届巩,Condition都能實(shí)現(xiàn)硅瞧,需要注意的是,Condition是綁定在Lock上的恕汇,必須通過(guò)Lock對(duì)象的 newCondition()
方法獲得腕唧。
Condition的強(qiáng)大之處,在于它可以針對(duì)同一個(gè)lock對(duì)象瘾英,創(chuàng)建多個(gè)不同的Condition條件枣接,以處理復(fù)雜的線程等待與喚醒場(chǎng)景。典型的例子就是“生產(chǎn)者-消費(fèi)者”問(wèn)題方咆。生產(chǎn)者與消費(fèi)者共用同一個(gè)固定大小的緩沖區(qū)月腋,當(dāng)緩沖區(qū)滿了,生產(chǎn)者還想向其中添加數(shù)據(jù)時(shí),就必須休眠榆骚,等待消費(fèi)者取走一個(gè)或多個(gè)數(shù)據(jù)后再喚醒片拍。同樣,當(dāng)緩沖區(qū)空了妓肢,消費(fèi)者還想從中取走數(shù)據(jù)時(shí)捌省,也要休眠,等待生產(chǎn)者向其中添加一個(gè)或多個(gè)數(shù)據(jù)后再喚醒碉钠「倩海可見(jiàn),Condition可以指定哪條線程被喚醒喊废,而notify/notifyAll則不行祝高。
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Condition測(cè)試
*
* 生產(chǎn)者-消費(fèi)者問(wèn)題
*/
public class ConditionTest {
private static final int REPOSITORY_SIZE = 3;
private static final int PRODUCT_COUNT = 10;
public static void main(String[] args) {
// 創(chuàng)建一個(gè)容量為REPOSITORY_SIZE的倉(cāng)庫(kù)
final Repository repository = new Repository(REPOSITORY_SIZE);
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < PRODUCT_COUNT; i++) {
try {
repository.put(Integer.valueOf(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}) ;
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < PRODUCT_COUNT; i++) {
try {
Object val = repository.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}) ;
producer.start();
consumer.start();
}
/**
* Repository 是一個(gè)定長(zhǎng)集合,當(dāng)集合為空時(shí)污筷,take方法需要等待工闺,直到有元素時(shí)才返回元素
* 當(dāng)其中的元素?cái)?shù)達(dá)到最大值時(shí),put方法需要等待瓣蛀,直到元素被take之后才能繼續(xù)put
*/
static class Repository {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items;
int putIndex, takeIndex, count;
public Repository(int size) {
items = new Object[size];
}
public void put(Object x) throws InterruptedException {
try {
lock.lock();
while (count == items.length) {
System.out.println("Buffer full, please wait");
// 開(kāi)始等待庫(kù)存不為滿
notFull.await();
}
// 生產(chǎn)一個(gè)產(chǎn)品
items[putIndex] = x;
// 增加當(dāng)前庫(kù)存量
count++;
System.out.println("Produce: " + x);
if (++putIndex == items.length) {
putIndex = 0;
}
// 通知消費(fèi)者線程庫(kù)存已經(jīng)不為空了
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
try {
lock.lock();
while (count == 0) {
System.out.println("No element, please wait");
// 開(kāi)始等待庫(kù)存不為空
notEmpty.await();
}
// 消費(fèi)一個(gè)產(chǎn)品
Object x = items[takeIndex];
// 減少當(dāng)前庫(kù)存量
count--;
System.out.println("Consume: " + x);
if (++takeIndex == items.length) {
takeIndex = 0;
}
// 通知生產(chǎn)者線程庫(kù)存已經(jīng)不為滿了
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
}
運(yùn)行結(jié)果:
Produce: 0
Produce: 1
Produce: 2
Buffer full, please wait
Consume: 0
Consume: 1
Produce: 3
Produce: 4
Buffer full, please wait
Consume: 2
Consume: 3
Consume: 4
No element, please wait
Produce: 5
Produce: 6
Produce: 7
Buffer full, please wait
Consume: 5
Consume: 6
Consume: 7
No element, please wait
Produce: 8
Produce: 9
Consume: 8
Consume: 9
ReadWriteLock讀寫(xiě)鎖
ReadWriteLock也是一個(gè)接口陆蟆,其優(yōu)勢(shì)是允許”讀并發(fā)“,也就是”讀寫(xiě)互斥惋增,寫(xiě)寫(xiě)互斥叠殷,讀讀不互斥“。在多線程讀的場(chǎng)景下诈皿,能極大的提高運(yùn)算效率林束,提升服務(wù)器吞吐量。其接口定義很簡(jiǎn)單:
package java.util.concurrent.locks;
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock可重入讀寫(xiě)鎖
ReentrantReadWriteLock是讀寫(xiě)鎖的實(shí)現(xiàn)類纫塌。我們將售票程序做個(gè)簡(jiǎn)單的改造:
package com.example.weishj.mytester.concurrency.sync.synchronizedtest;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 同步安全測(cè)試
*
* 演示ReentrantReadWriteLock實(shí)現(xiàn)同步诊县,它的特點(diǎn)是"讀并發(fā)"、"寫(xiě)互斥"措左、"讀寫(xiě)互斥"
*/
public class ReentrantReadWriteLockTest1 {
private static final int THREADS_COUNT = 3; // 線程數(shù)
private static final int TICKETS_PER_THREAD = 4; // 每個(gè)線程分配到的票數(shù)
// 共享資源(臨界資源)
private int ticket = THREADS_COUNT * TICKETS_PER_THREAD; // 總票數(shù)
private static final ReadWriteLock lock;
static {
// 為了通過(guò)一個(gè)示例同時(shí)演示"讀并發(fā)"、"寫(xiě)互斥"避除、"讀寫(xiě)互斥"的效果怎披,創(chuàng)建一個(gè)公平鎖
lock = new ReentrantReadWriteLock(false); // 此處也說(shuō)明讀鎖與寫(xiě)鎖之間同樣遵守公平性原則
}
public void buyTicket() {
try {
lock.writeLock().lock();
if (ticket > 0) {
System.out.println("Thread: " + Thread.currentThread().getName() + ", bought ticket-" + ticket--);
Thread.sleep(2);
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
System.out.println("Thread: " + Thread.currentThread().getName() + ", unlocked write");
lock.writeLock().unlock();
}
}
public void readTicket() {
try {
lock.readLock().lock();
System.out.println("Thread: " + Thread.currentThread().getName() + ", tickets left: " + ticket);
Thread.sleep(5);
} catch (Throwable t) {
t.printStackTrace();
} finally {
System.out.println("Thread: " + Thread.currentThread().getName() + ", unlocked read");
lock.readLock().unlock();
}
}
public static void main(String[] args) {
final ReentrantReadWriteLockTest1 instance = new ReentrantReadWriteLockTest1();
// 啟動(dòng) THREADS_COUNT 個(gè)線程
Thread[] writeThreads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
writeThreads[i] = new Thread(new Runnable() {
@Override
public void run() {
// 每個(gè)線程可以賣 TICKETS_PER_THREAD 張票
for (int j = 0; j < TICKETS_PER_THREAD; j++) {
instance.buyTicket();
}
}
});
writeThreads[i].start();
}
// 讀取此時(shí)的剩余票數(shù)
Thread[] readThreads = new Thread[2];
for (int i = 0; i < 2; i++) {
readThreads[i] = new Thread(new Runnable() {
@Override
public void run() {
// 每個(gè)線程可以讀 2 次剩余票數(shù)
for (int j = 0; j < 2; j++) {
instance.readTicket();
}
}
});
readThreads[i].start();
}
}
}
運(yùn)行結(jié)果:
Thread: Thread-0, bought ticket-12
Thread: Thread-0, unlocked write
Thread: Thread-0, bought ticket-11
Thread: Thread-0, unlocked write
Thread: Thread-0, bought ticket-10
Thread: Thread-0, unlocked write
Thread: Thread-0, bought ticket-9
Thread: Thread-0, unlocked write
Thread: Thread-1, bought ticket-8
Thread: Thread-1, unlocked write
Thread: Thread-1, bought ticket-7
Thread: Thread-1, unlocked write
Thread: Thread-1, bought ticket-6
Thread: Thread-1, unlocked write
Thread: Thread-1, bought ticket-5
Thread: Thread-1, unlocked write
Thread: Thread-2, bought ticket-4
Thread: Thread-2, unlocked write
Thread: Thread-2, bought ticket-3
Thread: Thread-2, unlocked write
Thread: Thread-2, bought ticket-2
Thread: Thread-2, unlocked write
Thread: Thread-2, bought ticket-1
Thread: Thread-2, unlocked write
Thread: Thread-3, tickets left: 0
Thread: Thread-4, tickets left: 0
Thread: Thread-3, unlocked read
Thread: Thread-3, tickets left: 0
Thread: Thread-4, unlocked read
Thread: Thread-4, tickets left: 0
Thread: Thread-3, unlocked read
Thread: Thread-4, unlocked read
上述結(jié)果是在”非公平鎖“的環(huán)境下得到的,無(wú)論嘗試運(yùn)行多少次瓶摆,2條讀線程都是被放在3條寫(xiě)線程執(zhí)行完畢后才開(kāi)始執(zhí)行凉逛,為了一次性驗(yàn)證所有結(jié)論,我們?cè)贀Q”公平鎖“重新執(zhí)行一次群井,結(jié)果如下:
Thread: Thread-0, bought ticket-12
Thread: Thread-0, unlocked write
Thread: Thread-1, bought ticket-11
Thread: Thread-1, unlocked write
Thread: Thread-2, bought ticket-10
Thread: Thread-2, unlocked write
Thread: Thread-3, tickets left: 9
Thread: Thread-4, tickets left: 9
Thread: Thread-4, unlocked read
Thread: Thread-3, unlocked read
Thread: Thread-0, bought ticket-9
Thread: Thread-0, unlocked write
Thread: Thread-1, bought ticket-8
Thread: Thread-1, unlocked write
Thread: Thread-2, bought ticket-7
Thread: Thread-2, unlocked write
Thread: Thread-4, tickets left: 6
Thread: Thread-3, tickets left: 6
Thread: Thread-3, unlocked read
Thread: Thread-4, unlocked read
Thread: Thread-0, bought ticket-6
Thread: Thread-0, unlocked write
Thread: Thread-1, bought ticket-5
Thread: Thread-1, unlocked write
Thread: Thread-2, bought ticket-4
Thread: Thread-2, unlocked write
Thread: Thread-0, bought ticket-3
Thread: Thread-0, unlocked write
Thread: Thread-1, bought ticket-2
Thread: Thread-1, unlocked write
Thread: Thread-2, bought ticket-1
Thread: Thread-2, unlocked write
這次讀線程就被穿插到寫(xiě)線程中間了状飞,從上述結(jié)果中可以看到:
- 當(dāng)任意線程寫(xiě)的時(shí)候,其他線程既不能讀也不能寫(xiě)
- Thread-3讀的時(shí)候,Thread-4同樣可以讀诬辈,但是不能有任何寫(xiě)線程
- 3條寫(xiě)線程永遠(yuǎn)按照”0-1-2“的順序執(zhí)行酵使,他們遵守”公平性“原則
- 2條讀線程之間非互斥,所以也談不上什么”公平性”原則
- 3條寫(xiě)線程”Thread-0焙糟、1口渔、2“各獲得過(guò)一次鎖之后,必定輪到2條讀線程”Thread-3穿撮、4“獲得鎖缺脉,而不是如”非公平鎖“的結(jié)果那樣,讀線程總是等到寫(xiě)線程全部執(zhí)行結(jié)束后才開(kāi)始執(zhí)行悦穿,也就是說(shuō)讀線程與寫(xiě)線程之間遵守同一個(gè)”公平性“原則
使用場(chǎng)景分析
synchronized
- 不需要“中斷”與“公平鎖”的業(yè)務(wù)場(chǎng)景
- 較為簡(jiǎn)單的“等待與喚醒”業(yè)務(wù)(與Object監(jiān)視器方法結(jié)合使用)
ReentrantLock可重入鎖
- 需要“響應(yīng)中斷”的業(yè)務(wù)場(chǎng)景:處于等待狀態(tài)的線程可以中斷
- 需要“公平鎖”的業(yè)務(wù)場(chǎng)景:線程有序獲得鎖攻礼,亦即“有序執(zhí)行”
- 與Condition結(jié)合,可以滿足更為復(fù)雜的“等待與喚醒”業(yè)務(wù)(可以指定哪個(gè)線程被喚醒)
ReentrantReadWriteLock可重入讀寫(xiě)鎖
- 允許“讀讀并發(fā)”的業(yè)務(wù)場(chǎng)景栗柒,可以大幅提高吞吐量
總結(jié)
synchronized實(shí)例方法
- 鎖定實(shí)例對(duì)象(this)
- 每個(gè)實(shí)例都有獨(dú)立的對(duì)象鎖秘蛔,因此只有針對(duì)同一個(gè)實(shí)例,才具備互斥性
- 同一個(gè)實(shí)例中的多個(gè)synchronized實(shí)例方法之間傍衡,也是互斥的
synchronized靜態(tài)方法
- 鎖定類對(duì)象(class)
- 同步靜態(tài)方法在任意實(shí)例對(duì)象之間深员,也是互斥的
- 同個(gè)類的同步靜態(tài)方法和同步實(shí)例方法之間,不具備互斥性
synchronized代碼塊
- 鎖住指定的對(duì)象(可以是任意實(shí)例對(duì)象蛙埂,類對(duì)象)
- 需要?jiǎng)?chuàng)建對(duì)象鎖時(shí)倦畅,建議使用
new byte[0]
,因?yàn)樵谒袑?duì)象中绣的,它的創(chuàng)建是最經(jīng)濟(jì)的 - 必須時(shí)刻明確對(duì)象鎖是誰(shuí)叠赐,只有配合正確的使用方法,才能得到正確的同步效果
ReentrantLock可重入鎖
- ReentrantLock是Lock接口的一種實(shí)現(xiàn)
- 需要手動(dòng)加解鎖屡江,操作復(fù)雜芭概,但更加靈活
- lock與unlock需要成對(duì)使用,且unlock應(yīng)該放在
finally
中 - 可以響應(yīng)中斷
- 可以實(shí)現(xiàn)“公平鎖”:先排隊(duì)等待(也就是等待時(shí)間越長(zhǎng))的線程先得到鎖
- 非公平鎖環(huán)境下惩嘉,哪條線程獲得鎖并非是完全隨機(jī)的罢洲,已經(jīng)進(jìn)入等待隊(duì)列中的那些線程就仍然是根據(jù)FIFO原則獲得鎖的
- 非公平鎖效率高于公平鎖
- ReentrantLock與Condition結(jié)合使用,類似synchronized與Object監(jiān)視器方法結(jié)合使用
- 在Condition中文黎,用await()替換wait()惹苗,用signal()替換notify(),用signalAll()替換notifyAll()
- Condition的強(qiáng)大之處耸峭,在于它可以針對(duì)同一個(gè)lock對(duì)象桩蓉,創(chuàng)建多個(gè)不同的Condition條件,以處理復(fù)雜的線程等待與喚醒場(chǎng)景
- Condition可以指定哪條線程被喚醒劳闹,而notify/notifyAll則不行
ReentrantReadWriteLock可重入讀寫(xiě)鎖
- ReentrantReadWriteLock是ReadWriteLock接口(讀寫(xiě)鎖)的一個(gè)實(shí)現(xiàn)類院究,而ReadWriteLock內(nèi)部則是由Lock實(shí)現(xiàn)的
- ReentrantReadWriteLock具有ReentrantLock的一切特性洽瞬,同時(shí)還具有自己的獨(dú)立特性:"讀讀并發(fā)"、"寫(xiě)寫(xiě)互斥"业汰、"讀寫(xiě)互斥"
- ReentrantReadWriteLock可以有效提高并發(fā)伙窃,增加吞吐量
- 在“公平鎖”環(huán)境下,讀線程之間沒(méi)有”公平性“可言蔬胯,而寫(xiě)線程之間对供,以及讀線程與寫(xiě)線程之間,則遵守同一個(gè)“公平性”原則