併發編程之多線程線程安全

1、什麼是線程安全?

爲何有線程安全問題?

當多個線程同時共享,同一個全局變量或靜態變量,作寫的操做時,可能會發生數據衝突問題,也就是線程安全問題。可是作讀操做是不會發生數據衝突問題。java

案例:需求如今有100張火車票,有兩個窗口同時搶火車票,請使用多線程模擬搶票效果。編程

代碼:緩存

public class ThreadTrain implements Runnable {
    private int trainCount = 100;

    @Override
    public void run() {
        while (trainCount > 0) {
            try {
                Thread.sleep(50);
            } catch (Exception e) {

            }
            sale();
        }
    }

    public void sale() {
        if (trainCount > 0) {
            System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
            trainCount--;
        }
    }

    public static void main(String[] args) {
        ThreadTrain threadTrain = new ThreadTrain();
        Thread t1 = new Thread(threadTrain, "①號");
        Thread t2 = new Thread(threadTrain, "②號");
        t1.start();
        t2.start();
    }

}複製代碼

運行結果:安全

一號窗口和二號窗口同時出售火車第九九張,部分火車票會重複出售。多線程

結論發現,多個線程共享同一個全局成員變量時,作寫的操做可能會發生數據衝突問題。ide

2、線程安全解決辦法:

一、使用多線程之間同步synchronized或使用鎖(lock)。

將可能會發生數據衝突問題(線程不安全問題),只能讓當前一個線程進行執行。代碼執行完成後釋放鎖,而後才能讓其餘線程進行執行。這樣的話就能夠解決線程不安全問題。函數

這樣多個線程共享同一個資源,不會受到其餘線程的干擾。性能

二、內置的鎖

Java提供了一種內置的鎖機制來支持原子性優化

每個Java對象均可以用做一個實現同步的鎖,稱爲內置鎖,線程進入同步代碼塊以前自動獲取到鎖,代碼塊執行完成正常退出或代碼塊中拋出異常退出時會釋放掉鎖this

內置鎖爲互斥鎖,即線程A獲取到鎖後,線程B阻塞直到線程A釋放鎖,線程B才能獲取到同一個鎖

內置鎖使用synchronized關鍵字實現,synchronized關鍵字有兩種用法:

1.修飾須要進行同步的方法(全部訪問狀態變量的方法都必須進行同步),此時充當鎖的對象爲調用同步方法的對象

2.同步代碼塊和直接使用synchronized修飾須要同步的方法是同樣的,可是鎖的粒度能夠更細,而且充當鎖的對象不必定是this,也能夠是其它對象,因此使用起來更加靈活

同步代碼塊synchronized

public void sale() {
        // 同步代碼塊(this明鎖),
        synchronized (this) {
            if (trainCount > 0) {
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
                trainCount--;
            }
        }
    }複製代碼

同步方法

public synchronized void sale() {
        if (trainCount > 0) {
            System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
            trainCount--;
        }
    }複製代碼

靜態同步函數

方法上加上static關鍵字,使用synchronized 關鍵字修飾 或者使用類.class文件。

靜態的同步函數使用的鎖是 該函數所屬字節碼文件對象

能夠用 getClass方法獲取,也能夠用當前 類名.class 表示。

public static void sale() {
        synchronized (ThreadTrain3.class) {
            if (trainCount > 0) {
                System.out.println(Thread.currentThread().getName() + ",出售第" + (100 - trainCount + 1) + "張票");
                trainCount--;
            }
        }
}複製代碼

synchronized 修飾方法使用鎖是當前this鎖。

synchronized 修飾靜態方法使用鎖是當前類的字節碼文件

3、多線程死鎖

同步中嵌套同步,致使鎖沒法釋放

class Thread009 implements Runnable {
    private int trainCount = 100;
    private Object oj = new Object();
    public boolean flag = true;

    public void run() {

        if (flag) {
            while (trainCount > 0) {
                synchronized (oj) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                    sale();
                }

            }
        } else {
            while (trainCount > 0) {
                sale();
            }

        }

    }

    public synchronized void sale() {
        synchronized (oj) {
            try {
                Thread.sleep(10);
            } catch (Exception e) {

            }
            if (trainCount > 0) {
                System.out.println(Thread.currentThread().getName() + "," + "出售第" + (100 - trainCount + 1) + "票");
                trainCount--;
            }
        }
    }
}

