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

本文Demo下載傳送門

寫在前面

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

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

當多個子線程訪問同一塊數據的時候,因爲非同步訪問,因此數據可能被同時修改,因此這時候數據不許確不安全。git

現實生活中的案例

假如一個銀行賬號能夠存在多張銀行卡,三我的去不一樣營業點同時往賬號存錢,假設賬號原來有100塊錢,如今三我的每人存錢100塊,咱們最後的結果應該是100 + 3 * 100 = 400塊錢。可是因爲多我的同時訪問數據,可能存在三我的同時存的時候都拿到原帳號有100,而後加上存的100塊再去修改數據,可能最後是200、300或者400。這種清狀況下就須要鎖,當一我的操做的時候把原帳號鎖起來,不能讓另外一我的操做。github

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

一、程序入口,啓動三個線程在後臺循環執行任務,添加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);
    }
}

 二、那咱們再來看看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在子線程中執行任務。多線程

三、咱們在來看看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,若是應用到咱們的銀行系統中,那這不是坑爹了麼,因此咱們在多線程開發的過後就用到了鎖。ide

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

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

synchronized的特色

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

一、咱們來看下下面這段代碼:

// 添加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()的鎖也是惟一的了。

二、難道咱們要給每個任務都要寫一個單例模式麼,咱們每次改變對象的屬性豈不是把以前以前的對象屬性給改變了?因此咱們使用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();方法的具體應該就是:

相關文章
相關標籤/搜索