Java工程師知識(shí)樹(shù) / Java基礎(chǔ)
synchronized的使用
JDK針對(duì)共享資源數(shù)據(jù)同步問(wèn)題有一種方式為使用synchronized
關(guān)鍵字谒臼,synchronized
提供了一種排他鎖機(jī)制例隆,可以讓程序在同一時(shí)間段內(nèi)只有一個(gè)線程執(zhí)行某些操作。
使用synchronized修飾執(zhí)行內(nèi)容后:
package com.thread.study;
public class TicketWindow implements Runnable {
public static int TICKET_NUM = 10;
@Override
public void run() {
while (true) {
synchronized (this) {
if (TICKET_NUM > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣了票號(hào)為" + TICKET_NUM-- + "的票");
} else {
return;
}
}
}
}
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
Thread t1 = new Thread(ticketWindow, "1號(hào)售票窗口");
Thread t2 = new Thread(ticketWindow, "2號(hào)售票窗口");
Thread t3 = new Thread(ticketWindow, "3號(hào)售票窗口");
t1.start();
t2.start();
t3.start();
}
}
// 執(zhí)行結(jié)果
1號(hào)售票窗口賣了票號(hào)為10的票
3號(hào)售票窗口賣了票號(hào)為9的票
2號(hào)售票窗口賣了票號(hào)為8的票
2號(hào)售票窗口賣了票號(hào)為7的票
2號(hào)售票窗口賣了票號(hào)為6的票
2號(hào)售票窗口賣了票號(hào)為5的票
2號(hào)售票窗口賣了票號(hào)為4的票
2號(hào)售票窗口賣了票號(hào)為3的票
2號(hào)售票窗口賣了票號(hào)為2的票
2號(hào)售票窗口賣了票號(hào)為1的票
將synchronized改為修改run()方法:
package com.thread.study;
public class TicketWindow implements Runnable {
public static int TICKET_NUM = 10;
@Override
public synchronized void run() {
while (true) {
if (TICKET_NUM > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "賣了票號(hào)為" + TICKET_NUM-- + "的票");
} else {
return;
}
}
}
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow();
Thread t1 = new Thread(ticketWindow, "1號(hào)售票窗口");
Thread t2 = new Thread(ticketWindow, "2號(hào)售票窗口");
Thread t3 = new Thread(ticketWindow, "3號(hào)售票窗口");
t1.start();
t2.start();
t3.start();
}
}
//執(zhí)行結(jié)果 不管執(zhí)行多少次都是
1號(hào)售票窗口賣了票號(hào)為10的票
1號(hào)售票窗口賣了票號(hào)為9的票
1號(hào)售票窗口賣了票號(hào)為8的票
1號(hào)售票窗口賣了票號(hào)為7的票
1號(hào)售票窗口賣了票號(hào)為6的票
1號(hào)售票窗口賣了票號(hào)為5的票
1號(hào)售票窗口賣了票號(hào)為4的票
1號(hào)售票窗口賣了票號(hào)為3的票
1號(hào)售票窗口賣了票號(hào)為2的票
1號(hào)售票窗口賣了票號(hào)為1的票
通過(guò)上述兩個(gè)例子對(duì)比,總結(jié)synchronized的使用:
- 由于
synchronized
關(guān)鍵字存在排他性,也就是說(shuō)所有的線程必須串行地經(jīng)過(guò)synchronized
保護(hù)的共享區(qū)域,如果synchronized
作用域越大,則代表著其效率越低,甚至還會(huì)喪失并發(fā)的優(yōu)勢(shì)疲酌。 -
synchronized
關(guān)鍵字應(yīng)該盡可能地只作用于共享資源(數(shù)據(jù))的讀寫作用域,或者說(shuō)是synchronized
鎖的是有增刪改操作的對(duì)象了袁。eg:
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
synchronized (list) {//synchronized鎖的是有增刪改操作的對(duì)象
list.add(Thread.currentThread().getName());
}
}, String.valueOf(i)).start();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
synchronized 關(guān)鍵字原理
synchronized說(shuō)明:
synchronized 關(guān)鍵字是解決共享資源數(shù)據(jù)同步的常用解決方案朗恳,有以下三種使用方式:
- 同步普通方法,鎖的是當(dāng)前對(duì)象载绿。
- 同步靜態(tài)方法粥诫,鎖的是當(dāng)前 Class 對(duì)象。
- 同步塊崭庸,鎖的是 {} 中的對(duì)象怀浆。
synchronized 實(shí)現(xiàn)原理:
JVM 是通過(guò)進(jìn)入、退出對(duì)象監(jiān)視器( Monitor )來(lái)實(shí)現(xiàn)對(duì)方法怕享、同步塊的同步的执赡。
具體實(shí)現(xiàn)是在編譯之后在同步方法調(diào)用前加入一個(gè) monitor.enter
指令,在退出方法和可能發(fā)生異常處插入 monitor.exit
的指令函筋。monitor指令是使用C++實(shí)現(xiàn)的沙合。
其本質(zhì)就是對(duì)一個(gè)對(duì)象監(jiān)視器( Monitor )進(jìn)行獲取,而這個(gè)獲取過(guò)程具有排他性從而達(dá)到了同一時(shí)刻只能一個(gè)線程訪問(wèn)的目的跌帐。
而對(duì)于沒(méi)有獲取到鎖的線程將會(huì)阻塞到方法入口處首懈,直到獲取鎖的線程 monitor.exit
之后才能嘗試?yán)^續(xù)獲取鎖绊率。
synchronized 特性:
- 互斥性(確保線程互斥的訪問(wèn)同步代碼)
- 可見(jiàn)性(保證共享變量的修改能夠及時(shí)可見(jiàn))
- 有序性(有效解決重排序問(wèn)題)
互斥性
1.互斥性,可以認(rèn)為獨(dú)享的意思究履,每次只允許一個(gè)操作者擁有共享資源滤否;
2.被synchronized修飾的代碼塊、實(shí)例方法最仑、靜態(tài)方法藐俺,多線程并發(fā)訪問(wèn)時(shí),只能有一個(gè)線程獲取到鎖泥彤,其它線程都處于阻塞等待欲芹,但在此期間,這些線程仍然可以訪問(wèn)其它非synchronized修飾的方法全景;
可見(jiàn)性
1.可見(jiàn)性,就是每次線程的到來(lái)牵囤,都能訪問(wèn)到最新的值爸黄;
2.因?yàn)樵诨コ庑缘幕A(chǔ)上,由于每次僅有一個(gè)線程執(zhí)行臨界區(qū)的代碼揭鳞,因此其修改的任何變量值對(duì)于稍后執(zhí)行該臨界區(qū)的線程來(lái)說(shuō)是可見(jiàn)的炕贵;
3.因?yàn)榛コ庑缘拇嬖冢脖WC了臨界區(qū)變量修改的原子性野崇,而volatile僅僅只能保證變量修改的可見(jiàn)性称开,并不能保證原子性;
有序性
1.有序性乓梨,就是按照順序來(lái)執(zhí)行鳖轰;
2.同樣因?yàn)樵诨コ庑缘幕A(chǔ)上,代碼塊也好扶镀,實(shí)例方法或靜態(tài)方法也好蕴侣,一旦被synchronized后,各個(gè)線程相互競(jìng)爭(zhēng)臭觉,反正每次只能有一個(gè)線程執(zhí)行昆雀;
3.打個(gè)比方,舉例靜態(tài)方法蝠筑,TestSynchronized.java 中有個(gè)靜態(tài) synchronized static test(){ i++, j++} 方法狞膘,并且代碼塊被synchronized修飾,讓N個(gè)線程都去調(diào)用這個(gè)方法什乙,最后會(huì)發(fā)現(xiàn)每次i和j的輸出值都是一樣的挽封。i++和j++要么一起執(zhí)行完,要么都不執(zhí)行臣镣,不會(huì)出現(xiàn)先i++后场仲,執(zhí)行了其他代碼和悦,過(guò)一會(huì)再執(zhí)行j++的情況。
流程圖如下:
同步代碼塊
public class Synchronize{
public static void main(String[] args) {
synchronized (Synchronize.class){
System.out.println("Synchronize");
}
}
}
---------使用 javap-c 編譯 Synchronize類 可以查看編譯之后的具體信息渠缕。-----------
{
public com.thread.study.Synchronize();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/thread/study/Synchronize
2: dup
3: astore_1
4: monitorenter //同步方法調(diào)用前加入一個(gè) monitor.enter 指令
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String Synchronize
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit //退出的地方插入 monitor.exit 的指令
15: goto 23
18: astore_2
19: aload_1
20: monitorexit //異掣胨兀可能出現(xiàn)的地方插入 monitor.exit 的指令 確保可以正常退出
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 5: 0
line 6: 5
line 7: 13
line 8: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "Synchronize.java"
可以看到在同步塊的入口和出口分別有 monitorenter
,monitorexit
指令亦鳞。
monitorenter指令JVM規(guī)范翻譯:
? 每個(gè)對(duì)象有打自娘胎出來(lái)就自帶一個(gè)內(nèi)置監(jiān)視器鎖(monitor)馍忽。當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán)燕差,過(guò)程如下:
? ? 如果monitor的進(jìn)入數(shù)為0遭笋,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1徒探,該線程即為monitor的所有者瓦呼。
? ? 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入测暗,則進(jìn)入monitor的進(jìn)入數(shù)加1.
? ? 如果其他線程已經(jīng)占用了monitor央串,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0碗啄,再重新嘗試獲取monitor的所有權(quán)。monitorexit指令JVM規(guī)范翻譯:
? ? 執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor的所有者稚字。
? ? 指令執(zhí)行時(shí)饲宿,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0胆描,那線程退出monitor瘫想,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè) monitor 的所有權(quán)昌讲。
同步方法
package com.thread.study;
public class TicketThread {
public static void main(String[] args) {
TicketRunnable ticketRunnable = new TicketThread.TicketRunnable();
Thread t1 = new Thread(ticketRunnable,"zhao");
Thread t2 = new Thread(ticketRunnable,"qian");
t1.start();
t2.start();
}
static class TicketRunnable implements Runnable{
private int TICKET_NUM = 10;
@Override
public synchronized void run() { // 同步方法
if (TICKET_NUM > 0) {
System.out.println(Thread.currentThread().getName() +TICKET_NUM);
TICKET_NUM --;
}
}
}
}
----------使用 javap-c 編譯 TicketThread 可以查看編譯之后的具體信息殿托。主要看內(nèi)部類TicketRunnable------------
{
com.thread.study.TicketRunnable();
descriptor: ()V
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 16: 0
public synchronized void run();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED // ACC_SYNCHRONIZED標(biāo)志
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field TICKET_NUM:I
3: ifle 45
6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
9: new #4 // class java/lang/StringBuilder
12: dup
13: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
16: invokestatic #6 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
19: invokevirtual #7 // Method java/lang/Thread.getName:()Ljava/lang/String;
22: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: getstatic #2 // Field TICKET_NUM:I
28: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
31: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: getstatic #2 // Field TICKET_NUM:I
40: iconst_1
41: isub
42: putstatic #2 // Field TICKET_NUM:I
45: return
LineNumberTable:
line 20: 0
line 21: 6
line 22: 37
line 24: 45
StackMapTable: number_of_entries = 1
frame_type = 45 /* same */
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field TICKET_NUM:I
5: return
LineNumberTable:
line 17: 0
}
SourceFile: "TicketThread.java"
可以看出在synchronized修飾的同步方法的flags中會(huì)有ACC_SYNCHRONIZED標(biāo)識(shí)
ACC_SYNCHRONIZED指令JVM規(guī)范翻譯:
? ? 方法級(jí)的同步是隱式的。同步方法的常量池中會(huì)有一個(gè)ACC_SYNCHRONIZED標(biāo)志剧蚣。
? ? 當(dāng)某個(gè)線程要訪問(wèn)某個(gè)方法的時(shí)候支竹,會(huì)檢查是否有ACC_SYNCHRONIZED,
? ? 如果有設(shè)置鸠按,則需要先獲得監(jiān)視器鎖礼搁,然后開(kāi)始執(zhí)行方法,方法執(zhí)行之后再釋放監(jiān)視器鎖目尖。
? ? 這時(shí)如果其他線程來(lái)請(qǐng)求執(zhí)行方法馒吴,會(huì)因?yàn)闊o(wú)法獲得監(jiān)視器鎖而被阻斷住。
? ? 值得注意的是,如果在方法執(zhí)行過(guò)程中饮戳,發(fā)生了異常豪治,并且方法內(nèi)部并沒(méi)有處理該異常,那么在異常被拋到方法外面之前監(jiān)視器鎖會(huì)被自動(dòng)釋放扯罐。
無(wú)論是monitorenter负拟、 monitorexit,或者是ACC_SYNCHRONIZED歹河,其都是基于Monitor機(jī)制實(shí)現(xiàn)的掩浙。
Monitor
monitor直譯過(guò)來(lái)是監(jiān)視器的意思,專業(yè)一點(diǎn)叫管程秸歧。Monitor機(jī)制一個(gè)重要特點(diǎn)是厨姚,在同一時(shí)間,只有一個(gè)線程/進(jìn)程能進(jìn)入monitor所定義的臨界區(qū)键菱,這使得monitor能夠實(shí)現(xiàn)互斥的效果谬墙。無(wú)法進(jìn)入monitor的臨界區(qū)的進(jìn)程/線程,應(yīng)該被阻塞经备,并且在適當(dāng)?shù)臅r(shí)候被喚醒拭抬。
java則基于monitor機(jī)制實(shí)現(xiàn)了它自己的線程同步機(jī)制,就是synchronized內(nèi)置鎖弄喘。
基本元素
- 臨界區(qū)
臨界區(qū)是被synchronized包裹的代碼塊玖喘,可能是個(gè)代碼塊甩牺,也可能是個(gè)方法蘑志。
- monitor對(duì)象和鎖
monitor對(duì)象是monitor機(jī)制的核心,它本質(zhì)上是jvm用c語(yǔ)言定義的一個(gè)數(shù)據(jù)類型贬派。對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)保存了線程同步所需的信息急但,比如保存了被阻塞的線程的列表,還維護(hù)了一個(gè)基于mutex的鎖搞乏,monitor的線程互斥就是通過(guò)mutex互斥鎖實(shí)現(xiàn)的波桩。
- 條件變量
條件變量和下方wait signal方法的使用有密切關(guān)系 。在獲取鎖進(jìn)入臨界區(qū)之后请敦,如果發(fā)現(xiàn)條件變量不滿足使用wait方法使線程阻塞镐躲,條件變量滿足后signal喚醒被阻塞線程。 tips:當(dāng)線程被signal喚醒之后侍筛,不是從wait那繼續(xù)執(zhí)行的萤皂,而是重新while循環(huán)一次判斷條件是否成立
- 定義在monitor對(duì)象上的wait,signal操作