public class Test009 {
    public static void main(String[] args) throws InterruptedException {
        Thread009 threadTrain = new Thread009();
        Thread t1 = new Thread(threadTrain, "窗口1");
        Thread t2 = new Thread(threadTrain, "窗口2");
        t1.start();
        Thread.sleep(40);
        threadTrain.flag = false;
        t2.start();

    }
}}複製代碼

4、Threadlocal

ThreadLocal提升一個線程的局部變量,訪問某個線程擁有本身局部變量。

當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其它線程所對應的副本。

ThreadLocal的接口方法

ThreadLocal類接口很簡單,只有4個方法,咱們先來了解一下:

  • void set(Object value)設置當前線程的線程局部變量的值。
  • public Object get()該方法返回當前線程所對應的線程局部變量。
  • public void remove()將當前線程局部變量的值刪除,目的是爲了減小內存的佔用,該方法是JDK 5.0新增的方法。須要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。
  • protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,而且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。
class Res {
    // 生成序列號共享變量
    public static Integer count = 0;
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {

            return 0;
        };

    };

    public Integer getNum() {
        int count = threadLocal.get() + 1;
        threadLocal.set(count);
        return count;
    }
}

public class ThreadLocaDemo2 extends Thread {
    private Res res;

    public ThreadLocaDemo2(Res res) {
        this.res = res;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
        }

    }

    public static void main(String[] args) {
        Res res = new Res();
        ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
        threadLocaDemo1.start();
        threadLocaDemo2.start();
        threadLocaDemo3.start();
    }
}
複製代碼

ThreadLoca實現原理

ThreadLoca經過map集合

Map.put(「當前線程」,值);

6、多線程有三大特性

Java內存結構(JVM),

原子性(保證線程安全、保持數據一致性);

可見性(某個線程修改全局變量的時候,其餘的線程也能獲取修改後的變量)

有序性

什麼是原子性

即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。

一個很經典的例子就是銀行帳戶轉帳問題:

好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。這2個操做必需要具有原子性才能保證不出現一些意外的問題。

咱們操做數據也是如此,好比i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具有原子性的,則多線程運行確定會出問題,因此也須要咱們使用同步和lock這些東西來確保這個特性了。

原子性其實就是保證數據一致、線程安全一部分,

什麼是可見性

當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。

若兩個線程在不一樣的cpu,那麼線程1改變了i的值還沒刷新到主存,線程2又使用了i,那麼這個i值確定仍是以前的,線程1對變量的修改線程沒看到這就是可見性問題。

什麼是有序性

程序執行的順序按照代碼的前後順序執行。

通常來講處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。以下:

int a = 10; //語句1

int r = 2; //語句2

a = a + 3; //語句3

r = a*a; //語句4

則由於重排序,他還可能執行順序爲 2-1-3-4,1-3-2-4

但毫不可能 2-1-4-3,由於這打破了依賴關係。

顯然重排序對單線程運行是不會有任何問題,而多線程就不必定了,因此咱們在多線程編程時就得考慮這個問題了。

7、Java內存模型

共享內存模型指的就是Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入時,能對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。

Java內存模型(JMM java memory model),jmm決定一個線程對共享變量的寫入時,能對另外一個線程是否可見;

主內存:共享變量;

本地內存:共有變量的副本;

file

從上圖來看,線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:

  1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。
    file

8、Volatile

可見性也就是說一旦某個線程修改了該被volatile修飾的變量,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,能夠當即獲取修改以後的值。

在Java中爲了加快程序的運行效率,對一些變量的操做一般是在該線程的寄存器或是CPU緩存上進行的,以後纔會同步到主存中,而加了volatile修飾符的變量則是直接讀寫主存。

Volatile 保證了線程間共享變量的及時可見性,但不能保證原子性:

class ThreadVolatileDemo extends Thread {
    public volatile   boolean flag = true;
    @Override
    public void run() {
        System.out.println("開始執行子線程....");
        while (flag) {
        }
        System.out.println("線程中止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }

}

public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已經設置成false");
        Thread.sleep(1000);
        System.out.println(threadVolatileDemo.flag);

    }
}複製代碼

Volatile與Synchronized區別

  1. 從而咱們能夠看出volatile雖然具備可見性可是並不能保證原子性。
  2. 性能方面,synchronized關鍵字是防止多個線程同時執行一段代碼,就會影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized。

可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。

相關文章
相關標籤/搜索