java的內置鎖:每一個java對象均可以用作一個實現同步的鎖,這些鎖成爲內置鎖。線程進入同步代碼塊或方法的時候會自動得到該鎖,在退出同步代碼塊或方法時會釋放該鎖。得到內置鎖的惟一途徑就是進入這個鎖的保護的同步代碼塊或方法。java
java內置鎖是一個互斥鎖,這就是意味着最多隻有一個線程可以得到該鎖,當線程A嘗試去得到線程B持有的內置鎖時,線程A必須等待或者阻塞,知道線程B釋放這個鎖,若是B線程不釋放這個鎖,那麼A線程將永遠等待下去。編程
java的對象鎖和類鎖:java的對象鎖和類鎖在鎖的概念上基本上和內置鎖是一致的,可是,兩個鎖實際是有很大的區別的,對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。咱們知道,類的對象實例能夠有不少個,可是每一個類只有一個class對象,因此不一樣對象實例的對象鎖是互不干擾的,可是每一個類只有一個類鎖。可是有一點必須注意的是,其實類鎖只是一個概念上的東西,並非真實存在的,它只是用來幫助咱們理解鎖定實例方法和靜態方法的區別的併發
上面已經對鎖的一些概念有了一點了解,下面探討synchronized關鍵字的用法。高併發
synchronized的用法:synchronized修飾方法和synchronized修飾代碼塊。性能
下面分別分析這兩種用法在對象鎖和類鎖上的效果。this
對象鎖的synchronized修飾方法和代碼塊:spa
public class TestSynchronized { public void test1() { synchronized (this) { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } } public synchronized void test2() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); Thread test1 = new Thread(new Runnable() { public void run() { myt2.test1(); } }, "test1"); Thread test2 = new Thread(new Runnable() { public void run() { myt2.test2(); } }, "test2"); test1.start(); test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }
1. test2 : 4 2. test2 : 3 3. test2 : 2 4. test2 : 1 5. test2 : 0 6. test1 : 4 7. test1 : 3 8. test1 : 2 9. test1 : 1 10. test1 : 0
上述的代碼,第一個方法時用了同步代碼塊的方式進行同步,傳入的對象實例是this,代表是當前對象,固然,若是須要同步其餘對象實例,也不可傳入其餘對象的實例;第二個方法是修飾方法的方式進行同步。由於第一個同步代碼塊傳入的this,因此兩個同步代碼所須要得到的對象鎖都是同一個對象鎖,下面main方法時分別開啓兩個線程,分別調用test1和test2方法,那麼兩個線程都須要得到該對象鎖,另外一個線程必須等待。上面也給出了運行的結果能夠看到:直到test2線程執行完畢,釋放掉鎖,test1線程纔開始執行。(可能這個結果有人會有疑問,代碼裏面明明是先開啓test1線程,爲何先執行的是test2呢?這是由於java編譯器在編譯成字節碼的時候,會對代碼進行一個重排序,也就是說,編譯器會根據實際狀況對代碼進行一個合理的排序,編譯前代碼寫在前面,在編譯後的字節碼不必定排在前面,因此這種運行結果是正常的, 這裏是題外話,最主要是檢驗synchronized的用法的正確性)線程
若是咱們把test2方法的synchronized關鍵字去掉,執行結果會如何呢?code
1.test1 : 4 2.test2 : 4 3.test2 : 3 4.test1 : 3 5.test1 : 2 6.test2 : 2 7.test2 : 1 8.test1 : 1 9.test2 : 0 10.test1 : 0
上面是執行結果,咱們能夠看到,結果輸出是交替着進行輸出的,這是由於,某個線程獲得了對象鎖,可是另外一個線程仍是能夠訪問沒有進行同步的方法或者代碼。進行了同步的方法(加鎖方法)和沒有進行同步的方法(普通方法)是互不影響的,一個線程進入了同步方法,獲得了對象鎖,其餘線程仍是能夠訪問那些沒有同步的方法(普通方法)。這裏涉及到內置鎖的一個概念(此概念出自java併發編程實戰第二章):對象的內置鎖和對象的狀態之間是沒有內在的關聯的,雖然大多數類都將內置鎖用作一種有效的加鎖機制,但對象的域並不必定經過內置鎖來保護。當獲取到與對象關聯的內置鎖時,並不能阻止其餘線程訪問該對象,當某個線程得到對象的鎖以後,只能阻止其餘線程得到同一個鎖。之因此每一個對象都有一個內置鎖,是爲了免去顯式地建立鎖對象。對象
因此synchronized只是一個內置鎖的加鎖機制,當某個方法加上synchronized關鍵字後,就代表要得到該內置鎖才能執行,並不能阻止其餘線程訪問不須要得到該內置鎖的方法。
類鎖的修飾(靜態)方法和代碼塊:
public class TestSynchronized { public void test1() { synchronized (TestSynchronized.class) { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } } public static synchronized void test2() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); Thread test1 = new Thread(new Runnable() { public void run() { myt2.test1(); } }, "test1"); Thread test2 = new Thread(new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2"); test1.start(); test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }
1.test1 : 4 2.test1 : 3 3.test1 : 2 4.test1 : 1 5.test1 : 0 6.test2 : 4 7.test2 : 3 8.test2 : 2 9.test2 : 1 10.test2 : 0
其實,類鎖修飾方法和代碼塊的效果和對象鎖是同樣的,由於類鎖只是一個抽象出來的概念,只是爲了區別靜態方法的特色,由於靜態方法是全部對象實例共用的,因此對應着synchronized修飾的靜態方法的鎖也是惟一的,因此抽象出來個類鎖。其實這裏的重點在下面這塊代碼,synchronized同時修飾靜態和非靜態方法
public class TestSynchronized { public synchronized void test1() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static synchronized void test2() { int i = 5; while (i-- > 0) { System.out.println(Thread.currentThread().getName() + " : " + i); try { Thread.sleep(500); } catch (InterruptedException ie) { } } } public static void main(String[] args) { final TestSynchronized myt2 = new TestSynchronized(); Thread test1 = new Thread(new Runnable() { public void run() { myt2.test1(); } }, "test1"); Thread test2 = new Thread(new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2"); test1.start(); test2.start(); // TestRunnable tr=new TestRunnable(); // Thread test3=new Thread(tr); // test3.start(); } }
1.test1 : 4 2.test2 : 4 3.test1 : 3 4.test2 : 3 5.test2 : 2 6.test1 : 2 7.test2 : 1 8.test1 : 1 9.test1 : 0 10.test2 : 0
上面代碼synchronized同時修飾靜態方法和實例方法,可是運行結果是交替進行的,這證實了類鎖和對象鎖是兩個不同的鎖,控制着不一樣的區域,它們是互不干擾的。一樣,線程得到對象鎖的同時,也能夠得到該類鎖,即同時得到兩個鎖,這是容許的。
到這裏,對synchronized的用法已經有了必定的瞭解。這時有一個疑問,既然有了synchronized修飾方法的同步方式,爲何還須要synchronized修飾同步代碼塊的方式呢?而這個問題也是synchronized的缺陷所在
synchronized的缺陷:當某個線程進入同步方法得到對象鎖,那麼其餘線程訪問這裏對象的同步方法時,必須等待或者阻塞,這對高併發的系統是致命的,這很容易致使系統的崩潰。若是某個線程在同步方法裏面發生了死循環,那麼它就永遠不會釋放這個對象鎖,那麼其餘線程就要永遠的等待。這是一個致命的問題。
固然同步方法和同步代碼塊都會有這樣的缺陷,只要用了synchronized關鍵字就會有這樣的風險和缺陷。既然避免不了這種缺陷,那麼就應該將風險降到最低。這也是同步代碼塊在某種狀況下要優於同步方法的方面。例如在某個類的方法裏面:這個類裏面聲明瞭一個對象實例,SynObject so=new SynObject();在某個方法裏面調用了這個實例的方法so.testsy();可是調用這個方法須要進行同步,不能同時有多個線程同時執行調用這個方法。
這時若是直接用synchronized修飾調用了so.testsy();代碼的方法,那麼當某個線程進入了這個方法以後,這個對象其餘同步方法都不能給其餘線程訪問了。假如這個方法須要執行的時間很長,那麼其餘線程會一直阻塞,影響到系統的性能。
若是這時用synchronized來修飾代碼塊:synchronized(so){so.testsy();},那麼這個方法加鎖的對象是so這個對象,跟執行這行代碼的對象沒有關係,當一個線程執行這個方法時,這對其餘同步方法時沒有影響的,由於他們持有的鎖都徹底不同。
不過這裏還有一種特例,就是上面演示的第一個例子,對象鎖synchronized同時修飾方法和代碼塊,這時也能夠體現到同步代碼塊的優越性,若是test1方法同步代碼塊後面有很是多沒有同步的代碼,並且有一個100000的循環,這致使test1方法會執行時間很是長,那麼若是直接用synchronized修飾方法,那麼在方法沒執行完以前,其餘線程是不能夠訪問test2方法的,可是若是用了同步代碼塊,那麼當退出代碼塊時就已經釋放了對象鎖,當線程還在執行test1的那個100000的循環時,其餘線程就已經能夠訪問test2方法了。這就讓阻塞的機會或者線程更少。讓系統的性能更優越。
一個類的對象鎖和另外一個類的對象鎖是沒有關聯的,當一個線程得到A類的對象鎖時,它同時也能夠得到B類的對象鎖。
可能上面只有理論和代碼,對剛接觸的人比較難理解,下面舉一個例子,
打個比方:一個object就像一個大房子,大門永遠打開。房子裏有 不少房間(也就是方法)。
這些房間有上鎖的(synchronized方法), 和不上鎖之分(普通方法)。房門口放着一把鑰匙(key),這把鑰匙能夠打開全部上鎖的房間。
另外我把全部想調用該對象方法的線程比喻成想進入這房子某個 房間的人。全部的東西就這麼多了,下面咱們看看這些東西之間如何做用的。
在此咱們先來明確一下咱們的前提條件。該對象至少有一個synchronized方法,不然這個key還有啥意義。固然也就不會有咱們的這個主題了。
一我的想進入某間上了鎖的房間,他來到房子門口,看見鑰匙在那兒(說明暫時尚未其餘人要使用上鎖的 房間)。因而他走上去拿到了鑰匙,而且按照本身 的計劃使用那些房間。注意一點,他每次使用完一次上鎖的房間後會立刻把鑰匙還回去。即便他要連續使用兩間上鎖的房間,中間他也要把鑰匙還回去,再取回來。
所以,普通狀況下鑰匙的使用原則是:「隨用隨借,用完即還。」
這時其餘人能夠不受限制的使用那些不上鎖的房間,一我的用一間能夠,兩我的用一間也能夠,沒限制。可是若是當某我的想要進入上鎖的房間,他就要跑到大門口去看看了。有鑰匙固然拿了就走,沒有的話,就只能等了。
要是不少人在等這把鑰匙,等鑰匙還回來之後,誰會優先獲得鑰匙?Not guaranteed。象前面例子裏那個想連續使用兩個上鎖房間的傢伙,他中間還鑰匙的時候若是還有其餘人在等鑰匙,那麼沒有任何保證這傢伙能再次拿到。 (JAVA規範在不少地方都明確說明不保證,像Thread.sleep()休息後多久會返回運行,相同優先權的線程那個首先被執行,當要訪問對象的鎖被 釋放後處於等待池的多個線程哪一個會優先獲得,等等。我想最終的決定權是在JVM,之因此不保證,就是由於JVM在作出上述決定的時候,毫不是簡簡單單根據 一個條件來作出判斷,而是根據不少條。而因爲判斷條件太多,若是說出來可能會影響JAVA的推廣,也多是由於知識產權保護的緣由吧。SUN給了個不保證 就混過去了。無可厚非。但我相信這些不肯定,並不是徹底不肯定。由於計算機這東西自己就是按指令運行的。即便看起來很隨機的現象,其實都是有規律可尋。學過 計算機的都知道,計算機裏隨機數的學名是僞隨機數,是人運用必定的方法寫出來的,看上去隨機罷了。另外,或許是由於要想弄的確太費事,也沒多大意義,所 以不肯定就不肯定了吧。)
再來看看同步代碼塊。和同步方法有小小的不一樣。
1.從尺寸上講,同步代碼塊比同步方法小。你能夠把同步代碼塊當作是沒上鎖房間裏的一塊用帶鎖的屏風隔開的空間。
2.同步代碼塊還能夠人爲的指定得到某個其它對象的key。就像是指定用哪一把鑰匙才能開這個屏風的鎖,你能夠用本房的鑰匙;你也能夠指定用另外一個房子的鑰匙才能開,這樣的話,你要跑到另外一棟房子那兒把那個鑰匙拿來,並用那個房子的鑰匙來打開這個房子的帶鎖的屏風。
記住你得到的那另外一棟房子的鑰匙,並不影響其餘人進入那棟房子沒有鎖的房間。
爲何要使用同步代碼塊呢?我想應該是這樣的:首先對程序來說同步的部分很影響運行效率,而一個方法一般是先建立一些局部變量,再對這些變量作一些 操做,如運算,顯示等等;而同步所覆蓋的代碼越多,對效率的影響就越嚴重。所以咱們一般儘可能縮小其影響範圍。
如何作?同步代碼塊。咱們只把一個方法中該同 步的地方同步,好比運算。
另外,同步代碼塊能夠指定鑰匙這一特色有個額外的好處,是能夠在必定時期內霸佔某個對象的key。還記得前面說過普通狀況下鑰匙的使用原則嗎。如今不是普通狀況了。你所取得的那把鑰匙不是永遠不還,而是在退出同步代碼塊時才還。
還用前面那個想連續用兩個上鎖房間的傢伙打比方。怎樣才能在用完一間之後,繼續使用另外一間呢。用同步代碼塊吧。先建立另一個線程,作一個同步代碼 塊,把那個代碼塊的鎖指向這個房子的鑰匙。而後啓動那個線程。只要你能在進入那個代碼塊時抓到這房子的鑰匙,你就能夠一直保留到退出那個代碼塊。也就是說 你甚至能夠對本房內全部上鎖的房間遍歷,甚至再sleep(10*60*1000),而房門口卻還有1000個線程在等這把鑰匙呢。很過癮吧。
synchronized做用於靜態方法和非靜態方法的區別:
1
2
3
4
5
6
7
8
9
|
/*
* 非靜態方法:
* 給對象加鎖(能夠理解爲給這個對象的內存上鎖,注意 只是這塊內存,其餘同類對象都會有各自的內存鎖),這時候
* 在其餘一個以上線程中執行該對象的這個同步方法(注意:是該對象)就會產生互斥
* 靜態方法:
* 至關於在類上加鎖(*.class 位於代碼區,靜態方法位於靜態區域,這個類產生的對象公用這個靜態方法,因此這塊
* 內存,N個對象來競爭), 這時候,只要是這個類產生的對象,在調用這個靜態方法時都會產生互斥
*/
|