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


本文Demo下載傳送門

寫在前面

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

為什么要保證線程安全/數據同步

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

現實生活中的案例

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

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

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

/**
 * 程序入口
 */
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這個任務的。

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()方法:

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、我們來看下下面這段代碼:

// 添加100個任務讓三個線程執行
for (int i = 0; i < 100; i++) {
    Tasker tasker = new Tasker();
    blockingQueue.add(tasker);
}

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

/**
 * 執行任務;synchronized鎖定方法。
 */
public synchronized void change() {
    value++;
    System.out.println(value);
}

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

Tasker tasker = new Tasker();
for (int i = 0; i < 100; i++) {
    blockingQueue.add(tasker);
}

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

for (int i = 0; i < 100; i++) {
    Tasker tasker = Tasker.getInstance();
    blockingQueue.add(tasker);
}

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

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

private static Object object = new Object();
 
public void change() {
    synchronized (object) {
        value++;
        System.out.println(value);
    }
}

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

public void change() {
    synchronized (Tasker.class) {
        value++;
        System.out.println(value);
    }
}

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM