摘自《Java并發(fā)編程的藝術(shù)》
1 volatile和synchronized關(guān)鍵字
關(guān)鍵字volatile
可以用來修飾字段(成員變量)腻菇,就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取,而對它的改變必須同步刷新回共享內(nèi)存惦积,它能保證所有線程對變量訪問的可見性
接校。
關(guān)鍵字synchronized
可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻狮崩,只能有一個線程處于方法或者同步塊中蛛勉,它保證了線程對變量訪問的可見性和排他性
。
通過使用javap工具查看生成的class文件信息來分析synchronized關(guān)鍵字的實現(xiàn)細節(jié)厉亏,代碼如下
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){
m();
}
}
public static synchronized void m(){
}
}
執(zhí)行javap -v Synchronized.class董习,部分相關(guān)輸出如下所示:
對于同步塊的實現(xiàn)使用了 monitorenter
和 monitorexit
指令,而同步方法則是依賴方法修飾符上的ACC_SYNCHRONIZED
來完成爱只。無論采用哪種方式皿淋,其本質(zhì)是對一個對象的監(jiān)視器進行獲取招刹,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器窝趣。
任意一個對象都擁有自己的監(jiān)視器疯暑,當這個對象由同步塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進入同步塊或者同步方法哑舒,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會被阻塞在同步塊和同步方法的入口處妇拯,進入BLOCKED狀態(tài)。
下圖描述了對象洗鸵、對象的監(jiān)視器越锈、同步隊列和執(zhí)行線程之間的關(guān)系:
從圖中可以看到,任意線程對Object(Object由synchronized保護)的訪問膘滨,首先要獲得Object的監(jiān)視器甘凭。如果獲取失敗,線程進入同步隊列火邓,線程狀態(tài)變?yōu)锽LOCKED丹弱。當訪問Object的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程铲咨,使其重新嘗試對監(jiān)視器的獲取躲胳。
2 等待 / 通知機制
等待/通知機制是指一個線程A調(diào)用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調(diào)用了對象O的notify()或notifyAll()方法纤勒,線程A收到通知后從對象O的wait()方法返回坯苹,進而執(zhí)行后續(xù)操作
。上述兩個線程對象O來完成交互踊东,而對象上的wait()和notify/notifyAll()的關(guān)系就如同開關(guān)信號一樣北滥,用來完成等待方通知方之間的交互工作。
下面所示的例子中闸翅,創(chuàng)建了兩個線程——WaitThread和NotifyThread,前者檢查flag值是否為false菊霜,如果符合要求坚冀,進行后續(xù)操作,否則在lock上等待鉴逞,后者在睡眠了一段時間后對lock進行通知记某,示例如下所示。
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread waitThread = new Thread(new Wait(), "WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable {
public void run() {
// 加鎖构捡,擁有l(wèi)ock的Monitor
synchronized (lock) {
// 當條件不滿足時液南,繼續(xù)wait,同時釋放了lock的鎖
while (flag) {
try {
System.out.println(Thread.currentThread()
+ " flag is true. wait@ "
+ new SimpleDateFormat("HH:mm:ss")
.format(new Date()));
lock.wait();
} catch (InterruptedException e) {
}
}
// 條件滿足時勾徽,完成工作
System.out.println(Thread.currentThread()
+ " flag is false. running@ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable {
public void run() {
// 加鎖滑凉,擁有l(wèi)ock的Monitor
synchronized (lock) {
// 獲取lock的鎖,然后進行通知,通知時不會釋放lock的鎖畅姊,
// 直到當前線程釋放了lock后咒钟,WaitThread才能從wait方法中返回
System.out.println(Thread.currentThread()
+ " hold lock. notify @ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notifyAll();
flag = false;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 再次加鎖
synchronized (lock) {
System.out.println(Thread.currentThread()
+ " hold lock again. sleep@ "
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
輸出如下(輸出內(nèi)容可能不同,主要區(qū)別在時間上)
Thread[WaitThread,5,main] flag is true. wait@ 16:15:44
Thread[NotifyThread,5,main] hold lock. notify @ 16:15:45
Thread[NotifyThread,5,main] hold lock again. sleep@ 16:15:50
Thread[WaitThread,5,main] flag is false. running@ 16:15:55
上述第3行和第4行輸出的順序可能會互換若未,而上述例子主要說明了調(diào)用wait()朱嘴、notify()以及notifyAll()時需要注意的細節(jié),如下粗合。
- 使用wait()萍嬉、notify()和notifyAll()時
需要先對調(diào)用對象加鎖
。 - 調(diào)用wait()方法后隙疚,線程狀態(tài)由RUNNING變?yōu)閃AITING壤追,并將當前線程放置到對象的等待隊列。
- notify()或notifyAll()方法調(diào)用后甚淡,等待線程依舊不會從wait()返回大诸,
需要調(diào)用notify()或notifAll()的線程釋放鎖之后,等待線程才有機會從wait()返回
贯卦。 -
notify()
方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中
资柔,而notifyAll()
方法則是將等待隊列中所有的線程
全部移到同步隊列,被移動的線程狀態(tài)由WAITING變?yōu)锽LOCKED撵割。 - 從wait()方法
返回的前提是獲得了調(diào)用對象的鎖
贿堰。
從上述細節(jié)中可以看到,等待/通知機制依托于同步機制啡彬,其目的就是確保等待線程從wait()方法返回時能夠感知到通知線程對變量做出的修改羹与。下圖描述了上述示例的過程。
在圖中庶灿,WaitThread首先獲取了對象的鎖纵搁,然后調(diào)用對象的wait()方法,從而放棄了鎖并進入了對象的等待隊列WaitQueue中往踢,進入等待狀態(tài)腾誉。由于WaitThread釋放了對象的鎖,NotifyThread隨后獲取了對象的鎖峻呕,并調(diào)用對象的notify()方法利职,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態(tài)變?yōu)樽枞麪顟B(tài)瘦癌。NotifyThread釋放了鎖之后猪贪,WaitThread再次獲取到鎖并從wait()方法返回繼續(xù)執(zhí)行。
2.1 生產(chǎn)者/消費者模式
Consumer.java
public class Consumer extends Thread {
// 每次消費的產(chǎn)品數(shù)量
private int num;
// 所在放置的倉庫
private Storage storage;
// 構(gòu)造函數(shù)讯私,設(shè)置倉庫
public Consumer(Storage storage) {
this.storage = storage;
}
// 線程run函數(shù)
public void run() {
consume(num);
}
// 調(diào)用倉庫Storage的生產(chǎn)函數(shù)
public void consume(int num) {
storage.consume(num);
}
// get/set方法
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
}
Producer.java
public class Producer extends Thread {
// 每次生產(chǎn)的產(chǎn)品數(shù)量
private int num;
// 所在放置的倉庫
private Storage storage;
// 構(gòu)造函數(shù)热押,設(shè)置倉庫
public Producer(Storage storage) {
this.storage = storage;
}
// 線程run函數(shù)
public void run() {
produce(num);
}
// 調(diào)用倉庫Storage的生產(chǎn)函數(shù)
public void produce(int num) {
storage.produce(num);
}
// get/set方法
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public Storage getStorage() {
return storage;
}
public void setStorage(Storage storage) {
this.storage = storage;
}
}
Storage.java
public class Storage {
// 倉庫最大存儲量
private final int MAX_SIZE = 100;
// 倉庫存儲的載體
private LinkedList<Object> list = new LinkedList<Object>();
// 生產(chǎn)num個產(chǎn)品
public void produce(int num) {
// 同步代碼段
synchronized (list) {
// 如果倉庫剩余容量不足
while (list.size() + num > MAX_SIZE) {
System.out.println("【要生產(chǎn)的產(chǎn)品數(shù)量】:" + num + "\t【庫可以存放存量】:"
+ list.size() + "\t暫時不能執(zhí)行生產(chǎn)任務(wù)!");
try {
// 由于條件不滿足西傀,生產(chǎn)阻塞
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生產(chǎn)條件滿足情況下,生產(chǎn)num個產(chǎn)品
for (int i = 1; i <= num; ++i) {
list.add(new Object());
}
System.out.println("【已經(jīng)生產(chǎn)產(chǎn)品數(shù)】:" + num + "\t【現(xiàn)倉儲量為】:" + list.size());
// 通知消費者來消費
list.notifyAll();
}
}
// 消費num個產(chǎn)品
public void consume(int num) {
// 同步代碼段
synchronized (list) {
// 如果倉庫存儲量不足
while (list.size() < num) {
System.out.println("【要消費的產(chǎn)品數(shù)量】:" + num + "\t【庫存量】:"
+ list.size() + "\t暫時不能執(zhí)行生產(chǎn)任務(wù)!");
try {
// 由于條件不滿足楞黄,消費阻塞
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消費條件滿足情況下池凄,消費num個產(chǎn)品
for (int i = 1; i <= num; ++i) {
list.remove();
}
System.out.println("【已經(jīng)消費產(chǎn)品數(shù)】:" + num + "\t【現(xiàn)倉儲量為】:" + list.size());
// 通知生產(chǎn)者生產(chǎn)
list.notifyAll();
}
}
// get/set方法
public LinkedList<Object> getList() {
return list;
}
public void setList(LinkedList<Object> list) {
this.list = list;
}
public int getMAX_SIZE() {
return MAX_SIZE;
}
}
Test.java
public class Test {
public static void main(String[] args) {
// 倉庫對象
Storage storage = new Storage();
// 生產(chǎn)者對象
Producer p1 = new Producer(storage);
Producer p2 = new Producer(storage);
Producer p3 = new Producer(storage);
Producer p4 = new Producer(storage);
Producer p5 = new Producer(storage);
Producer p6 = new Producer(storage);
Producer p7 = new Producer(storage);
// 消費者對象
Consumer c1 = new Consumer(storage);
Consumer c2 = new Consumer(storage);
Consumer c3 = new Consumer(storage);
// 設(shè)置生產(chǎn)者產(chǎn)品生產(chǎn)數(shù)量
p1.setNum(10);
p2.setNum(10);
p3.setNum(10);
p4.setNum(10);
p5.setNum(10);
p6.setNum(10);
p7.setNum(80);
// 設(shè)置消費者產(chǎn)品消費數(shù)量
c1.setNum(50);
c2.setNum(20);
c3.setNum(30);
// 線程開始執(zhí)行
c1.start();
c2.start();
c3.start();
p1.start();
p2.start();
p3.start();
p4.start();
p5.start();
p6.start();
p7.start();
}
}
3 Thread.join()的使用
如果一個線程A執(zhí)行了thread.join()語句,其含義是:當前線程A等待thread線程終止之后才 從thread.join()返回鬼廓。線程Thread除了提供join()方法之外肿仑,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示碎税,如果線程thread在給定的超時時間里沒有終止尤慰,那么將會從該超時方法中返回。
4 ThreadLocal的使用
ThreadLocal雷蹂,即線程變量伟端,是一個以ThreadLocal對象為鍵、任意對象為值的存儲結(jié)構(gòu)匪煌。這 個結(jié)構(gòu)被附帶在線程上责蝠,也就是說一個線程可以根據(jù)一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
可以通過set(T)方法來設(shè)置一個值萎庭,在當前線程下再通過get()方法獲取到原先設(shè)置的值霜医。
在代碼清單4-15所示的例子中,構(gòu)建了一個常用的Profiler類驳规,它具有begin()和end()兩個方法肴敛,而end()方法返回從begin()方法調(diào)用開始到end()方法被調(diào)用時的時間差,單位是毫秒吗购。
public class Profiler {
// 第一次get()方法調(diào)用時會進行初始化(如果set方法沒有調(diào)用)医男,每個線程會調(diào)用一次
private static final ThreadLocal<Long> TIME_THREADLOCAL =
new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return System.currentTimeMillis();
}
};
public static void main(String[] args) throws InterruptedException {
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("Time cost is: " + Profiler.end() + " mills");
}
public static final void begin() {
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end() {
// 時間消耗
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}
}
輸出結(jié)果如下所示
Cost: 1001 mills