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