1. 程式人生 > >【Java並發編程實戰】—–synchronized

【Java並發編程實戰】—–synchronized

tex family stat ring 抽屜 java虛擬機 其它 什麽 實際應用

在我們的實際應用其中可能常常會遇到這樣一個場景:多個線程讀或者、寫相同的數據,訪問相同的文件等等。對於這樣的情況假設我們不加以控制,是非常easy導致錯誤的。

在java中,為了解決問題,引入臨界區概念。所謂臨界區是指一個訪問共用資源的程序片段,而這些共用資源又無法同一時候被多個線程訪問。

在java中為了實現臨界區提供了同步機制。當一個線程試圖訪問一個臨界區時,他將使用一種同步機制來查看是不是已經有其它線程進入臨界區。

假設沒有則他就能夠進入臨界區,否則他就會被同步機制掛起,指定進入的線程離開這個臨界區。

臨界區規定:每次僅僅準許一個進程進入臨界區,進入後不同意其它進程進入。調度法則為(百度百科):

1、假設有若幹進程要求進入空暇的臨界區,一次僅同意一個進程進入。

2、不論什麽時候,處於臨界區內的進程不可多於一個。

如已有進程進入自己的臨界區,則其它全部試圖進入臨界區的進程必須等待。

3、進入臨界區的進程要在有限時間內退出,以便其它進程能及時進入自己的臨界區。

4、假設進程不能進入自己的臨界區,則應讓出CPU,避免進程出現“忙等”現象。

以下介紹使用synchronizedkeyword來實現同步機制。

一、synchronizedkeyword

1.1、簡單介紹

synchronized,我們謂之鎖。主要用來給方法、代碼塊加鎖。

當某個方法或者代碼塊使用synchronized時,那麽在同一時刻至多僅有有一個線程在運行該段代碼。當有多個線程訪問同一對象的加鎖方法/代碼塊時,同一時間僅僅有一個線程在運行,其余線程必須要等待當前線程運行完之後才幹運行該代碼段。可是。其余線程是能夠訪問該對象中的非加鎖代碼塊的。

synchronized主要包含兩種方法:synchronized 方法、synchronized 塊。

1.2、synchronized 方法

通過在方法聲明中增加 synchronizedkeyword來聲明 synchronized 方法。

如:

public synchronized void getResult();

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

事實上synchronized方法是存在缺陷的,假設我們將一個非常大的方法聲明為synchronized將會大大影響效率的。假設多個線程在訪問一個synchronized方法,那麽同一時刻僅僅有一個線程在運行該方法。而其它線程都必須等待,可是假設該方法沒有使用synchronized。則全部線程能夠在同一時刻運行它,降低了運行的總時間。所以假設我們知道一個方法不會被多個線程運行到或者說不存在資源共享的問題,則不須要使用synchronizedkeyword。可是假設一定要使用synchronizedkeyword,那麽我們能夠synchronized代碼塊來替換synchronized方法。

1.3、synchronized 塊

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

synchronized(object) {  
    //同意訪問控制的代碼  
}

假設我們須要以這樣的方式來使用synchronizedkeyword,那麽必須要通過一個對象引用來作為參數,通常這個參數我們常使用為this.

synchronized (this) {
    //同意訪問控制的代碼 
}

對於synchronized(this)有例如以下理解:

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

2、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,還有一個線程仍然能夠訪問object中的非synchronized(this)同步代碼塊。

3、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其它線程對object中全部其它synchronized(this)同步代碼塊得訪問將被堵塞。

4、第三個樣例相同適用其它同步代碼塊。也就是說,當一個線程訪問object的一個synchronized(this)同步代碼塊時。它就獲得了這個object的對象鎖。

結果。其它線程對該object對象全部同步代碼部分的訪問都將被臨時堵塞。

5、以上規則對其它對象鎖相同適用

http://freewxy.iteye.com/blog/978159,這篇博客使用實例對上面四點進行了較為具體的說明,這裏就不多闡述了。

http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html這篇博客對synchronized的使用舉了一個非常不錯的樣例(拿鑰匙進房間)。這裏由於篇幅問題LZ就不多闡述了。以下我們來刨刨synchronized略微高級點的東西。

1.4、進階

在java多線程中存在一個“先來後到”的原則,也就是說誰先搶到鑰匙,誰先用。

我們知道為避免資源競爭產生問題,java使用同步機制來避免,而同步機制是使用鎖概念來控制的。那麽在Java程序其中,鎖是怎樣體現的呢?這裏我們須要弄清楚兩個概念:

什麽是鎖?

什麽是鎖?在日常生活中。它就是一個加在門、箱子、抽屜等物體上的封緘器,防止別人偷窺或者偷盜,起到一個保護的作用。

