最重要一條:
synchronized是針對對象的隱式鎖使用的,註意是對象!
舉個小例子,該例子沒有任何業務含義,隻是為瞭說明synchronized的基本用法:
Java代碼
Class MyClass(){
synchronized void myFunction(){
//do something
}
}
public static void main(){
MyClass myClass = new MyClass();
myClass.myFunction();
}
好瞭,就這麼簡單。
myFunction()方法是個同步方法,隱式鎖是誰的?答:是該方法所在類的對象。
看看怎麼使用的:myClass.myFunction();很清楚瞭吧,隱式鎖是myClass的。
說的在明白一點,線程想要執行myClass.myFunction();就要先獲得myClass的鎖。
下面總結一下:
1、synchronized關鍵字的作用域有二種:
1)是某個對象實例內,synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法(如果一個對象有多個synchronized方法,隻要一個線程訪問瞭其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法)。這時,不同的對象實例的synchronized方法是不相幹擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法;
2)是某個類的范圍,synchronized static aStaticMethod{}防止多個線程同時訪問這個類中的synchronized static 方法。它可以對類的所有對象實例起作用。(註:這個可以認為是對Class對象起作用)
2、除瞭方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個區塊中,表示隻對這個區塊的資源實行互斥訪問。用法是: synchronized(this){/*區塊*/},它的作用域是this,即是當前對象。當然這個括號裡可以是任何對象,synchronized對方法和塊的含義和用法並不本質不同;
3、synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成瞭f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法;
synchronized可能造成死鎖,比如:
Java代碼
class DeadLockSample{
public final Object lock1 = new Object();
public final Object lock2 = new Object();
public void methodOne(){
synchronized(lock1){
…
synchronized(lock2){…}
}
}
public void methodTwo(){
synchronized(lock2){
…
synchronized(lock1){…}
}
}
}
假設場景:線程A調用methodOne(),獲得lock1的隱式鎖後,在獲得lock2的隱式鎖之前線程B進入運行,調用methodTwo(),搶先獲得瞭lock2的隱式鎖,此時線程A等著線程B交出lock2,線程B等著lock1進入方法塊,死鎖就這樣被創造出來瞭。
下面舉一個有業務含義的例子幫助理解,並展示一下synchronized與wait()、notifyAll()的使用。
這裡先介紹一下這兩個方法:
wait()/notify():調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用任意對象的notify()方法則導致因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。
好瞭,再來看看synchronized與這兩個方法之間的關系:
1.有synchronized的地方不一定有wait,notify
2.有wait,notify的地方必有synchronized.這是因為wait和notify不是屬於線程類,而是每一個對象都具有的方法(事實上,這兩個方法是Object類裡的),而且,這兩個方法都和對象鎖有關,有鎖的地方,必有synchronized。
慢著,讓我們思考一下Java這個設計是否合理?前面說瞭,鎖是針對對象的,wait()/notify()的操作是與對象鎖相關的,那麼把wait()/notify()設計在Object中也就是合情合理的瞭。
恩,再想一下,為什麼有wait,notify的地方必有synchronized?
synchronized方法中由當前線程占有鎖。另一方面,調用wait()notify()方法的對象上的鎖必須為當前線程所擁有。因此,wait()notify()方法調用必須放置在synchronized方法中,synchronized方法的上鎖對象就是調用wait()notify()方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException 異常。
好瞭,以上準備知識充足瞭,現在說例子:銀行轉賬,同一時刻隻有一個人可以轉賬。
那麼我們自然想到在Bank類中有一個同步的轉賬方法:
Java代碼
public Class Bank(){
float account[ACCOUNT_NUM];
…
public synchronized void transfer(from, to, amount){
//轉賬
}
}
現在有一個問題,如果一個人獲得瞭使用銀行的鎖,但是餘額不足怎麼辦?
好,那我們進行改進:
Java代碼
public Class Bank(){
float account[ACCOUNT_NUM];
…
public synchronized void transfer(int from, int to, float amount){
while(account[from]){
wait();
}
account[from] -= amount;
account[to] += amount;
notifyAll();
}
}
這樣就滿足需求瞭。
可見,用對象鎖來管理試圖進入synchronized方法的線程,
另外,由條件判斷來管理已經進入同步方法中的線程即當前線程
這裡還補充兩點:
1. 調用wait()方法前的判斷最好用while,而不用if;因為while可以實現被喚醒後線程再次作條件判斷;而if則隻能判斷一次
2. 用notifyAll()優先於notify()。
另外註意一點:
能調用wait()/notify()的隻有當前線程,前提是必須獲得瞭對象鎖,就是說必須要進入到synchronized方法中。
————————————-我是分割線—————————————-
補充一點JMM的相關知識,對理解線程同步很有好處。
(說明一下:以下內容參考瞭一些網上零零碎碎的帖子,非照搬且無商業目的,請勿跨省。)
JVM中(留神:馬上講到的這兩個存儲區隻在JVM內部與物理存儲區無關)存在一個主內存(Main Memory),Java中所有的變量存儲在主內存中,所有實例和實例的字段都在此區域,對於所有的線程是共享的(相當於黑板,其他人都可以看到的)。每個線程都有自己的工作內存(Working Memory),工作內存中保存的是主存中變量的拷貝,(相當於自己筆記本,隻能自己看到),工作內存由緩存和堆棧組成,其中緩存保存的是主存中的變量的copy,堆棧保存的是線程局部變量。線程對所有變量的操作都是在工作內存中進行的,線程之間無法直接互相訪問工作內存,變量的值得變化的傳遞需要主存來完成。在JMM中通過並發線程修改的變量值,必須通過線程變量同步到主存後,其他線程才能訪問到。
看看這個圖是不是更形象
好啦,下面來看線程對某個變量的操作步驟:
1.從主內存中復制數據到工作內存
2.執行代碼,對數據進行各種操作和計算
3.把操作後的變量值重新寫回主內存中
現在舉個例子,設想兩個棋手要通過兩個終端顯示器(Working Memory)對奕,而觀眾要通過服務器大屏幕(Main Memory )觀看他們的比賽過程。這兩個棋手相當於是同步中的線程,觀眾相當於其它線程。棋手是無法直接操作服務器的大屏幕的,他隻能看到自己的終端顯示器,隻能先從服務器上將當前結果先復制到自己的終端上(步驟1),然後在自己的終端上操作(步驟2),將操作的結果記錄在終端上,然後在某一時刻同步到服務器上(步驟3)。他所能看到的結果就是從服務器上復制到自己的終端上的內容,而要想把自己操作後的結果讓其他人看到必須同步到服務器上才行。至於什麼時候同步,那要看終端和服務器的通信機制。
回到這三個步驟,這個順序是我們希望的,但是,JVM並不保證第1步和第3步會嚴格按照上述次序立即執行。因為根據java語言規范的規定,線程的工作內存和主存間的數據交換是松耦合的,什麼時候需要刷新工作內存或者什麼時候更新主存的內容,可以由具體的虛擬機實現自行決定。由於JVM可以對特征代碼進行調優,也就改變瞭某些運行步驟的次序的顛倒,那麼每次線程調用變量時是直接取自己的工作存儲器中的值還是先從主存儲器復制再取是沒有保證的,任何一種情況都可能發生。同樣的,線程改變變量的值之後,是否馬上寫回到主存儲器上也是不可保證的,也許馬上寫,也許過一段時間再寫。那麼,在多線程的應用場景下就會出現問題瞭,多個線程同時訪問同一個代碼塊,很有可能某個線程已經改變瞭某變量的值,當然現在的改變僅僅是局限於工作內存中的改變,此時JVM並不能保證將改變後的值立馬寫到主內存中去,也就意味著有可能其他線程不能立馬得到改變後的值,依然在舊的變量上進行各種操作和運算,最終導致不可預料的結果。
這可如何是好呢?還好有synchronized和volatile:
1.多個線程共有的字段應該用synchronized或volatile來保護.
2.synchronized負責線程間的互斥.即同一時候隻有一個線程可以執行synchronized中的代碼.
synchronized還有另外一個方面的作用:在線程進入synchronized塊之前,會把工作存內存中的所有內容映射到主內存上,然後把工作內存清空再從主存儲器上拷貝最新的值。而在線程退出synchronized塊時,同樣會把工作內存中的值映射到主內存,不過此時並不會清空工作內存。這樣一來就可以強制其按照上面的順序運行,以保證線程在執行完代碼塊後,工作內存中的值和主內存中的值是一致的,保證瞭數據的一致性!
3.volatile負責線程中的變量與主存儲區同步.但不負責每個線程之間的同步.
volatile的含義是:線程在試圖讀取一個volatile變量時,會從主內存區中讀取最新的值。現在很清楚瞭吧。
————————————-我也是分割線—————————————-
說到synchronized,那就再來談談ThreadLocal。
在JDK的API文檔中ThreadLocal的定義第一句道出:This class provides thread-local variables. 好,這個類提供瞭線程本地的變量。隻看這一句,讓我們結合到上面JMM的知識我們來分析一下理一下頭緒:
我們已經知道瞭synchronized的含義是同步,也就是針對的是主存中的變量,隻不過多線程執行時為瞭實現同步就需要每個線程在操作這個變量時要完成那三個步驟(對,就是主存與線程工作內存之間完成交互的那三步),我們很自然想到:
1. 使用目的:需要有某些變量在多個線程中共享,有共享才會需要同步。
2. 執行效率:直觀上感覺一下,同步的執行效率肯定不高,事實上也的確是這樣,為什麼?看看那三步多麻煩。
好,現在再來看看ThreadLocal的定義,我們能想到什麼?
首先讓我們思考一個問題,並不是所有多線程程序都需要共享啊,這個時候還用同步那一套豈不是很多餘?讓每個線程維護自己的變量不就OK瞭,反正又不需要共享。對,ThreadLocal就是幹這個事的。另一方面,那不用多說,性能上肯定優越嘍。
小結一下:對比synchronized和ThreadLocal首先要清楚,兩者的使用目的不同,關鍵點就在是否需要共享變量。就是說,ThreadLocal根本不是同步。再說囉嗦一點:ThreadLocal和Synchonized都用於解決多線程並發訪問。但是ThreadLocal與synchronized有本質的區別,synchronized是利用鎖的機制,使變量或代碼塊在某一時該隻能被一個線程訪問。而ThreadLocal為每一個線程都提供瞭變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離瞭多個線程對數據的數據共享。Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。兩者處於不同的問題域。這個都不清晰的話說再多都沒用,隻會更糊塗。
好瞭,上個例子看看ThreadLocal怎麼用的
Java代碼
public class TreadLocalDemo implements Runnable {
private final static ThreadLocal studentLocal = new ThreadLocal(); //ThreadLocal對象在這
public static void main(String[] agrs) {
TreadLocalDemo td = new TreadLocalDemo();
Thread t1 = new Thread(td,”a”);
Thread t2 = new Thread(td,”b”);
t1.start();
t2.start();
}
public void run() {
accessStudent();
}
public void accessStudent() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName+” is running!”);
Random random = new Random();
int age = random.nextInt(100);
System.out.println(“thread “+currentThreadName +” set age to:”+age);
Student student = getStudent(); //每個線程都獨立維護一個Student變量
student.setAge(age);
System.out.println(“thread “+currentThreadName+” first read age is:”+student.getAge());
try {
Thread.sleep(5000);
}
catch(InterruptedException ex) {
ex.printStackTrace();
}
System.out.println(“thread “+currentThreadName +” second read age is:”+student.getAge());
}
protected Student getStudent() {
Student student = (Student)studentLocal.get(); //從ThreadLocal對象中取
if(student == null) {
student = new Student();
studentLocal.set(student); //如果沒有就創建一個
}
return student;
}
protected void setStudent(Student student) {
studentLocal.set(student); //放入ThreadLocal對象中
}
}
ThreadLocal通過一個Map來為每個線程都持有一個變量副本,用ThreadLocal對象以鍵值對的方式來維護這些線程獨立變量 。