線程間的共享和協(xié)作
線程間的共享
JVM 會(huì)為每一個(gè)線程獨(dú)立
分配虛擬機(jī)椖淠耍空間
桩皿,本地方法棧空間
以及程序計(jì)數(shù)器
幢炸,而對(duì)于共享內(nèi)存
中的變量
泄隔,是對(duì)每一個(gè)線程而言是共享
的,因此多線程并發(fā)
訪問(wèn)共享內(nèi)存
中的變量
時(shí)就會(huì)出現(xiàn)線程安全
問(wèn)題宛徊。具體可以參考JVM 內(nèi)存模型這篇博客佛嬉。
synchronized 內(nèi)置鎖
在前面提到共享資源在多個(gè)線程并發(fā)訪問(wèn)時(shí)會(huì)出現(xiàn)線程安全問(wèn)題
,而解決線程安全問(wèn)題就是要解決以下兩個(gè)問(wèn)題闸天,一是要保證共享資源的在多個(gè)線程之間是互斥訪問(wèn)
的暖呕,二是要保證共享資源在多個(gè)線程之間的數(shù)據(jù)同步
的。
我們用一張圖來(lái)描述 synchronized 保證線程安全的本質(zhì)原因:
從上圖中苞氮,我們可以看出:
原子性:
互斥訪問(wèn)
保證了共享變量同一時(shí)刻
只有一個(gè)線程能夠訪問(wèn)湾揽,體現(xiàn)了操作共享資源的原子性。
可見(jiàn)性:
數(shù)據(jù)同步
在線程獲取鎖時(shí)從主存中讀取共享變量的值到線程工作內(nèi)存笼吟,在釋放鎖之前將工作內(nèi)存的共享變量值刷新到主存中库物,這就體現(xiàn)了共享變量在多線程之間的可見(jiàn)性
。
對(duì)象鎖
synchronized 作用于對(duì)象實(shí)例方法上贷帮,對(duì)象鎖是當(dāng)前 this 對(duì)象艳狐。
public class SyncTest {
private int count;
//作用于實(shí)例方法上,對(duì)象鎖是當(dāng)前 this 對(duì)象
public synchronized void increase() {
count++;
}
}
synchronized 作用于對(duì)象實(shí)例方法內(nèi)部的同步代碼塊上皿桑,對(duì)象鎖是當(dāng)前 this 對(duì)象/或者 monitor毫目。
public class SyncTest {
private int count;
private Object monitor = new Object();
public void increase() {
// 對(duì)象鎖是當(dāng)前對(duì)象 this
synchronized (this) {
count++;
}
//對(duì)象鎖是 monitor
//synchronized (monitor) {
//count++;
//}
}
}
類(lèi)鎖
其實(shí)類(lèi)鎖也是一個(gè)對(duì)象鎖蔬啡,為什么這樣說(shuō)呢?因?yàn)轭?lèi)鎖使用的是一個(gè)類(lèi)的 Class 對(duì)象作為鎖镀虐, Class 是用來(lái)描述所有的類(lèi)箱蟆,因此使用 Class 對(duì)象也是一種對(duì)象鎖,只是一般情況將其稱(chēng)為類(lèi)鎖而已刮便。
//類(lèi)鎖:使用在類(lèi)靜態(tài)方法上
public synchronized static void change() {
//do sth
}
//類(lèi)鎖:SyncTest.class對(duì)象作為對(duì)象實(shí)例方法代碼塊鎖
public static void change2() {
synchronized (SyncTest.class) {
//do sth
}
}
synchronized 注意點(diǎn)
synchronized 能夠保證
線程安全
的前提是操作共享資源
的多個(gè)線程
必須持有的是同一把鎖
空猜。
線程的協(xié)作
等待/通知機(jī)制
是指一個(gè)線程A
調(diào)用了對(duì)象O
的wait()
方法進(jìn)入等待狀態(tài)
,而另一個(gè)線程B
調(diào)用了對(duì)象O
的notify()
或者notifyAll()
方法恨旱,線程A
收到通知
后從對(duì)象O的wait()
方法返回辈毯,進(jìn)而執(zhí)行后續(xù)操作。上述兩個(gè)線程通過(guò)對(duì)象O來(lái)完成交互搜贤。
JDK 提供實(shí)現(xiàn)等待/通知的 API
注意:以下方法不是 Thread 提供的谆沃,而是 Object 的。
- wait()
如果正在執(zhí)行的線程內(nèi)部調(diào)用執(zhí)行了該方法仪芒,那么線程將進(jìn)入
WAITING
狀態(tài)(線程狀態(tài)可以參考(ps:劣實(shí)基礎(chǔ)–Java 并發(fā)編程基礎(chǔ)知識(shí))唁影,等待其他線程通知或者線程被中斷才會(huì)返回,注意: wait() 會(huì)釋放當(dāng)前對(duì)象鎖和釋放 CPU 執(zhí)行權(quán)掂名,具體可以看下面介紹的鎖池
和等待池
- wait(long)
超時(shí)等待一段時(shí)間据沈,這里的參數(shù)時(shí)間是毫秒,也就是等待長(zhǎng)達(dá)n毫秒,如果沒(méi)有通知就超時(shí)返回饺蔑。指定時(shí)間到了不會(huì)拋出異常锌介,而是繼續(xù)往下執(zhí)行。除非在 wait 期間發(fā)生了中斷猾警,那么 wait 將出異常孔祸。
- wait (long,int)
對(duì)于超時(shí)時(shí)間更細(xì)粒度的控制,可以達(dá)到納秒
- notify()
通知一個(gè)在對(duì)象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到了對(duì)象的鎖,沒(méi)有獲得鎖的線程重新進(jìn)入WAITING狀態(tài)肿嘲。
- notifyAll()
通知所有等待在該對(duì)象上的線程
等待和通知的標(biāo)準(zhǔn)范式
等待方遵循如下原則:
- 獲取對(duì)象鎖。
- 如果條件不符合筑公,那么調(diào)用該對(duì)象的 wait()方法雳窟,被其他 notify() 之后仍要檢查條件。
- 條件滿足則執(zhí)行對(duì)應(yīng)的邏輯匣屡。
偽代碼如下:
synchronized(鎖對(duì)象){
while(條件不滿足){
鎖對(duì)象.wait();
}
//滿足條件處理對(duì)應(yīng)的邏輯
}
通知方遵循如下原則:
- 獲取對(duì)象鎖封救。
- 改變條件。
- 通知正在等待對(duì)象鎖的線程捣作。
偽代碼如下:
synchronized(鎖對(duì)象){
改變條件
鎖對(duì)象.notifyAll();
}
一個(gè)對(duì)象擁有兩個(gè)池:
- 鎖池
假設(shè) A 線程持有對(duì)象 Object 的鎖,此時(shí)其他線程想要執(zhí)行該對(duì)象的某一個(gè)同步方法或者同步塊券躁,這些線程就會(huì)進(jìn)入該對(duì)象的鎖池中惩坑。
- 等待池
假設(shè) A 線程正在同步方法或者同步塊中執(zhí)行中調(diào)用了object.wait() 掉盅,那么線程 A 就會(huì)進(jìn)入對(duì)象 object 的等待池中,等待其他線程調(diào)用該對(duì)象的 notify() 或者 notifyAll() 方法以舒。如果其他線程調(diào)用的 object.notity() 方法趾痘,那么 CPU 會(huì)從等待池中隨機(jī)取出一個(gè)線程放入鎖池中,如果其他線程調(diào)用 object.notifyAll() 那么 CPU 會(huì)將等待池中所有的線程到放入到鎖池中蔓钟,準(zhǔn)備爭(zhēng)奪鎖的持有權(quán)永票。
看了上面的等待池和鎖池的作用后,這里有一個(gè)疑問(wèn):notify 和 notifyAll 應(yīng)該用誰(shuí)滥沫?
如果多個(gè)線程都調(diào)用了 對(duì)象鎖.wait() 方法侣集,那么如果只是調(diào)用 對(duì)象鎖.notify() 方法,那么不一定會(huì)喚醒你想要的那個(gè)線程兰绣,CPU 只是隨機(jī)地都等待池種去取出一個(gè)線程放入鎖池中世分,所以說(shuō)最好是使用
notifyAll()
;
下面舉一個(gè)老王和老張買(mǎi)小米9手機(jī)的栗子:
等待/通知 范式的應(yīng)用
public class XimaoShop implements Runnable {
//鎖
private Object lock = new Object();
private int xiaomi9Discount = 10;
/*
通知方:折扣改變的通知方法
*/
public void depreciateXiaomi9(int discount) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "收到總部通知,現(xiàn)在進(jìn)行小米9打" + discount + "折活動(dòng)狭魂,通知米粉們來(lái)買(mǎi)吧");
xiaomi9Discount = discount;
//通知客戶:小米9打折了哦罚攀,趕緊去看看價(jià)格吧。
//notify() 隨機(jī)通知一個(gè)等待線程
// lock.notify();
//notifyAll() 通知所有等待的線程
lock.notifyAll();
}
}
/*
等待方:查詢小米9價(jià)格
*/
public void getXiaomi9Price() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "正在查詢小米9價(jià)格");
//小米9的折扣還沒(méi)低于8折雌澄,不要給我推銷(xiāo)
while (xiaomi9Discount > 8) {
try {
System.out.println(Thread.currentThread().getName() + "發(fā)現(xiàn)小米9價(jià)格折扣為" + xiaomi9Discount + "太少斋泄,我要開(kāi)始等待降價(jià),老板镐牺,降價(jià)了炫掐,就通知我哦,開(kāi)始等待...");
//等待:等待小米9降價(jià)
lock.wait();
System.out.println(Thread.currentThread().getName() + "收到通知:小米9搞活動(dòng)睬涧,打折了哦募胃,目前折扣為:" + xiaomi9Discount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "剁手買(mǎi)頂配小米9:" + xiaomi9Discount + "折購(gòu)入");
}
}
@Override
public void run() {
getXiaomi9Price();
}
public static void main(String[] args) throws InterruptedException {
XimaoShop shop = new XimaoShop();
//老王想要買(mǎi)手機(jī)
Thread getXiaomiPriceThread = new Thread(shop);
//老張也要買(mǎi)手機(jī)
Thread getXiaomiPriceThread2 = new Thread(shop);
getXiaomiPriceThread.start();
getXiaomiPriceThread2.start();
Thread.sleep(1000);
//降價(jià)了
shop.depreciateXiaomi9(9);
Thread.sleep(1000);
//又降價(jià)了
shop.depreciateXiaomi9(8);
}
}
- lock.notify()
根據(jù)輸出結(jié)果可以看出,當(dāng)降價(jià)到滿足條件時(shí)畦浓,只有 Thread-1 收到通知痹束。
- lock.notifyAll() 的輸出結(jié)果
根據(jù)輸出結(jié)果可以看出,當(dāng)降價(jià)到滿足條件時(shí)讶请,只有 Thread-1 和 Thread-2 都收到通知祷嘶。
線程隔離ThreadLocal
ThreadLocal 即線程變量,是一個(gè)以ThreadLocal
對(duì)象為鍵夺溢、任意對(duì)象
為值的存儲(chǔ)結(jié)構(gòu)论巍。這個(gè)結(jié)構(gòu) ThreadLocal.ThreadLocalMap
被附帶在線程
上,也就是說(shuō)一個(gè)線程
可以根據(jù)一個(gè)ThreadLocal
對(duì)象查詢到綁定
在這個(gè)線程
上的一個(gè)值
, ThreadLocal
往往用來(lái)實(shí)現(xiàn)變量
在線程之間
的隔離
风响。
- 定義一個(gè) ThreadLocal嘉汰,存儲(chǔ)的是 String 類(lèi)型,默認(rèn)存儲(chǔ) subject 的值為"我是默認(rèn)值"状勤。
public class ThreadLocalTools {
public static String subject = "我是默認(rèn)值";
public static ThreadLocal<String> sThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return subject;
}
};
}
- 使用 ThreadLocal
package com.example.threadlocal;
public class ThreadLocalDemo {
public static void main(String[] args) {
Thread thread1 = new Thread("線程1") {
@Override
public void run() {
super.run();
ThreadLocalTools.sThreadLocal.set("Flutter");
String result = ThreadLocalTools.sThreadLocal.get();
System.out.println(Thread.currentThread().getName() + "-" + result);
//線程執(zhí)行完鞋怀,要清除
ThreadLocalTools.sThreadLocal.remove();
}
};
Thread thread2 = new Thread("線程2") {
@Override
public void run() {
super.run();
ThreadLocalTools.sThreadLocal.set("Android");
String result = ThreadLocalTools.sThreadLocal.get();
System.out.println(Thread.currentThread().getName() + "-" + result);
//線程執(zhí)行完双泪,要清除
ThreadLocalTools.sThreadLocal.remove();
}
};
thread1.start();
thread2.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String result = ThreadLocalTools.sThreadLocal.get();
System.out.println(Thread.currentThread().getName() + "-" + result);
//線程執(zhí)行完,要清除
ThreadLocalTools.sThreadLocal.remove();
}
}
運(yùn)行結(jié)果:
線程2-Android
線程1-Flutter
main-我是默認(rèn)值
我是默認(rèn)值
從上面的運(yùn)行結(jié)果可以看出接箫,不同線程都擁有一個(gè)獨(dú)有的 subject 的副本變量攒读,不同線程對(duì)這個(gè)副本的修改都是針對(duì)當(dāng)前線程的,對(duì)其他線程的 subject 副本變量不會(huì)造成影響辛友。
記錄于2019年4月12日