synchronized
Java 語言為了解決并發(fā)編程中存在的原子性枫甲、可見性和有序性問題沪蓬,提供了一系列和并發(fā)處理相關的關鍵字揭芍,比如 synchronized眨唬、volatile幻件、final拨黔、concurren 包等。
synchronized 關鍵字在需要原子性绰沥、可見性和有序性這三種特性的時候都可以作為其中 一 種 解 決 方 案 篱蝇, 看 起 來 是“ 萬 能 ” 的 。 的 確 徽曲, 大 部 分 并 發(fā) 控 制 操 作 都 能 使 用synchronized 來完成零截。
海明威在他的《午后之死》說過的:“冰山運動之雄偉壯觀,是因為他只有八分之一在水面上秃臣〗а茫”對于程序員來說,synchronized 只是個關鍵字而已奥此,用起來很簡單绍撞。之所以我們可以在處理多線程問題時可以不用考慮太多,就是因為這個關鍵字幫我們屏蔽了很多細節(jié)
主要介紹 synchronized 的用法得院、synchronized 的原理傻铣,以及 synchronized 是如何提供原子性、可見性和有序性保障的等
synchronized 的用法
synchronized 是 Java 提供的一個并發(fā)控制的關鍵字祥绞。主要有兩種用法非洲,分別是同步方法和同步代碼塊。也就是說蜕径,synchronized 既可以修飾方法也可以修飾代碼塊两踏。
public class SynchronizedDemo {? ? //同步方法? ? public synchronized void doSth(){? ? ? ? System.out.println("Hello World");? ? }? ? //同步代碼塊? ? public void doSth1(){? ? ? ? synchronized (SynchronizedDemo.class){? ? ? ? ? ? System.out.println("Hello World");? ? ? ? }? ? }}
被 synchronized 修飾的代碼塊及方法,在同一時間兜喻,只能被單個線程訪問梦染。
synchronized 的實現(xiàn)原理
synchronized,是 Java 中用于解決并發(fā)情況下數(shù)據(jù)同步訪問的一個很重要的關鍵字朴皆。當我們想要保證一個共享資源在同一時間只會被一個線程訪問到時帕识,我們可以在代碼中使用synchronized 關鍵字對類或者對象加鎖
對上面的代碼進行反編譯,可以得到如下代碼:
public synchronized void doSth();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Hello World5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnpublic void doSth1();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: ldc #5 // class com/hollis/SynchronizedTest2: dup3: astore_14: monitorenter5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;8: ldc #3 // String Hello World10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V13: aload_114: monitorexit15: goto 2318: astore_219: aload_120: monitorexit21: aload_222: athrow23: return
通過反編譯后代碼可以看出:對于同步方法遂铡,JVM 采用 ACC_SYNCHRONIZED 標記符來實現(xiàn)同步肮疗。 對于同步代碼塊。JVM 采用 monitorenter扒接、monitorexit 兩個指令來實現(xiàn)同步
方法級的同步是隱式的伪货。同步方法的常量池中會有一個 ACC_SYNCHRONIZED 標志们衙。當某個線程要訪問某個方法的時候,會檢查是否有 ACC_SYNCHRONIZED碱呼,如果有設置蒙挑,則需要先獲得監(jiān)視器鎖,然后開始執(zhí)行方法愚臀,方法執(zhí)行之后再釋放監(jiān)視器鎖脆荷。這時如果其他線程來請求執(zhí)行方法,會因為無法獲得監(jiān)視器鎖而被阻斷住懊悯。值得注意的是蜓谋,如果在方法執(zhí)行過程中,發(fā)生了異常炭分,并且方法內部并沒有處理該異常桃焕,那么在異常被拋到方法外面之前監(jiān)視器鎖會被自動釋放
同步代碼塊使用 monitorenter 和 monitorexit 兩個指令實現(xiàn)∨趺可以把執(zhí)行monitorenter 指令理解為加鎖观堂,執(zhí)行 monitorexit 理解為釋放鎖。 每個對象維護著一個記錄著被鎖次數(shù)的計數(shù)器呀忧。未被鎖定的對象的該計數(shù)器為 0师痕,當一個線程獲得鎖(執(zhí)行monitorenter)后,該計數(shù)器自增變?yōu)?1 而账,當同一個線程再次獲得該對象的鎖的時候胰坟,計數(shù)器再次自增。當同一個線程釋放鎖(執(zhí)行 monitorexit 指令)的時候泞辐,計數(shù)器再自減笔横。當計數(shù)器為 0 的時候。鎖將被釋放咐吼,其他線程便可以獲得鎖吹缔。
無論是 ACC_SYNCHRONIZED 還是 monitorenter、monitorexit 都是基于Monitor 實現(xiàn)的锯茄,在 Java 虛擬機(HotSpot)中厢塘,Monitor 是基于 C++實現(xiàn)的,由ObjectMonitor 實現(xiàn)肌幽。
ObjectMonitor 類中提供了幾個方法晚碾,如 enter、exit牍颈、wait迄薄、notify、notifyAll 等煮岁。sychronized 加鎖的時候讥蔽,會調用 objectMonitor 的 enter 方法,解鎖的時候會調用 exit方法画机。
synchronized 與原子性
原子性是指一個操作是不可中斷的冶伞,要全部執(zhí)行完成,要不就都不執(zhí)行
線程是 CPU調度的基本單位步氏。CPU 有時間片的概念响禽,會根據(jù)不同的調度算法進行線程調度。當一個線程獲得時間片之后開始執(zhí)行荚醒,在時間片耗盡之后芋类,就會失去 CPU 使用權。所以在多線程場景下界阁,由于時間片在線程間輪換侯繁,就會發(fā)生原子性問題
在 Java 中,為了保證原子性泡躯,提供了兩個高級的字節(jié)碼指令 monitorenter 和monitorexit贮竟。前面中,介紹過较剃,這兩個字節(jié)碼指令咕别,在 Java 中對應的關鍵字就是synchronized
通過 monitorenter 和 monitorexit 指令,可以保證被 synchronized 修飾的代碼在同一時間只能被一個線程訪問写穴,在鎖未釋放之前惰拱,無法被其他線程訪問到。因此啊送,在 Java 中可以使用 synchronized 來保證方法和代碼塊內的操作是原子性的弓颈。
線程 1 在執(zhí)行 monitorenter 指令的時候,會對 Monitor 進行加鎖删掀,加鎖后其他線程無法獲得鎖翔冀,除非線程 1 主動解鎖。即使在執(zhí)行過程中披泪,由于某種原因纤子,比如 CPU 時間片用完,線程 1 放棄了 CPU款票,但是控硼,他并沒有進行解鎖。而由于 synchronized 的鎖是可重入的艾少,下一個時間片還是只能被他自己獲取到卡乾,還是會繼續(xù)執(zhí)行代碼。直到所有代碼執(zhí)行完缚够。這就保證了原子性幔妨。
synchronized 與可見性
可見性是指當多個線程訪問同一個變量時鹦赎,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值误堡。
Java 內存模型規(guī)定了所有的變量都存儲在主內存中古话,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝锁施,線程對變量的所有操作都必須在工作內存中進行陪踩,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量悉抵,線程間變量的傳遞均需要自己的工作內存和主存之間進行數(shù)據(jù)同步進行肩狂。所以,就可能出現(xiàn)線程 1 改了某個變量的值姥饰,但是線程 2 不可見的情況
前面我們介紹過傻谁,被 synchronized 修飾的代碼,在開始執(zhí)行時會加鎖媳否,執(zhí)行完成后會進行解鎖栅螟。而為了保證可見性,有一條規(guī)則是這樣的:對一個變量解鎖之前篱竭,必須先把此變量同步回主存中力图。這樣解鎖后,后續(xù)線程就可以訪問到被修改后的值掺逼。
所以吃媒,synchronized 關鍵字鎖住的對象,其值是具有可見性的
synchronized 與有序性
有序性即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行
除引入了時間片以外吕喘,由于處理器優(yōu)化和指令重排等赘那,CPU 還可能對輸入代碼進行亂序執(zhí)行,比如 load->add->save 有可能被優(yōu)化成 load->save->add 氯质。這就是可能存在有序性問題
這里需要注意的是募舟,synchronized 是無法禁止指令重排和處理器優(yōu)化的。也就是說闻察,synchronized 無法避免上述提到的問題拱礁。
那么,為什么還說 synchronized 也提供了有序性保證呢辕漂?
這就要再把有序性的概念擴展一下了呢灶。Java 程序中天然的有序性可以總結為一句話:如果在本線程內觀察,所有操作都是天然有序的钉嘹。如果在一個線程中觀察另一個線程鸯乃,所有操作都是無序的。
as-if-serial 語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度)跋涣,單線程程序的執(zhí)行結果都不能被改變缨睡。編譯器和處理器無論如何優(yōu)化鸟悴,都必須遵守as-if-serial 語義
這里不對 as-if-serial 語義詳細展開了,簡單說就是宏蛉,as-if-serial 語義保證了單線程中遣臼,指令重排是有一定的限制的性置,而只要編譯器和處理器都遵守了這個語義拾并,那么就可以認為單線程程序是按照順序執(zhí)行的。當然鹏浅,實際上還是有重排的嗅义,只不過我們無須關心這種重排的干擾。
所以呢隐砸,由于 synchronized 修飾的代碼之碗,同一時間只能被同一線程訪問。那么也就是單線程執(zhí)行的季希。所以褪那,可以保證其有序性。
synchronized 與鎖優(yōu)化
前面介紹了 synchronized 的用法式塌、原理以及對并發(fā)編程的作用博敬。是一個很好用的關鍵字。
synchronized 其實是借助 Monitor 實現(xiàn)的峰尝,在加鎖時會調用 objectMonitor 的enter 方法偏窝,解鎖的時候會調用 exit 方法。事實上武学,只有在 JDK1.6 之前祭往,synchronized的實現(xiàn)才會直接調用 ObjectMonitor 的 enter 和 exit,這種鎖被稱之為重量級鎖
所以火窒,在 JDK1.6 中出現(xiàn)對鎖進行了很多的優(yōu)化硼补,進而出現(xiàn)輕量級鎖,偏向鎖熏矿,鎖消除已骇,適應性自旋鎖,鎖粗化(自旋鎖在 1.4 就有曲掰,只不過默認的是關閉的疾捍,jdk1.6 是默認開啟的),這些操作都是為了在線程之間更高效的共享數(shù)據(jù) 栏妖,解決競爭問題乱豆。
final
final 是 Java 中的一個關鍵字,它所表示的是“這部分是無法修改的”吊趾。
使用 final 可以定義 :變量宛裕、方法瑟啃、類。
final 變量
如果將變量設置為 final揩尸,則不能更改 final 變量的值(它將是常量)蛹屿。
class Test{? ? final String name = "Hollis";}
一旦 final 變量被定義之后,是無法進行修改的岩榆。
final 方法
如果方法聲明為 final错负,則不能覆蓋它。
class Parent{? ? final void name() {? ? ? ? System.out.println("Hollis");? ? }}
當我們定義以上類的子類的時候勇边,無法覆蓋其 name 方法犹撒,會編譯失敗
final 類
如果把一個類聲明為 final,則不能繼承它粒褒。
final class Parent {}
static
static 表示“靜態(tài)”的意思识颊,用來修飾成員變量和成員方法,也可以形成靜態(tài) static代碼塊奕坟。
靜態(tài)變量
我們用 static 表示變量的級別祥款,一個類中的靜態(tài)變量,不屬于類的對象或者實例月杉。因為靜態(tài)變量與所有的對象實例共享刃跛,因此他們不具線程安全性。
通常沙合,靜態(tài)變量常用 final 關鍵來修飾奠伪,表示通用資源或可以被所有的對象所使用。如果靜態(tài)變量未被私有化首懈,可以用“類名.變量名”的方式來使用绊率。
private static int count;public static String str;
靜態(tài)方法
與靜態(tài)變量一樣,靜態(tài)方法是屬于類而不是實例究履。
一個靜態(tài)方法只能使用靜態(tài)變量和調用靜態(tài)方法滤否。通常靜態(tài)方法通常用于想給其他的類使用而不需要創(chuàng)建實例。例如:Collections class(類集合)最仑。
Java 的包裝類和實用類包含許多靜態(tài)方法藐俺。main()方法就是 Java 程序入口點,是靜態(tài)方法泥彤。
//static method examplepublic static void setCount(int count) {if(count > 0)? ? StaticExample.count = count;}//static util methodpublic static int addInts(int i, int...js){? ? int sum=i;? ? for(int x : js) sum+=x;? ? return sum;}
從 Java8 以上版本開始也可以有接口類型的靜態(tài)方法了
靜態(tài)代碼塊
Java 的靜態(tài)塊是一組指令在類裝載的時候在內存中由 Java ClassLoader 執(zhí)行欲芹。
靜態(tài)塊常用于初始化類的靜態(tài)變量。大多時候還用于在類裝載時候創(chuàng)建靜態(tài)資源吟吝。
Java 不允許在靜態(tài)塊中使用非靜態(tài)變量菱父。一個類中可以有多個靜態(tài)塊,盡管這似乎沒有什么用。靜態(tài)塊只在類裝載入內存時浙宜,執(zhí)行一次官辽。
Static{//can be used to initialize resources when class is loadedSystem.out.println("StaticExample static block");//can access only static variables and methodsstr="Test";setCount(2);}
靜態(tài)類
Java 可以嵌套使用靜態(tài)類,但是靜態(tài)類不能用于嵌套的頂層粟瞬。
靜態(tài)嵌套類的使用與其他頂層類一樣同仆,嵌套只是為了便于項目打包。
const
const 是 Java 預留關鍵字裙品,用于后期擴展用俗批,用法跟 final 相似,不常用