Java併發編程:內置鎖 Synchronized

    在多線程編程中,線程安全問題是一個最爲關鍵的問題,其核心概念就在於正確性,即當多個線程訪問某一共享、可變數據時,始終都不會導致數據破壞以及其他不該出現的結果。而所有的併發模式在解決這個問題時,採用的方案都是序列化訪問臨界資源 。在 Java 中,提供了兩種方式來實現同步互斥訪問:synchronized 和 Lock。本文針對 synchronized 內置鎖 詳細討論了其在 Java 併發 中的應用,包括它的具體使用場景(同步方法、同步代碼塊、實例對象鎖 和 Class 對象鎖)、可重入性 和 注意事項。synchronized 使得在一段時間內只有一個任務可以運行這段代碼。因爲鎖語句產生了一種互相排斥的效果。這種機制常常稱爲互斥量(mute)

一. 線程安全問題

在單線程中不會出現線程安全問題,而在多線程編程中,有可能會出現同時訪問同一個 共享、可變資源 的情況,這種資源可以是:一個變量、一個對象、一個文件等。特別注意兩點,

  • 共享: 意味着該資源可以由多個線程同時訪問;
  • 可變: 意味着該資源可以在其生命週期內被修改。

     所以,當多個線程同時訪問這種資源的時候,就會存在一個問題:

     由於每個線程執行的過程是不可控的,所以需要採用同步機制來協同對對象可變狀態的訪問。 

現在是有四個線程在買票,當num=1的時候線程1,線程2,線程3,線程4都進來了,此時線程1,2,3,4的num=1都搶到cpu的執行時間片了,現在可能出現線程1的輸出Thread-0:1,線程2此時輸出Thread-1:0,線程3輸出Thread-2:-1;此時就出現了不安全的問題了;
package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  20:58 2018/5/31
 * @Description: 用買票的案例說明線程的安全問題
 */
class Ticket implements Runnable {

    private  int num=100;
    @Override
    public void run() {
        while (true) {
            if (num > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"......sale....."+num--);
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1=new Thread(ticket);
        Thread t2=new Thread(ticket);
        Thread t3=new Thread(ticket);
        Thread t4=new Thread(ticket);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

輸出的結果最後的幾條數據是:

Thread-0--sale---3
Thread-3--sale---2
Thread-2--sale---1
Thread-1--sale---0
Thread-0--sale----1
Thread-3--sale----2

Process finished with exit code 1


2. 線程安全問題的原因

    這其實就是一個線程安全問題,即多個線程同時訪問一個資源時,會導致程序運行結果並不是想看到的結果。這裏面,這個資源被稱爲:臨界資源。也就是說,當多個線程同時訪問臨界資源(一個對象,對象中的屬性,一個文件,一個數據庫等)時,就可能會產生線程安全問題。

  不過,當多個線程執行一個方法時,該方法內部的局部變量並不是臨界資源,因爲這些局部變量是在每個線程的私有棧中,因此不具有共享性,不會導致線程安全問題。

    1:多個線程在操作共享的數據

    2:操作共享的線程代碼有多條

3. 線程安全問題的解決方式

實際上,所有的併發模式在解決線程安全問題時,採用的方案都是 序列化訪問臨界資源 。即在同一時刻,只能有一個線程訪問臨界資源,也稱作 同步互斥訪問。換句話說,就是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其他線程繼續訪問。

  在 Java 中,提供了兩種方式來實現同步互斥訪問:synchronized 和 Lock。本文主要講述 synchronized 的使用方法

4. synchronized 同步方法或者同步塊

        在瞭解 synchronized 關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,即 能到達到互斥訪問目的的鎖。舉個簡單的例子,如果對臨界資源加上互斥鎖,當一個線程在訪問該臨界資源時,其他線程便只能等待。

  在 Java 中,可以使用 synchronized 關鍵字來標記一個方法或者代碼塊,當某個線程調用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執行完畢或者代碼塊執行完畢,這個線程纔會釋放該對象的鎖,其他線程才能執行這個方法或者代碼塊。

同步代碼塊的格式:

synchronized(對象){
     需要被同步的代碼;
}
package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  20:58 2018/5/31
 * @Description: 用買票的案例說明線程的安全問題
 */
class Ticket implements Runnable {

    private int num = 100;
    Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (num > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "......sale....." + num--);
                }
            }
        }
    }
}

public class TicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);
        Thread t4 = new Thread(ticket);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

