JAVA多線程基礎學習二:synchronized

本篇主要介紹Java多線程中的同步,也就是如何在Java語言中寫出線程安全的程序,如何在Java語言中解決非線程安全的相關問題,沒錯就是使用synchronized.

一、如何解決線程安全問題?

一般來說,是如何解決線程安全問題的呢?
基本上所有的併發模式在解決線程安全問題時,都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。本文主要講述synchronized的使用方法,Lock的使用方法後面再說。

二、synchronized關鍵字

1.簡介

synchronized,我們謂之鎖,主要用來給方法、代碼塊加鎖。當某個方法或者代碼塊使用synchronized時,那麼在同一時刻至多僅有有一個線程在執行該段代碼。當有多個線程訪問同一對象的加鎖方法/代碼塊時,同一時間只有一個線程在執行,其餘線程必須要等待當前線程執行完之後才能執行該代碼段。但是,其餘線程是可以訪問該對象中的非加鎖代碼塊的。
synchronized主要包括兩種方法:synchronized 方法、synchronized 塊。

2.synchronized 方法

通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。如:

public synchronized void getResult();

synchronized方法控制對類成員變量的訪問。它是如何來避免類成員變量的訪問控制呢?我們知道方法使用了synchronized關鍵字表明該方法已加鎖,在任一線程在訪問改方法時都必須要判斷該方法是否有其他線程在“獨佔”。每個類實例對應一個把鎖,每個synchronized方法都必須調用該方法的類實例的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,被阻塞的線程方能獲得該鎖。

其實synchronized方法是存在缺陷的,如果我們將一個很大的方法聲明爲synchronized將會大大影響效率的。如果多個線程在訪問一個synchronized方法,那麼同一時刻只有一個線程在執行該方法,而其他線程都必須等待,但是如果該方法沒有使用synchronized,則所有線程可以在同一時刻執行它,減少了執行的總時間。所以如果我們知道一個方法不會被多個線程執行到或者說不存在資源共享的問題,則不需要使用synchronized關鍵字。但是如果一定要使用synchronized關鍵字,那麼我們可以synchronized代碼塊來替換synchronized方法。

3.synchronized 塊

synchronized代碼塊所起到的作用和synchronized方法一樣,只不過它使臨界區變的儘可能短了,換句話說:它只把需要的共享數據保護起來,其餘的長代碼塊留出此操作。語法如下:

synchronized(object) {  
    //允許訪問控制的代碼  
}

如果我們需要以這種方式來使用synchronized關鍵字,那麼必須要通過一個對象引用來作爲參數,通常這個參數我們常使用爲this.

4.synchronized的使用

synchronized代碼塊,被修飾的代碼成爲同步語句塊,其作用的範圍是調用這個代碼塊的對象,我們在用synchronized關鍵字的時候,能縮小代碼段的範圍就儘量縮小,能在代碼段上加同步就不要再整個方法上加同步。這叫減小鎖的粒度,使代碼更大程度的併發。

synchronized方法,被修飾的方法成爲同步方法,其作用範圍是整個方法,作用對象是調用這個方法的對象。

synchronized靜態方法,修飾一個static靜態方法,其作用範圍是整個靜態方法,作用對象是這個類的所有對象。

synchronized類,其作用範圍是Synchronized後面括號括起來的部分synchronized(className.class),作用的對象是這個類的所有對象。

synchronized(),()中是鎖住的對象, synchronized(this)鎖住的只是對象本身,同一個類的不同對象調用的synchronized方法並不會被鎖住,而synchronized(className.class)實現了全局鎖的功能,所有這個類的對象調用這個方法都受到鎖的影響,此外()中還可以添加一個具體的對象,實現給具體對象加鎖。

5.synchronized注意事項

當兩個併發線程訪問同一個對象中的synchronized代碼塊時,在同一時刻只能有一個線程得到執行,另一個線程受阻塞,必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。兩個線程間是互斥的,因爲在執行synchronized代碼塊時會鎖定當前的對象,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。

