線程安全、數據同步之 synchronized 與 Lock

轉載https://www.cnblogs.com/jycboy/p/5620691.html

 

 

寫在前面

本篇文章講的東西都是Android開源網絡框架NoHttp的核心點,當然線程、多線程、數據安全這是Java中就有的,爲了運行快我們用一個Java項目來講解。

爲什麼要保證線程安全/數據同步

當多個子線程訪問同一塊數據的時候,由於非同步訪問,所以數據可能被同時修改,所以這時候數據不準確不安全。

現實生活中的案例

假如一個銀行帳號可以存在多張銀行卡,三個人去不同營業點同時往帳號存錢,假設帳號原來有100塊錢,現在三個人每人存錢100塊,我們最後的結果應該是100 + 3 * 100 = 400塊錢。但是由於多個人同時訪問數據,可能存在三個人同時存的時候都拿到原賬號有100,然後加上存的100塊再去修改數據,可能最後是200、300或者400。這種清情況下就需要鎖,當一個人操作的時候把原賬號鎖起來,不能讓另一個人操作。

案例(非線程安全)代碼實現:

1、程序入口,啓動三個線程在後臺循環執行任務,添加100個任務到隊列:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

/**

 * 程序入口

 */

public void start() {

    // 啓動三個線程

    for (int i = 0; i < 3; i++) {

        new MyTask(blockingQueue).start();

    }

  

    // 添加100個任務讓三個線程執行

    for (int i = 0; i < 100; i++) {

        Tasker tasker = Tasker.getInstance();

        blockingQueue.add(tasker);

    }

}

 2、那我們再來看看MyTask這個線程是怎麼回事,它是怎麼執行Tasker這個任務的。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class MyTask extends Thread {

  

    ...

  

    @Override

    public void run() {

        while (true) {

            try {

                Tasker person = blockingQueue.take();

                person.change();

            catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

  

}

 

分析一下上面的代碼,就是一直等待循環便利隊列,每拿到一個Tasker時去調用void change()方法讓Tasker在子線程中執行任務。

3、我們在來看看Tasker對象怎麼執行,單例模式的對象,被重複添加到隊列中執行void change()方法:

1

2

3

4

5

6

7

8

9

10

public class Tasker implements Serializable, Comparable<Tasker> {

  

    private static Integer value = 0;

  

    public void change() {

        value++;

        System.out.println(value);

    }

    ...

}

  我們來分析一下上面的代碼,void change()每被調用一次,屬性value的值曾加1,理論上應該是0 1 2 3 4 5 6 7 8 9 10…這樣的數據被打印出來,最差的情況下也是1 3 4 6 5 2 8 7 9 10 12 11…這樣順序亂一下而已,但是我們運行起來看看:

線程不安全演示

 

我們發現了爲什麼會有3 4 3 3 這種重複數據出現呢?嗯對了,這就是文章開頭說的多個線程拿到的value字段都是2,然後各自+1後打印出來的結果都是3,如果應用到我們的銀行系統中,那這不是坑爹了麼,所以我們在多線程開發的事後就用到了鎖。

多線程保證數據的線程安全與數據同步

多線程開發中不可避免的要用到鎖,一段被加鎖的代碼被一個線程執行之前,線程要先拿到執行這段代碼的權限,在Java裏邊就是拿到某個同步對象的鎖(一個對象只有一把鎖),如果這個時候同步對象的鎖被其他線程拿走了,這個線程就只能等了(線程阻塞在鎖池等待隊列中)。拿到權限(鎖)後,他就開始執行同步代碼,線程執行完同步代碼後馬上就把鎖還給同步對象,其他在鎖池中等待的某個線程就可以拿到鎖執行同步代碼了。這樣就保證了同步代碼在統一時刻只有一個線程在執行。Java中常用的鎖有synchronized和Lock兩種。
鎖的特點:每個對象只有一把鎖,不管是synchronized還是Lock它們鎖定的只能是某個具體對象,也就是說該對象必須是唯一的,才能被鎖起,不被多個線程同時使用。

synchronized的特點

同步鎖,當它鎖定的方法或者代碼塊發生異常的時候,它會在自動釋放鎖;但是如果被它鎖定的資源被線程競爭激烈的時候,它的表現就沒那麼好了。

1、我們來看下下面這段代碼:

1

2

3

4

5

// 添加100個任務讓三個線程執行

for (int i = 0; i < 100; i++) {

    Tasker tasker = new Tasker();

    blockingQueue.add(tasker);

}

  這段代碼是文章最開頭的一段,只是把Tasker.getInstance()改爲了new Tasker();,我們現在給Tadkervoid change()方法加上synchronized鎖:

1

2

3

4

5

6

7

/**

 * 執行任務;synchronized鎖定方法。

 */

public synchronized void change() {

    value++;

    System.out.println(value);

}

  我們再次執行後發現,艾瑪怎麼還是有重複的數字打印呢,不是鎖起來了麼?但是細心的讀者注意到我們添加Tasker到隊列中的時候是每次都new Tasker();,這樣每次添加進去的任務都是一個新的對象,所以每個對象都有一個自己的鎖,一共3個線程,每個線程持有當前task出的對象的鎖,這必然不能產生同步的效果。換句話說,如果要對value同步,那麼這些線程所持有的對象鎖應當是共享且唯一的!這裏就驗證了上面講的鎖的特點了。那麼正確的代碼應該是:

1

2

3

4

Tasker tasker = new Tasker();

for (int i = 0; i < 100; i++) {

    blockingQueue.add(tasker);

}

  或者給這個任務提供單例模式:

1

2

3

4

for (int i = 0; i < 100; i++) {

    Tasker tasker = Tasker.getInstance();

    blockingQueue.add(tasker);

}

 這樣對象是唯一的,那麼public synchronized void change()的鎖也是唯一的了。

2、難道我們要給每一個任務都要寫一個單例模式麼,我們每次改變對象的屬性豈不是把之前之前的對象屬性給改變了?所以我們使用synchronized還有一種方案:在執行任務的代碼塊放一個靜態對象,然後用synchronized加鎖。我們知道靜態對象不跟着對象的改變而改變而是一直在內存中存在,所以:

1

2

3

4

5

6

7

8

private static Object object = new Object();

  

public void change() {

    synchronized (object) {

        value++;

        System.out.println(value);

    }

}

  這樣就能保證鎖對象的唯一性了,無論我們用new Tasker();Tasker.getInstance();都不受影響。
我們知道,對於同步靜態方法,對象鎖就是該靜態放發所在的類的Class實例,由於在JVM中,所有被加載的類都有唯一的類對象,具體到本例,就是唯一的Tasker.class對象。不管我們創建了該類的多少實例,但是它的類實例仍然是一個。所以我們上面的代碼也可以改爲:

1

2

3

4

5

6

public void change() {

    synchronized (Tasker.class) {

        value++;

        System.out.println(value);

    }

}

 根據上面的經驗,我們的Tasker.getInstance();方法的具體應該就是:

1

2

3

4

5

6

7

8

9

private static Tasker tasker;

  

public static Tasker getInstance() {

    synchronized (Tasker.class) {

        if (tasker == null)

            tasker = new Tasker();

        return tasker;

    }

}

 3、 synchronized的代碼塊遇到異常後自動釋放鎖。我們上面提到synchronized遇到異常後自動釋放鎖,所以如果我們不能保證代碼塊是否會發生異常的情況下(當時是資源不緊張時)是可以使用synchronized,我們模擬一下:

1

2

3

4

5

6

7

8

public void change() {

    synchronized (object) {

        value++;

        System.out.println(value);

    }

    if (value == 50)

        throw new RuntimeException("");

}

 上面代碼應該很清楚了,但value增加到50的時候,這個線程會發生異常,根據我們的推斷,執行50的這個線程發生崩潰,但是其他兩個線程應該還是正常執行的,我們來測試一下: 

synchronized代碼快發生異常後釋放鎖

我們看到之前是三個數字一起打印,後來變成兩個線程一起打印了,很顯然一個線程崩潰了之後還有兩個線程在執行,說明object這個鎖被釋放了。

Lock

由於我們提到synchronized無法中斷一個正在等候獲得鎖的線程,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。所以JSR 166小組花時間爲我們開發了java.util.concurrent.lock框架,當Lock鎖定的方法或者代碼塊發生異常的時候,它不會自動釋放鎖;它擁有與synchronized相同的併發性和內存語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。)
Lock的實現類有哪些?我們在代碼中選中Lock,按下Ctrl + T,顯示出如下:

Lock的實現類

我們看到有一個讀出鎖ReadLock、一個寫入鎖WriteLock、一個重入鎖ReenTrantLock,我們這裏主要說在多線程開發中用的最多的重入鎖ReenTrantLock
廢話不多說了,其實代碼上來講和上面原來一樣的,我們看看怎麼實現:

1

2

3

4

5

6

7

8

9

10

11

12

13

/** Lock模塊事例 **/

private static Lock lock = new ReentrantLock();

  

public void change() {

lock.lock();

  

{// 代碼塊

    value++;

    System.out.println(value);

}

  

lock.unlock();

}

  我們看到使用也蠻簡單,而且擴展性更好。但是呢我們上面提到如果我們在這裏發生了異常呢:

1

2

3

4

{// 代碼塊

    value++;

    System.out.println(value);

}

  經測試,果然被鎖起來,所有線程都拿不到執行權限了,所以呢這裏也給出一解決方案,哈哈也許你早就想到了,就是咱的try {} finally {}

1

2

3

4

5

6

7

8

9

10

11

public void change() {

    lock.lock();

    try {

        value++;

        System.out.println(value);

        if (value == 50)

            throw new RuntimeException("");

    finally {

        lock.unlock();

    }

}

  

我們看到我們在上面的代碼中加了一個和synchronized一樣的異常,我們再次測試後發現,完全沒有發生異常啊是不是哈哈哈,這就是ReentrantLock,這位看的朋友你會用了嗎?


NoHttp 源碼及Demo託管在Github歡迎大家Starhttps://github.com/yanzhenjie/NoHttp

對新手很有指導意義。。。。。。

 


如果您覺得閱讀本文對您有幫助,請點一下�?推薦”按鈕,您的“推薦�?將是我最大的寫作動力!歡迎各位轉載,但是未經作者本人同意,轉載文章之後必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責任的權利�?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章