在java中相同如此。鎖對對象起到一個保護的作用,一個線程假設獨占了某個資源。那麽其它的線程別想用,想用?等我用完再說吧!

在java程序運行環境中。JVM須要對兩類線程共享的數據進行協調:

1、保存在堆中的實例變量

2、保存在方法區中的類變量。

在java虛擬機中,每個對象和類在邏輯上都是和一個監視器相關聯的。對於對象來說。相關聯的監視器保護對象的實例變量。 對於類來說。監視器保護類的類變量。假設一個對象沒有實例變量,或者說一個類沒有變量。相關聯的監視器就什麽也不監視。

為了實現監視器的排他性監視能力,java虛擬機為每個對象和類都關聯一個鎖。

代表不論什麽時候僅僅同意一個線程擁有的特權。線程訪問實例變量或者類變量不需鎖。 假設某個線程獲取了鎖。那麽在它釋放該鎖之前其它線程是不可能獲取相同鎖的。一個線程能夠多次對同一個對象上鎖。對於每個對象,java虛擬機維護一個加鎖計數器。線程每獲得一次該對象,計數器就加1,每釋放一次。計數器就減 1,當計數器值為0時。鎖就被全然釋放了。
java編程人員不須要自己動手加鎖,對象鎖是java虛擬機內部使用的。在java程序中。僅僅須要使用synchronized塊或者synchronized方法就能夠標誌一個監視區域。

當每次進入一個監視區域時,java 虛擬機都會自己主動鎖上對象或者類。

(摘自java的鎖機制)。

鎖的是什麽?

在這個問題之前我們必須要明白一點:不管synchronizedkeyword加在方法上還是對象上,它取得的鎖都是對象