當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。(兩個線程使用的是同一個對象)

當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞(同上,兩個線程使用的是同一個對象)。
下面通過代碼來實現:

1)當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。

public class Thread1 implements Runnable {  
     public void run() {  
          synchronized(this) {  
               for (int i = 0; i < 5; i++) {  
                    System.out.println(Thread.currentThread().getName() + " synchronized loop " + i);  
               }  
          }  
     }  
     public static void main(String[] args) {  
          Thread1 t1 = new Thread1();  
          Thread ta = new Thread(t1, "A");  
          Thread tb = new Thread(t1, "B");  
          ta.start();  
          tb.start();  
     } 
}

輸出結果:

A synchronized loop 0  
A synchronized loop 1  
A synchronized loop 2  
A synchronized loop 3  
A synchronized loop 4  
B synchronized loop 0  
B synchronized loop 1  
B synchronized loop 2  
B synchronized loop 3  
B synchronized loop 4

2)然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          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 Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}

輸出結果:

t1 : 4  
t2 : 4  
t1 : 3  
t2 : 3  
t1 : 2  
t2 : 2  
t1 : 1  
t2 : 1  
t1 : 0  
t2 : 0

3)尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。

//修改Thread2.m4t2()方法:  
     public void m4t2() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }

     }

輸出結果:

t1 : 4  
t1 : 3  
t1 : 2  
t1 : 1  
t1 : 0  
t2 : 4  
t2 : 3  
t2 : 2  
t2 : 1  
t2 : 0

4)第三個例子同樣適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。

//修改Thread2.m4t2()方法如下:

    public synchronized void m4t2() {  
         int i = 5;  
         while( i-- > 0) {  
              System.out.println(Thread.currentThread().getName() + " : " + i);  
              try {  
                   Thread.sleep(500);  
              } catch (InterruptedException ie) {  
              }  
         }  
    }

輸出結果:

t1 : 4  
t1 : 3  
t1 : 2  
t1 : 1  
t1 : 0  
t2 : 4  
t2 : 3  
t2 : 2  
t2 : 1  
t2 : 0

5)每個類也會有一個鎖,它可以用來控制對static數據成員的併發訪問。
並且如果一個線程執行一個對象的非static synchronized方法,另外一個線程需要執行這個對象所屬類的static synchronized方法,此時不會發生互斥現象,因爲訪問static synchronized方法佔用的是類鎖,而訪問非static synchronized方法佔用的是對象鎖,所以不存在互斥現象。
代碼如下:

public class Test {
 
    public static void main(String[] args)  {
        final InsertData insertData = new InsertData();
        new Thread(){
            @Override
            public void run() {
                insertData.insert();
            }
        }.start(); 
        new Thread(){
            @Override
            public void run() {
                insertData.insert1();
            }
        }.start();
    }  
}
 
class InsertData { 
    public synchronized void insert(){
        System.out.println("執行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行insert完畢");
    }
     
    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}

輸出結果:

執行insert
執行insert1
執行insert1完畢
執行insert完畢

第一個線程裏面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。

三、面試題

當一個線程進入一個對象的synchronized方法A之後,其它線程是否可進入此對象的synchronized方法B?
答:不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。因爲非靜態方法上的synchronized修飾符要求執行方法時要獲得對象的鎖,如果已經進入A方法說明對象鎖已經被取走,那麼試圖進入B方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。

synchronized關鍵字的用法?
答:synchronized關鍵字可以將對象或者方法標記爲同步,以實現對對象和方法的互斥訪問,可以用synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時將synchronized作爲方法的修飾符。

簡述synchronized 和java.util.concurrent.locks.Lock的異同?
答:Lock是Java 5以後引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的所有功能;主要不同點:Lock有比synchronized更精確的線程語義和更好的性能,而且不強制性的要求一定要獲得鎖。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,並且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)

以上就是synchronized的概念和基本使用用法;但如想要更加深入分析synchronized的實現原理,則需要繼續攻關,本文僅僅是入門級,希望對你有幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章