JAVA并发编程3_线程同步之synchronized关键字
在上一篇博客里講解了JAVA的線程的內存模型,見:JAVA并發編程2_線程安全&內存模型,接著上一篇提到的問題解決多線程共享資源的情況下的線程安全問題。
不安全線程分析
public class Test implements Runnable {private int i = 0;private int getNext() {return i++;}@Overridepublic void run() { // synchronizedwhile (true) {synchronized(this){if(i<10){System.out.println(getNext());}elsebreak;}}}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();} }與之前的代碼的區別在于run方法被synchronized關鍵字修飾。
根據上一篇博客的分析:多線程在訪問共享資源的時候由于CPU輪流給每個任務分配其占用的時間,而CPU的調度是隨機的,因此就會發生某個線程正在訪問該變量的時候CPU卻將時間片分發給了其他的線程,這樣就會發生這樣的現象:一個線程從主內存讀取到某個變量的值還沒來得及修改(或者修改后刷新主內存),另一個線程就獲得了CPU的執行權,也從主內存讀取改變量的值。當CPU執行權再次回到第一個線程的時候會接著之前的中斷處執行(修改變量等),執行權回到第二個線程時卻不能看到第一個線程中改變了的值。歸結起來就是說違背了線程內存的可見性。避免上看起來產生第一種輸出的可能順序如下圖所示(實際上可能的情況非常多,因為i++不是單個的原子操作):
i++對應下面的JVM指令,因此在期間另一個線程都可能會修改這個變量。
4: aload_0
5: iconst_0
6: putfield????? #2????????????????? // Field i:I
為了體現內存的可見性,synchronized關鍵字能使它保護的代碼以串行的方式來訪問(同一時刻只能由一個線程訪問)。保證某個線程以一種可預測的方式來查看另一個線程的執行結果。
線程同步
JAVA提供的鎖機制包括同步代碼塊和同步方法。
每個Java對象都可以用做一個實現同步的鎖,這些所成為內置鎖(Intrinsic Lock)或監視器鎖(Monitor Lock),一個線程進入同步帶嗎快之前會自動獲得鎖,并且推出同步帶嗎快時自動釋放鎖。獲得內置鎖的位移途徑就是進入由這個鎖保護的同步代碼塊或方法并且該鎖還未被其他線程獲得。
Java內置鎖相當于互斥體(互斥鎖),意味著最多有一個線程持有這種鎖。當線程A嘗試獲取一個由線程B持有的鎖時,線程A必須等待或者阻塞,知道線程B釋放這個鎖,如果線程B永遠不釋放鎖,那么線程A將永遠等待下去。
每次只能有一個線程執行內置鎖保護的代碼塊,因此這個鎖保護的同步代碼塊會以原子方式執行,多個線程在執行該代碼塊時也不會相互干擾。
原子性的含義:一組語句作為一個不可分割的單元被執行。任何一個執行同步代碼塊的線程,都不可能看到有其他線程正在執行由同一個鎖保護的同步代碼塊。
千萬注意:并不是說synchronized代碼塊或者synchronized方法是不可分割的整體,是原子的,因為,顯然使用不同鎖的話之間不存在互斥關系。
買票例子的引入
下面是模擬火車站賣票的程序,理論上是要將編號為1-10的票賣按照由大到小順序賣出去,結果用兩個窗口(線程)賣就出現了這樣的結果,有些編號的票賣了兩次,有些沒賣出去,并且還有編號為0的票賣了出去。顯然結果錯誤的。
public class Test implements Runnable {private int i = 10;private void sale(){while (true) {if (i >0){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread() + "正在賣第" + i + "張票");i--;} else break;}}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();} }出現這種結果的原因就是沒有對多個線程共同訪問的資源進行同步加鎖。下面我們對其進行線程同步,達到想要的效果:
synchronized代碼塊:
synchronized (lock){//同步的代碼}lock必須是一個引用類型的變量。
使用synchronized同步代碼塊:
public class Test implements Runnable {private int i = 10;private void sale(){Object o = new Object();while (true) {synchronized(o){if (i >0){System.out.println(Thread.currentThread() + "正在賣第" + i + "張票");i--;}else break; }}}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();} }咦?使用了同步代碼塊了怎么結果還是不對呢??我們先看正確的同步:
public class Test implements Runnable {private int i = 10;Object o = new Object();// 通常使用:/*static*/ byte[] lock = new byte[0];private void sale(){while (true) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}synchronized(o){if (i >0){System.out.println(Thread.currentThread() + "正在賣第" + i + "張票");i--;}elsebreak;}}}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();}這里線程同步的原理是怎樣的呢?因為任何一個Java對象都可以作為一個同步鎖,上面代碼的對象o就是一個同步鎖。
一個線程執行到synchronized代碼塊,線程嘗試給同步鎖上鎖,如果同步鎖已經被鎖,則線程不能獲取到鎖,線程就被阻塞;如果同步鎖沒被鎖,則線程將同步鎖上鎖,并且持有該鎖,然后執行代碼塊;代碼塊正常執行結束或者非正常結束,同步鎖都將解鎖。
所以線程執行同步代碼塊時,持有該同步鎖。其他線程不能獲取鎖,就不能進入同步代碼塊(前提是使用同一把鎖),只能等待鎖被釋放。
這時候回頭看上上段代碼中的同步代碼塊,由于兩個線程使用的鎖是不一樣的(創建了兩個對象),因此,就算線程A在執行同步代碼塊,當線程2獲得CPU執行權時,檢查到這個鎖并未被其他線程鎖定,因此不具有互斥性,不能達到線程同步的效果。
同步方法
將synchronized作為關鍵字修飾類的某個方法,這樣該方法就變成了同步方法。
直接將sale函數改為synchronized方法的結果是雖然賣票不會亂序,但是只有一個線程在賣票。所以稍微做些調整:
public class Test implements Runnable {private int i = 10;private void sale(){while (true) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}f();}}private synchronized void f(){if (i >0){System.out.println(Thread.currentThread() + "正在賣第" + i + "張票");i--;}elsereturn;}@Overridepublic void run() {sale();}public static void main(String[] args) {Test t = new Test();Thread t1 = new Thread(t);Thread t2 = new Thread(t);t1.start();t2.start();Thread.yield();} }這時候的鎖是哪個對象呢?
當修飾的方法是類方法時同步鎖是該類對應的Class對象;
當修飾普通方法時,該同步鎖是當前對象即this。
體會:不要濫用synchronized方法
在平時的編程中為了達到線程同步的目的,在不經認真思考的情況下,經常發生synchronized關鍵字的濫用,歸根結底是沒有理解同步的原理本質。
看下面的代碼:
public class Test implements Runnable{ @Override public void run() { f();} public synchronized void f(){ System.out.println(this);}public static void main(String[] args) { Test t1=new Test(); Test t2=new Test(); // f()里面的代碼無法達到同步的目的new Thread(t1).start(); new Thread(t2).start(); } } //Output //Test@2073b879 //Test@d542094根據打印的結果也可以看出來函數f()是無法同步的,因為這兩個線程使用了兩個同步鎖。這就告訴我們,并不要看到一個方法是synchronized的就想當然的認為它是同步方法就在不同的線程里隨便調用。
注:上面的代碼里面多次使用到了Thread.sleep(long)方法,是讓當前線程睡眠一會,這個方法會讓當前線程放棄CPU的執行權,處于Time Waiting狀態,CPU不在為其分配時間片。由于機器的不同可能不容易出現我們期望的線程切換,目這樣做就可以強制的讓線程切換。
另外,在synchronized代碼里面使用sleep無效。因為該線程sleep后CPU不在為其分配時間片,但是這個時候線程已經拿到了同步鎖,即使睡到天荒地老,它也不會把同步鎖交出去,別的線程得到了CPU執行卻卻苦于沒有同步鎖而被拒之門外。后面學習線程的狀態會講到這些。
會寫代碼不一定理解了,理解了不一定能給別人講清楚。想把一個東西用文字表述清楚真的挺不容易。
轉載于:https://www.cnblogs.com/qhyuan1992/p/5385310.html
總結
以上是生活随笔為你收集整理的JAVA并发编程3_线程同步之synchronized关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法:合并排序(Merge Sort)
- 下一篇: 翻译题(map使用)