在java中每個對象都能夠作為鎖,它主要體如今以下三個方面:

  • 對於同步方法。鎖是當前實例對象。
  • 對於同步方法塊,鎖是Synchonized括號中配置的對象。

  • 對於靜態同步方法,鎖是當前對象的Class對象。

    首先我們先看以下樣例:

    public class ThreadTest_01 implements Runnable{
    
        @Override
        public synchronized void run() {
            for(int i = 0 ; i < 3 ; i++){
                System.out.println(Thread.currentThread().getName() + "run......");
            }
        }
        
        public static void main(String[] args) {
            for(int i = 0 ; i < 5 ; i++){
                new Thread(new ThreadTest_01(),"Thread_" + i).start();
            }
        }
    }

    部分運行結果:

    Thread_2run......
    Thread_2run......
    Thread_4run......
    Thread_4run......
    Thread_3run......
    Thread_3run......
    Thread_3run......
    Thread_2run......
    Thread_4run......

    這個結果與我們預期的結果有點不同(這些線程在這裏亂跑),照理來說,run方法加上synchronizedkeyword後。會產生同步效果,這些線程應該是一個接著一個運行run方法的。在上面LZ提到,一個成員方法加上synchronizedkeyword後,實際上就是給這個成員方法加上鎖,具體點就是以這個成員方法所在的對象本身作為對象鎖。可是在這個實例其中我們一共new了10個ThreadTest對象,那個每個線程都會持有自己線程對象的對象鎖,這必然不能產生同步的效果。

    所以:假設要對這些線程進行同步,那麽這些線程所持有的對象鎖應當是共享且唯一的!

    這個時候synchronized鎖住的是那個對象?它鎖住的就是調用這個同步方法對象。就是說threadTest這個對象在不同線程中運行同步方法,就會形成相互排斥。

    達到同步的效果。所以將上面的new Thread(new ThreadTest_01(),”Thread_” + i).start(); 改動為new Thread(threadTest,”Thread_” + i).start();就能夠了。

    對於同步方法,鎖是當前實例對象。

    上面實例是使用synchronized方法。我們在看看synchronized代碼塊:

    public class ThreadTest_02 extends Thread{
    
        private String lock ;
        private String name;
        
        public ThreadTest_02(String name,String lock){
            this.name = name;
            this.lock = lock;
        }
        
        @Override
        public void run() {
            synchronized (lock) {
                for(int i = 0 ; i < 3 ; i++){
                    System.out.println(name + " run......");
                }
            }
        }
        
        public static void main(String[] args) {
            String lock  = new String("test");
            for(int i = 0 ; i < 5 ; i++){
                new ThreadTest_02("ThreadTest_" + i,lock).start();
            }
        }
    }

    運行結果:

    ThreadTest_0 run......
    ThreadTest_0 run......
    ThreadTest_0 run......
    ThreadTest_1 run......
    ThreadTest_1 run......
    ThreadTest_1 run......
    ThreadTest_4 run......
    ThreadTest_4 run......
    ThreadTest_4 run......
    ThreadTest_3 run......
    ThreadTest_3 run......
    ThreadTest_3 run......
    ThreadTest_2 run......
    ThreadTest_2 run......
    ThreadTest_2 run......

    在main方法中我們創建了一個String對象lock。並將這個對象賦予每個ThreadTest2線程對象的私有變量lock。

    我們知道java中存在一個字符串池,那麽這些線程的lock私有變量實際上指向的是堆內存中的同一個區域,即存放main函數中的lock變量的區域,所以對象鎖是唯一且共享的。線程同步!。

    在這裏synchronized鎖住的就是lock這個String對象。

    對於同步方法塊,鎖是Synchonized括號中配置的對象。

    public class ThreadTest_03 extends Thread{
    
        public synchronized static void test(){
            for(int i = 0 ; i < 3 ; i++){
                System.out.println(Thread.currentThread().getName() + " run......");
            }
        }
        
        @Override
        public void run() {
            test();
        }
    
        public static void main(String[] args) {
            for(int i = 0 ; i < 5 ; i++){
                new ThreadTest_03().start();
            }
        }
    }

    運行結果:

    Thread-0 run......
    Thread-0 run......
    Thread-0 run......
    Thread-4 run......
    Thread-4 run......
    Thread-4 run......
    Thread-1 run......
    Thread-1 run......
    Thread-1 run......
    Thread-2 run......
    Thread-2 run......
    Thread-2 run......
    Thread-3 run......
    Thread-3 run......
    Thread-3 run......

    在這個實例中,run方法使用的是一個同步方法,並且是static的同步方法,那麽這裏synchronized鎖的又是什麽呢?我們知道static超脫於對象之外。它屬於類級別的。

    所以,對象鎖就是該靜態放發所在的類的Class實例。由於在JVM中,全部被載入的類都有唯一的類對象,在該實例其中就是唯一的 ThreadTest_03.class對象。不管我們創建了該類的多少實例,可是它的類實例仍然是一個!所以對象鎖是唯一且共享的。線程同步!!

    對於靜態同步方法,鎖是當前對象的Class對象。

    假設一個類中定義了一個synchronized的static函數A,也定義了一個synchronized的instance函數B。那麽這個類的同一對象Obj,在多線程中分別訪問A和B兩個方法時,不會構成同步,由於它們的鎖都不一樣。A方法的鎖是Obj這個對象,而B的鎖是Obj所屬的那個Class。

    鎖的升級

    java中鎖一共同擁有四種狀態,無鎖狀態。偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。

    鎖能夠升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這樣的鎖升級卻不能降級的策略。目的是為了提高獲得鎖和釋放鎖的效率。以下主要部分主要是對博客:聊聊並發(二)Java SE1.6中的Synchronized的總結。

    鎖自旋

    我們知道在當某個線程在進入同步方法/代碼塊時若發現該同步方法/代碼塊被其它如今所占,則它就要等待,進入堵塞狀態,這個過程性能是低下的。

    在遇到鎖的爭用也許等待事,線程能夠不那麽著急進入堵塞狀態。而是等一等,看看鎖是不是立即就釋放了,這就是鎖自旋。鎖自旋在一定程度上能夠對線程進行優化處理。

    偏向鎖

    偏向鎖主要為了解決在沒有競爭情況下鎖的性能問題。在大多數情況下鎖鎖不僅不存在多線程競爭,並且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。

    當某個線程獲得鎖的情況,該線程是能夠多次鎖住該對象,可是每次運行這樣的操作都會由於CAS(CPU的Compare-And-Swap指令)操作而造成一些開銷消耗性能,為了降低這樣的開銷。這個鎖會偏向於第一個獲得它的線程。假設在接下來的運行過程中。該鎖沒有被其它的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。

    當有其它線程在嘗試著競爭偏向鎖時,持有偏向鎖的線程就會釋放鎖。

    鎖膨脹

    多個或多次調用粒度太小的鎖。進行加鎖解鎖的消耗,反而還不如一次大粒度的鎖調用來得高效。

    輕量級鎖

    輕量級鎖能提升程序同步性能的根據是“對於絕大部分的鎖,在整個同步周期內都是不存在競爭的”,這是一個經驗數據。輕量級鎖在當前線程的棧幀中建立一個名為鎖記錄的空間,用於存儲鎖對象眼下的指向和狀態。假設沒有競爭。輕量級鎖使用CAS操作避免了使用相互排斥量的開銷,但假設存在鎖競爭,除了相互排斥量的開銷外。還額外發生了CAS操作,因此在有競爭的情況下。輕量級鎖會比傳統的重量級鎖更慢。

    1.5參考資料

    1、《Java 7 並發編程實戰手冊》

    2、java synchronized具體解釋

    3、聊聊並發(二)Java SE1.6中的Synchronized

    4、java的鎖機制

    5、Java的無鎖編程和鎖優化

  • 【Java並發編程實戰】—–synchronized