當在某個線程中執行這段代碼塊,該線程會獲取對象lock的鎖,從而使得其他線程無法同時訪問該代碼塊。其中,lock 可以是 this,代表獲取當前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。特別地, 實例同步方法 與 synchronized(this)同步塊 是互斥的,因爲它們鎖的是同一個對象。但與 synchronized(非this)同步塊 是異步的,因爲它們鎖的是不同對象。 

synchronized方法

package com.huanghe.chapter21;

import sun.invoke.util.BytecodeName;

/**
 * @Author: River
 * @Date:Created in  22:09 2018/5/31
 * @Description:
 */
public class BankDemo {
    public static void main(String[] args) {
        Custom c = new Custom();
        Thread t1 = new Thread(c);
        Thread t2 = new Thread(c);
        t1.start();
        t2.start();
    }
}

class Bank{
    private int sum;

    //這個方法會引起線程不安全問題,比如線程1進來執行了sum=0+100=100;之後切換到了線程2進行執行sum=sum+100=200,線程2執行之後
    //輸出的是200,線程2執行之後切換到線程1輸出200,所以會輸出200,200,這就出現問題了,此時在方法出添加synchronized,就可以避免
    public synchronized void add(int num) {
        sum = sum + num;
        System.out.println("sum="+sum);
    }
}

class Custom implements Runnable {
    private Bank b = new Bank();

    @Override
    public void run() {
        for (int i = 0; i <3 ; i++) {
            b.add(100);
        }
    }
}

不過需要注意以下三點:

  1)當一個線程正在訪問一個對象的 synchronized 方法,那麼其他線程不能訪問該對象的其他 synchronized 方法。這個原因很簡單,因爲一個對象只有一把鎖,當一個線程獲取了該對象的鎖之後,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。

  2)當一個線程正在訪問一個對象的 synchronized 方法,那麼其他線程能訪問該對象的非 synchronized 方法。這個原因很簡單,訪問非 synchronized 方法不需要獲得該對象的鎖,假如一個方法沒用 synchronized 關鍵字修飾,說明它不會使用到臨界資源,那麼其他線程是可以訪問這個方法的。

  3)如果一個線程 A 需要訪問對象 object1 的 synchronized 方法 fun1,另外一個線程 B 需要訪問對象 object2 的 synchronized 方法 fun1,即使 object1 和 object2 是同一類型),也不會產生線程安全問題,因爲他們訪問的是不同的對象,所以不存在互斥問題。

驗證同步代碼塊使用的是哪個鎖?

package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  9:32 2018/6/1
 * @Description:
 */
public class SynFunctionLockDemo {
    public static void main(String[] args) {
        Ticket1 t = new Ticket1();
        System.out.println(t);

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        //讓主線程sleep
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.flag=false;
        t2.start();
    }
}

class Ticket1 implements Runnable {

    private int num = 100;

    Object obj = new Object();

    boolean flag = true;


    @Override
    public void run() {
        if (flag) {
            while (true) {
                synchronized (this) {
                    if (num > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "......obj....." + num--);
                    }
                }
            }
        } else {
            while (true) {
                show();
            }
        }
    }

    public synchronized void show() {
        if (num > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "......fun....." + num--);
        }
    }
}

結果:

Thread-0......obj.....100
Thread-0......obj.....99
Thread-0......obj.....98
Thread-1......fun.....97
Thread-1......fun.....96
Thread-1......fun.....95
Thread-1......fun.....94
Thread-1......fun.....93
Thread-1......fun.....92
Thread-1......fun.....91
Thread-1......fun.....90
Thread-1......fun.....89
Thread-1......fun.....88
Thread-1......fun.....87
Thread-1......fun.....86
Thread-1......fun.....85
Thread-1......fun.....84
Thread-1......fun.....83
Thread-1......fun.....82
Thread-1......fun.....81
Thread-1......fun.....80
Thread-1......fun.....79
Thread-1......fun.....78
Thread-1......fun.....77
Thread-1......fun.....76
Thread-1......fun.....75
Thread-1......fun.....74
Thread-1......fun.....73
Thread-1......fun.....72
Thread-1......fun.....71
Thread-1......fun.....70
Thread-1......fun.....69

可以驗證同步函數使用的鎖是this

同步函數和同步代碼塊的區別:

1:同步方法使用synchronized修飾方法,在調用該方法前,需要獲得內置鎖(java每個對象都有一個內置鎖),否則就處於阻塞狀態

2:同步代碼塊使用synchronized(object){}進行修飾,在調用該代碼塊時,需要獲得內置鎖,否則就處於阻塞狀態

