一、java內存模型
提到同步、鎖,就必須提到Java的內存模型,為了提高程序的執行效率,java也吸收了傳統應用程序的多級緩存體系。
在共享內存的多處理器體系架構中,每個處理器都擁有自己的緩存,並且定期地與主內存進行協調。在不同的處理器架構中提供了不同級別的緩存一致性(Cache Coherence),其中一部分只提供最小的保證,即允許不同的處理器在任意時刻從同一個存儲位置上看到不同的值。操作系統、編譯器以及運行時(有時甚至包括應用程序)需要彌合這種在硬件能力與線程安全之間的差異。
要想確保每個處理器都能在任意時刻知道其他處理器正在進行的工作,將需要非常大的開銷。在大多數時間里,這種信息是不必要的。因此處理器會適當放寬存儲一致性保證,以換取性能的提升。在架構定義的內存模型中將告訴應用程序可以從內存系統中獲得怎樣的保證,此外還定義了一些特殊的指令(稱為內存柵欄),當需要共享數據時,這些指令就能實現額外的存儲協調保證。為了使java開發人員無須關心不同架構內存模型之間的差異,Java還提供了自己的內存模型,並且JVM通過在適當的位置上插入內存柵欄來屏蔽在JVM與底層之平台內存模型之間的差異。
經過上面的講解和上圖,我們知道線程在運行時候有一塊內存專用區域,Java程序會將變量同步到線程所在的內存。這時候會操作工作內存中的變量,而線程中的變量何時同步回到內存是不可預期的。但是java內存模型規定,通過關鍵詞”synchronized“、”volatile“可以讓java保證某些約束。
- “volatile” - 保證讀寫的都是主內存變量。
- “synchronized” - 保證在塊開始時,都同步主內存值到工作內存,而快結束時,將工作內存同步會主內存。
重排序
public class PossibleReordering { static int x = 0,y=0; static int a=0,b=0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { @Override public void run() { a = 1; x = b; } }); Thread two = new Thread(new Runnable() { @Override public void run() { b = 2; y = a; } }); one.start();two.start(); one.join();two.join(); System.out.println("x:" + x+",y:"+y); } }
重排序。如上圖,執行結果,一般人可能認為是1,1;真正的執行結果可能每次都不一樣。拜JMM重排序所賜,JMM使得不同線程的操作順序是不同的,從而導致在缺乏同步的情況下,要推斷操作的執行結果將變得更加復雜。各種使操作延遲或看似亂序執行的不同原因,都可以歸為重排序。內存級的重排序會使程序的行為變得不可預測。如果沒有同步,要推斷出程序的執行順序是非常困難的,而要確保在程序中正確的使用同步卻是非常容易的。同步將限制編譯器和硬件運行時對內存操作重排序的方式。
鎖synchronized
鎖實現了對臨界資源的互斥訪問,被synchronized修飾的代碼只有一條線程可以通過,是嚴格的排它鎖、互斥鎖。沒有獲得對應鎖對象監視器(monitor)的線程會進入等待隊列,任何線程必須獲得monitor的所有權才可以進入同步塊,退出同步快或者遇到異常都要釋放所有權,JVM規范通過兩個內存屏障(memory barrier)命令來實現排它邏輯。內存屏障可以理解成順序執行的一組CPU指令,完全無視指令重排序。
什么是鎖
public class TestStatic {
public syncronized static void write(boolean flag) { xxxxx } public synchronized static void read() { xxxxx } }
線程1訪問TestStatic.write()方法時,線程2能訪問TestStatic.read()方法嗎
線程1訪問new TestStatic().write()方法時,線程2能訪問new TestStatic().read()方法嗎
線程1訪問TestStatic.write()方法時,線程2能訪問new TestStatic().read()方法嗎
public class Test {
public syncronized void write(boolean flag) { xxxxx } public synchronized void read() { xxxxx } }
Test test = new Test();線程1訪問test.write() 方法,線程2能否訪問test.read()方法
Test a = new Test(); Test b = new Test();線程1訪問a.write()訪問,線程2能否訪問b.read()方法
答案,java中每個對象都可以作為一個鎖,而對象就決定了鎖的粒度大小。
對於實例同步方法,鎖是當前對象。
對於靜態方法,鎖是TestSTatic.class對象
對於同步代碼塊,鎖是Synchronized括號里面配置的對象
TestStatic類,1問,作用范圍全體class對象,線程1拿到,線程2就不能拿到
2問,3問同上
Test類,1問,不能,鎖都是實例對象test,線程1拿到鎖之后,線程2無法訪問
2問,可以,線程1鎖是實例a,線程2是實例b。
獨占鎖
如果你不敢確定該用什么鎖,就用這個吧,在保證正確的前提下,后續在提高開發效率。
public class ServerStatus {
public final Set<String> users; public final Set<String> quers; public synchronized void addUser(String u ) { users.add(u); } public synchronized void addQuery(String q ) { quers.add(q); } public synchronized void removeUser(String u) { users.remove(u); } public synchronized void removeQuery(String q) { quers.remove(q); } }
分拆鎖
如果在整個應用程序只有一個鎖,而不是為每個對象分配一個獨立的鎖,那么所有同步代碼塊的執行就會變成串行化執行。由於很多線程都會競爭同一個全局鎖,因此兩個線程同時請求這個鎖的概率將會劇增,從而導致更嚴重的競爭。所以如果將這些鎖請求分到更多的鎖上,就能有效降低鎖競爭程度。由於等待而被阻塞的線程將更少,從而可伸縮性將提高。
上文中users、quers是兩個相互獨立的變量,可以將此分解為兩個獨立的鎖,每個鎖只保護一個變量,降低每個鎖被請求的頻率。
public class ServerStatus {
public final Set<String> users; public final Set<String> quers; public void addUser(String u ) { synchronized(users) { users.add(u); } } public void addQuery(String q ) { synchronized(quers) { quers.add(q); } } public void removeUser(String u) { synchronized(users) { users.remove(u); } } public void removeQuery(String q) { synchronized(quers) { quers.remove(q); } } }
分離鎖
在某些情況下,可以將鎖分解技術進一步擴展為對一組獨立對象上的鎖進行分解,這種情況稱為鎖分段。例如ConcurrencyHashMap是有一個包含16個鎖的數組實現,每個鎖保護所有散列桶的1/16,其中第N個散列桶由第(N mod 16)個鎖來保護。假設所有關鍵字都時間均與分布,那么相當於把鎖的請求減少到原來的1/16,可以支持多達16個的並發寫入。
鎖分段的劣勢在於:與采用單個鎖來實現獨占訪問相比,要獲取多個鎖來實現獨占訪問將更加困難並且開銷更高,比如計算size、重hash。
分布式鎖
zookeeper,判斷臨時節點是否存在,存在就說明已經有人爭搶到鎖;不存在就創建節點,表明擁有該鎖。
記下,以后詳細研究
《 分布式鎖實現:數據庫、redis、zookeeper》volatile
volatile是比synchronized更輕量級的同步原語,volatile可以修飾實例變量、靜態變量、以及數組變量(網上大牛說,維護的是引用,但是里面的對象。。。嘿嘿嘿)。被volatile修飾的變量,JVM規范規定,一個線程在修改完,另外的線程能讀取最新的值。
但僅僅保證可見性,不保證原子性,所以volatile通常用來修飾boolean類型或者狀態比較少的數據類型,而且不能用來更新依賴變量之前值的操作(例volatile++)。
volatile內部僅僅是對變量的操作多了一條cpu指令(lock#指令),它會強制寫數據到緩存,如果緩存數據同時也在主存,會強制寫數據更新到主存,並且使所有持有該主存數據地址的緩存統統失效,觸發其他持有緩存數據的線程從主存獲取最新數據,從而實現同步。