synchronized 關(guān)鍵字
1. 前言
本節(jié)內(nèi)容主要是對(duì) synchronized 關(guān)鍵字的使用進(jìn)行講解,具體內(nèi)容點(diǎn)如下:
- 了解 synchronized 關(guān)鍵字的概念,從總體層面對(duì) synchronized 關(guān)鍵字進(jìn)行了解,是我們本節(jié)課程的基礎(chǔ)知識(shí);
- 了解 synchronized 關(guān)鍵字的作用,知道 synchronized 關(guān)鍵字使用的意義,使我們學(xué)習(xí)本節(jié)內(nèi)容的出發(fā)點(diǎn);
- 掌握 synchronized 關(guān)鍵字的 3 中使用方式,使我們本節(jié)課程的核心內(nèi)容,所有的內(nèi)容講解都是圍繞這一知識(shí)點(diǎn)進(jìn)行的;
- 了解 synchronized 關(guān)鍵字的內(nèi)存語(yǔ)義,將 synchronized 關(guān)鍵字與 Java 的線程內(nèi)存模型進(jìn)行關(guān)聯(lián),更好的了解 synchronized 關(guān)鍵字的作用及意義,為本節(jié)重點(diǎn)內(nèi)容。
2. synchronized 關(guān)鍵字介紹
概念:synchronized 同步塊是 Java 提供的一種原子性內(nèi)置鎖,Java 中的每個(gè)對(duì)象都可以把它當(dāng)作一個(gè)同步鎖來(lái)使用,這些 Java 內(nèi)置的使用者看不到的鎖被稱為內(nèi)部鎖,也叫作監(jiān)視器鎖。
線程的執(zhí)行:代碼在進(jìn)入 synchronized 代碼塊前會(huì)自動(dòng)獲取內(nèi)部鎖,這時(shí)候其他線程訪問(wèn)該同步代碼塊時(shí)會(huì)被阻塞掛起。拿到內(nèi)部鎖的線程會(huì)在正常退出同步代碼塊或者拋出異常后或者在同步塊內(nèi)調(diào)用了該內(nèi)置鎖資源的 wait 系列方法時(shí)釋放該內(nèi)置鎖。
內(nèi)置鎖:即排它鎖,也就是當(dāng)一個(gè)線程獲取這個(gè)鎖后,其他線程必須等待該線程釋放鎖后才能獲取該鎖。
Tips:由于 Java 中的線程是與操作系統(tǒng)的原生線程一一對(duì)應(yīng)的,所以當(dāng)阻塞一個(gè)線程時(shí),需要從用戶態(tài)切換到內(nèi)核態(tài)執(zhí)行阻塞操作,這是很耗時(shí)的操作,而 synchronized 的使用就會(huì)導(dǎo)致上下文切換。
后續(xù)章節(jié)我們會(huì)引入 Lock 接口和 ReadWriteLock 接口,能在一定場(chǎng)景下很好地避免 synchronized 關(guān)鍵字導(dǎo)致的上下文切換問(wèn)題。
3. synchronized 關(guān)鍵字的作用
作用:在并發(fā)編程中存在線程安全問(wèn)題,使用 synchronized 關(guān)鍵字能夠有效的避免多線程環(huán)境下的線程安全問(wèn)題,線程安全問(wèn)題主要考慮以下三點(diǎn):
- 存在共享數(shù)據(jù),共享數(shù)據(jù)是對(duì)多線程可見(jiàn)的,所有的線程都有權(quán)限對(duì)共享數(shù)據(jù)進(jìn)行操作;
- 多線程共同操作共享數(shù)據(jù)。關(guān)鍵字 synchronized 可以保證在同一時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)同步方法或者同步代碼塊,同時(shí) synchronized 關(guān)鍵字可以保證一個(gè)線程變化的可見(jiàn)性;
- 多線程共同操作共享數(shù)據(jù)且涉及增刪改操作。如果只是查詢操作,是不需要使用 synchronized 關(guān)鍵字的,在涉及到增刪改操作時(shí),為了保證數(shù)據(jù)的準(zhǔn)確性,可以選擇使用 synchronized 關(guān)鍵字。
4. synchronized 的三種使用方式
Java 中每一個(gè)對(duì)象都可以作為鎖,這是 synchronized 實(shí)現(xiàn)同步的基礎(chǔ)。synchronized 的三種使用方式如下:
- 普通同步方法(實(shí)例方法):鎖是當(dāng)前實(shí)例對(duì)象 ,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖;
- 靜態(tài)同步方法:鎖是當(dāng)前類的 class 對(duì)象 ,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖;
- 同步方法塊:鎖是括號(hào)里面的對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖。
接下來(lái)會(huì)對(duì)這三種使用方式進(jìn)行詳細(xì)的講解,也是本節(jié)課程的核心內(nèi)容。
5. synchronized 作用于實(shí)例方法
為了更加深刻的體會(huì) synchronized 作用于實(shí)例方法的使用,我們先來(lái)設(shè)計(jì)一個(gè)場(chǎng)景,并根據(jù)要求,通過(guò)代碼的實(shí)例進(jìn)行實(shí)現(xiàn)。
場(chǎng)景設(shè)計(jì):
- 創(chuàng)建兩個(gè)線程,分別設(shè)置線程名稱為 threadOne 和 threadTwo;
- 創(chuàng)建一個(gè)共享的 int 數(shù)據(jù)類型的 count,初始值為 0;
- 兩個(gè)線程同時(shí)對(duì)該共享數(shù)據(jù)進(jìn)行增 1 操作,每次操作 count 的值增加 1;
- 對(duì)于 count 數(shù)值加 1 的操作,請(qǐng)創(chuàng)建一個(gè)單獨(dú)的 increase 方法進(jìn)行實(shí)現(xiàn);
- increase 方法中,先打印進(jìn)入的線程名稱,然后進(jìn)行 1000 毫秒的 sleep,每次加 1 操作后,打印操作的線程名稱和 count 的值;
- 運(yùn)行程序,觀察打印結(jié)果。
結(jié)果預(yù)期:因?yàn)?increase 方法有兩個(gè)打印的語(yǔ)句,不會(huì)出現(xiàn) threadOne 和 threadTwo 的交替打印,一個(gè)線程執(zhí)行完 2 句打印之后,才能給另外一個(gè)線程執(zhí)行。
實(shí)例:
public class DemoTest extends Thread {
//共享資源
static int count = 0;
/**
* synchronized 修飾實(shí)例方法
*/
public synchronized void increase() throws InterruptedException {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
@Override
public void run() {
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
DemoTest test = new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("threadOne");
t2.setName("threadTwo");
t1. start();
t2. start();
}
結(jié)果驗(yàn)證:
threadTwo 獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadTwo: 1
threadOne 獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadOne: 2
從結(jié)果可以看出,threadTwo 進(jìn)入該方法后,休眠了 1000 毫秒,此時(shí)線程 threadOne 依然沒(méi)有辦法進(jìn)入,因?yàn)?threadTwo 已經(jīng)獲取了鎖,threadOne 只能等待 threadTwo 執(zhí)行完畢后才可進(jìn)入執(zhí)行,這就是 synchronized 修飾實(shí)例方法的使用。
Tips:仔細(xì)看 DemoTest test = new DemoTest () 這就話,我們創(chuàng)建了一個(gè) DemoTest 的實(shí)例對(duì)象,對(duì)于修飾普通方法,synchronized 關(guān)鍵字的鎖即為 test 這個(gè)實(shí)例對(duì)象。
6. synchronized 作用于靜態(tài)方法
Tips:對(duì)于 synchronized 作用于靜態(tài)方法,鎖為當(dāng)前的 class,要明白與修飾普通方法的區(qū)別,普通方法的鎖為創(chuàng)建的實(shí)例對(duì)象。為了更好地理解,我們對(duì)第 5 點(diǎn)講解的代碼進(jìn)行微調(diào),然后觀察打印結(jié)果。
代碼修改:其他代碼不變,只修改如下部分代碼。
- 新增創(chuàng)建一個(gè)實(shí)例對(duì)象 testNew ;
- 將線程 2 設(shè)置為 testNew 。
public static void main(String[] args) throws InterruptedException {
DemoTest test = new DemoTest();
DemoTest testNew = new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(testNew);
t1.setName("threadOne");
t2.setName("threadTwo");
t1. start();
t2. start();
}
結(jié)果驗(yàn)證:
threadTwo 獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadOne 獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadTwo: 1
threadOne: 2
結(jié)果分析:我們發(fā)現(xiàn) threadTwo 和 threadOne 同時(shí)進(jìn)入了該方法,為什么會(huì)出現(xiàn)這種問(wèn)題呢?
因?yàn)槲覀兇舜蔚男薷氖切略隽?testNew 這個(gè)實(shí)例對(duì)象,也就是說(shuō),threadTwo 的鎖是 testNew ,threadOne 的鎖是 test。
兩個(gè)線程持有兩個(gè)不同的鎖,不會(huì)產(chǎn)生互相 block。相信講到這里,同學(xué)對(duì)實(shí)例對(duì)象鎖的作用也了解了,那么我們?cè)俅螌?increase 方法進(jìn)行修改,將其修改成靜態(tài)方法,然后輸出結(jié)果。
代碼修改:
public static synchronized void increase() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。" );
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
結(jié)果驗(yàn)證:
threadOne獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadOne: 1
threadTwo獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadTwo: 2
結(jié)果分析:我們看到,結(jié)果又恢復(fù)了正常,為什么會(huì)這樣?
關(guān)鍵的原因在于,synchronized 修飾靜態(tài)方法,鎖為當(dāng)前 class,即 DemoTest.class。
public class DemoTest extends Thread {}
無(wú)論 threadOne 和 threadTwo 如何進(jìn)行 new 實(shí)例對(duì)象的創(chuàng)建,也不會(huì)改變鎖是 DemoTest.class 的這一事實(shí)。
7. synchronized 作用于同步代碼塊
Tips:對(duì)于 synchronized 作用于同步代碼,鎖為任何我們創(chuàng)建的對(duì)象,只要是個(gè)對(duì)象即可,如 new Object () 可以作為鎖,new String () 也可作為鎖,當(dāng)然如果傳入 this,那么此時(shí)代表當(dāng)前對(duì)象。
我們將代碼恢復(fù)到第 5 點(diǎn)的知識(shí),然后在第 5 點(diǎn)知識(shí)的基礎(chǔ)上,再次對(duì)代碼進(jìn)行如下修改:
代碼修改:
/**
* synchronized 修飾實(shí)例方法
*/
static final Object objectLock = new Object(); //創(chuàng)建一個(gè)對(duì)象鎖
public static void increase() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。" );
synchronized (objectLock) {
sleep(1000);
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
}
代碼解析:我們創(chuàng)建了一個(gè) objectLock 作為對(duì)象鎖,除了第一句打印語(yǔ)句,讓后三句代碼加入了 synchronized 同步代碼塊,當(dāng) threadOne 進(jìn)入時(shí),threadTwo 不可進(jìn)入后三句代碼的執(zhí)行。
結(jié)果驗(yàn)證:
threadOne 獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadTwo 獲取到鎖,其他線程在我執(zhí)行完畢之前,不可進(jìn)入。
threadOne: 1
threadTwo: 2
8. 小結(jié)
本節(jié)內(nèi)容的核心即 synchronized 關(guān)鍵字的 3 種使用方式,這是必須要掌握的問(wèn)題。除此之外,不同的使用方法獲取到的鎖的類型是不一樣的,這是本節(jié)內(nèi)容的重點(diǎn),也是必須要掌握的知識(shí)。
對(duì) synchronized 關(guān)鍵字的熟練使用,是并發(fā)編程中的一項(xiàng)重要技能。