3:同步函數使用的鎖匙this,而同步代碼塊使用的鎖匙任意的對象

靜態同步函數使用的鎖(class 對象鎖,類.class):

特別地,每個類也會有一個鎖,靜態的 synchronized方法 就是以Class對象作爲鎖。另外,它可以用來控制對 static 數據成員 (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 { 

    // 非 static synchronized 方法
    public synchronized void insert(){
        System.out.println("執行insert");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行insert完畢");
    }

    // static synchronized 方法
    public synchronized static void insert1() {
        System.out.println("執行insert1");
        System.out.println("執行insert1完畢");
    }
}/* Output: 
        執行insert
        執行insert1
        執行insert1完畢
        執行insert完畢
 *///:~

根據執行結果,我們可以看到第一個線程裏面執行的是insert方法,不會導致第二個線程執行insert1方法發生阻塞現象。下面,我們看一下 synchronized 關鍵字到底做了什麼事情,我們來反編譯它的字節碼看一下,下面這段代碼反編譯後的字節碼爲:

有一點要注意:對於 synchronized方法 或者 synchronized代碼塊,當出現異常時,JVM會自動釋放當前線程佔用的鎖,因此不會由於異常導致出現死鎖現象。

四. 可重入性

一旦有一個線程訪問某個對象的synchronized修飾的方法或代碼區域時,該線程則獲取這個對象的鎖,其他線程不能再調用該對象被synchronized影響的任何方法。那麼,如果這個線程自己調用該對象的其他synchronized方法,Java是如何判定的?這就涉及到了Java中鎖的重要特性:可重入性,

重入的一種實現方法是,爲每個鎖關聯一個獲取計數值和一個所有者線程。當計數值爲0時,這個鎖就被認爲是沒有被任何線程所持有,當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取計數值置爲1,如果同一個線程再次獲取這個鎖,計數值將遞增,而當線程退出同步代碼塊時,計數器會相應地遞減。當計數值爲0時,這個鎖將被釋放。

public class Father  
{  
    public synchronized void doSomething(){  
        ......  
    }  
}  
  
public class Child extends Father  
{  
    public synchronized void doSomething(){  
        ......  
        super.doSomething();  
    }  
}  

 子類覆寫了父類的同步方法,然後調用父類中的方法,此時如果沒有可重入的鎖,那麼這段代碼件產生死鎖。

由於Father和Child中的doSomething方法都是synchronized方法,因此每個doSomething方法在執行前都會獲取Child對象實例上的鎖。如果內置鎖不是可重入的,那麼在調用super.doSomething時將無法獲得該Child對象上的互斥鎖,因爲這個鎖已經被持有,從而線程會永遠阻塞下去,一直在等待一個永遠也無法獲取的鎖。重入則避免了這種死鎖情況的發生。

    同一個線程在調用本類中其他synchronized方法/塊或父類中的synchronized方法/塊時,都不會阻礙該線程地執行,因爲互斥鎖時可重入的。

五. 死鎖

常見的情景之一是同步的嵌套

package com.huanghe.chapter21;

/**
 * @Author: River
 * @Date:Created in  10:49 2018/6/1
 * @Description:
 */
public class DeadLockTest {
    public static void main(String[] args) {
        Test a = new Test(true);
        Test b = new Test(false);

        Thread t1 = new Thread(a);
        Thread t2 = new Thread(b);
    }

}

class Test implements Runnable{
    private boolean flag;

    Test(boolean flag) {
        this.flag=flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (MyLock.locka) {
                System.out.println(Thread.currentThread().getName()+"if   locka....");
                synchronized (MyLock.lockb) {
                    System.out.println(Thread.currentThread().getName()+"if   locka....");
                }
            }
        } else {
            synchronized (MyLock.lockb) {
                System.out.println(Thread.currentThread().getName()+"else  lockb....");
                synchronized (MyLock.locka) {
                    System.out.println(Thread.currentThread().getName()+"else locka.....");
                }
            }
        }
    }
}

class MyLock{
    public static final Object locka=new Object();
    public static final Object lockb=new Object();
}

輸出的結果:

Thread-1 else lockb.......

Thread-0 if   locka.......

從結果中可以看出來,當線程1拿到了b鎖,所以執行了else lockb.......,而線程0拿到了a鎖執行if locka

線程0接下來需要去執行第二條語句的時候由於b鎖被線程1拿着所以無法執行,線程1接下來需要去執行第二條語句的時候需要locka,但是locka被線程0擁有着,所以出現了死鎖的情況。


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