java多線程:線程同步synchronized(不一樣步的問題、隊列與鎖),死鎖的產生和解決


0、不一樣步的問題


併發的線程不安全問題:html

多個線程同時操做同一個對象,若是控制很差,就會產生問題,叫作線程不安全。java

咱們來看三個比較經典的案例來講明線程不安全的問題安全

0.1 訂票問題

例如前面說過的黃牛訂票問題,可能出現負數或相同。多線程

線程建立方式&&黃牛訂票模擬併發

0.2 銀行取錢

再來看一個取錢的例子:app

/*
    模擬一個帳戶
*/
class Account{
    int money;
    String name;
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}
/*
    模擬取款機,方便設置名字,繼承Thread而不是實現Runnable
*/
class Drawing extends Thread{
    Account account;
    int outMoney;//取出去了多少錢
    int outTotal;//總共取到了多少錢

    public Drawing(Account account, int outMoney,String name) {
        super(name);
        this.account = account;
        this.outMoney = outMoney;
    }

    @Override
    public void run() {
        account.money -= outMoney;
        outTotal += outMoney;
        System.out.println(this.getName() + "---帳戶餘額爲:" + account.money);
        System.out.println(this.getName() + "---總共取到了:" + outTotal);
    }
}

而後咱們寫個客戶端調用一下,假設兩我的同時取錢,操做同一個帳戶ide

public class Checkout {
    public static void main(String[] args) {
        Account account = new Account(200000,"禮金");
        Drawing you = new Drawing(account,8000,"你");
        Drawing wife = new Drawing(account,300000,"你老婆");
        you.start();
        wife.start();
    }
}

運行起來,問題就會出現。性能

每次的結果都不同,並且,這樣確定會把錢取成負數,顯然這是非法的(嘻嘻),首先邏輯上須要修改,當錢少於 0 了就應該退出,而且不能繼續取錢的動做了。按照這個思路,加上一個判斷呢?學習

if (account.money < outMoney){
    System.out.println("餘額不足");
    return;
}
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

但是即使是這樣,發現仍是會出現結果爲負的狀況,沒法保證線程安全測試

0.3 數字遞增

還有一個經典的例子,那就是對於直接計算迭代過慢,而轉爲多線程。

一個數字 num ,開闢一萬個線程對他作 ++ 操做,看結果會是多少。

public class AddSum {
    private static int num = 0;
    public static void main(String[] args) {
        for (int i=0; i<=10000; i++){
            new Thread(()->{
                num++;
            }).start();
        }
        System.out.println(num);
    }
}

每次運算的結果都不同,同樣的是,結果永遠 < 10000 。

或者用給 list 裏添加數字來測試:

List<String> list = new ArrayList<>();
for (int i=0; i<10000; i++){
    new Thread(()->{
        list.add(Thread.currentThread().getName());
    }).start();
}
System.out.println(list.size());

同樣的結果。

線程不安全的問題如何解決呢?


1、同步(synchronized)


1.1 問題出現的緣由

從前面的介紹裏,咱們總結出會出現同步問題的狀況,也就是併發三要素:多個線程、同時操做、操做同一個對象。另外,操做的特色是:操做類型爲修改這個時候會產生併發的問題,線程安全問題。

1.2 解決方案

  1. 確保線程安全,第一就是排隊。只要排隊,那麼無論多少線程,始終一個時間點只會有一個線程在執行,就保證了安全。
    不過排隊會有一個問題:怎麼直到輪到我了呢,也就是怎麼知道排在前面的線程執行完了呢?
  2. 現實生活中,可能會用相似房卡的形式,前一我的把卡交還了,纔會有後面的人有機會入住。這就是

利用 隊列 + 鎖 的方式保證線程安全的方式叫線程同步,就是一種等待機制,多個同時訪問此對象的線程進入這個對象的等待池 造成隊列,前面的線程使用完畢後,下一個線程再使用。

鎖機制最開始在 java 裏就是一個關鍵字 synchronized(同步),屬於排他鎖,當一個線程得到對象的排他鎖,獨佔資源,其餘線程必須等待,使用後釋放鎖便可。

按照這種思路,能夠想象到這種保證安全方式的弊端,也就是早期的 synchronized 存在的問題:

  1. 一個線程持有鎖會致使其餘全部須要這個鎖的線程掛起;
  2. 多線程競爭下,加鎖、釋放鎖致使耗時嚴重,性能問題
  3. 一個優先級高的線程等待一個優先級低的線程的鎖釋放,會使得本應該的優先級倒置,引發性能問題。

另外,Synchronized 是基於底層操做系統的 Mutex Lock 實現的,每次獲取和釋放鎖操做都會帶來用戶態和內核態的切換,從而增長系統性能開銷。所以,在鎖競爭激烈的狀況下,Synchronized 同步鎖在性能上就表現得很是糟糕,它也常被你們稱爲重量級鎖。

可是 jdk 6 以後有了很強的改進,這個內容待更新,留個坑。


2、同步關鍵字的用法


2.1 同步方法

synchronized 方法控制對 成員變量或者類變量 對象的訪問,每一個對象對應一把鎖。寫法以下:

public synchronized void test(){
    //。。。
}
  1. 若是修飾的是具體對象:鎖的是對象
  2. 若是修飾的是成員方法:那鎖的就是 this
  3. 若是修飾的是靜態方法:鎖的就是這個對象.class

每一個 synchronized 方法都必須得到調用該方法的對象的鎖才能執行,不然所屬的這個線程阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時,鎖釋放。

同步方法的寫法代碼,以上面的取錢案例歷的 取錢類爲例,若是直接在提款機的操做,把 run 方法或者裏面的內容提出來變成 test ,加上 synchronized 修飾:

@Override
public void run() {
    test();
}
public synchronized void test(){
    //內容都不變
}

會發現,仍然出現了負數。鎖定失敗。

分析

咱們認爲在 test 方法裏進行的對象修改,因此把他鎖上就行了,可是對於這個類,這個提款機類來講,test 方法是成員方法,所以鎖的對象其實是 this ,也就是提款機。

但咱們的初衷,要線程鎖的資源應該是 Account 對象,而不是提款機對象。

2.2 同步塊

除了方法,synchronized 還能夠修飾塊,叫作同步塊

synchronized 修飾同步塊的方式是:

synchronized (obj){
    //...
}

其中的 obj 能夠是任何對象,可是用到它,確定是設置爲那個共享資源,這個 obj 被稱爲同步監視器同步監視器的做用就是,判斷這個監視器是否被鎖定(是否能訪問),從而決定是否能執行其中的代碼。

java的花括號中內容有如下幾種:

  1. 方法裏面的塊:局部塊。解決變量做用域的問題,快速釋放內存(好比方法裏面再有個for循環,裏面的變量);
  2. 類層的塊:構造塊。初始化信息,和構造方法是同樣的;
  3. 類層的靜態塊:靜態構造快。最先加載,不是對象的信息,而是類的信息;
  4. 方法裏面的同步塊:監視對象。

第四種就是咱們這裏學習的同步塊。

注意,若是是同步方法裏,不必指定同步監視器,由於同步方法的監視器已是 this 或者 .class。

用同步塊的方式對提款機問題進行修改:

public void test(){
    synchronized(account){
            //內容不變
    }
}

也就是加上對 account 的監視器,鎖住這個對象。這樣運行結果就正確了 。

這種作法效率不高,由於雖然對 account 上了鎖,可是每一次都要把整個流程走一遍,方法體的內容是不少的,另外,每次加鎖與否,都是性能的消耗,進入以後再出來,哪怕什麼也不作,也是消耗。

其實,咱們能夠在加鎖的前面再加一重判斷,那麼以後就不必再進行上鎖的過程了。

public void test(){
    if (account.money ==0 ){
        return;
    }
    synchronized(account){
    }
}

就是這樣的一個代碼,在併發量很高的時候,每每能夠大大提升效率

對於上面的 10000 個線程的加法那個問題,咱們也能夠經過 synchronized 加鎖,來保證結果的正確性。

(可是 synchronized 修飾的要是引用類型,因此直接對 int num 加鎖不行,通常直接使用專門提供的原子類)

list 的里加數字的測試:

List<String> list = new ArrayList<>();
for (int i=0; i<10000; i++){
    new Thread(()->{
        synchronized (list){
            list.add(Thread.currentThread().getName());
        }
    }).start();
}
Thread.sleep(2000);
System.out.println(list.size());

main方法,下面的print語句,這些都是線程,因此可能上面尚未操做的時候,就已經輸出了,爲了方便觀察,咱們在最後輸出以前先讓main線程休眠一會,再看裏面add的結果是否正確。

tips:對於容器的操做,Java的util.concurrent包裏也直接提供了對應的安全容器CopyOnWriteArrayList。

CopyOnWriteArrayList<String > list1 = new CopyOnWriteArrayList<>();
for (int i=0; i<10000; i++){
    new Thread(()->{
        list1.add(Thread.currentThread().getName());
    }).start();
}
Thread.sleep(2000);
System.out.println(list1.size());

2.3 問題

synchronized 塊過小,可能鎖不住,安全性又不行了,鎖的方法太大,又效率會下降,因此要很注意控制範圍

並且,還有相似於 單例模式 裏 Double-Check 寫法針對的問題,有時候一重鎖性質不夠,兩重鎖仍然不夠保證安全。


3、線程同步問題應用示例


3.1 快樂影院

電影院買票。

/**
* 快樂影院
*/
public class HappyCinema {
    public static void main(String[] args) {
        Cinema cinema = new Cinema(20, "萬達");
        new Thread(new Customer(cinema,2)).start();
        new Thread(new Customer(cinema,1)).start();
    }
}
/**
* 電影院,提供訂票方法
*/
class Cinema{
    int available;
    String name;

    public Cinema(int available, String name) {
        this.available = available;
        this.name = name;
    }
    //提供購票方法
    public boolean bookTickets(int seats){
        System.out.println("可用位置爲:"+available);
        if (seats > available){
            return false;
        }
        available -= seats;
        return true;
    }
}
/**
* 顧客,有多個顧客,模仿多線程
*/
class Customer implements Runnable{
    Cinema cinema;
    int seats;
    //顧客建立的時候帶上要預約的做爲+訂哪一個影院
    public Customer(Cinema cinema, int seats) {
        this.cinema = cinema;
        this.seats = seats;
    }

    @Override
    public void run() {
        boolean flag = cinema.bookTickets(seats);
        if (flag){
            System.out.println("出票成功,"+Thread.currentThread().getName()+"買了 "+seats+" 張票");
        }else{
            System.out.println("出票失敗,"+Thread.currentThread().getName()+"買票,但位置不足 ");
        }
    }
}

對於一個電影院的票:available 資源來講,多個線程訪問,是須要同步的,不然就會出現不安全的問題。

解決:

@Override
public void run() {
    synchronized (cinema){
                //。。。
        }
    }
}

3.2 快樂影院進階

影院票的時候不是簡單計數,是能夠選座位的,咱們修改代碼,具體到某一個座位號的預約。

將 int 座位數目改爲 List,那麼購票方法改動以下:

public boolean bookTickets(List<Integer> seats){
        System.out.println("可用位置爲:" + available);
        List<Integer> copy = new ArrayList<>(available);
        //相減
        copy.removeAll(seats);
        //判斷改變後
        if (available.size() != copy.size() + seats.size() ){
            return false;
        }
        available = copy;
        return true;
    }

其餘地方只須要作簡單的修改,在調用的時候傳入一個構造好的 list 便可,這個時候再來看:

若是兩個顧客同時訂票的位置衝突

能夠看到完成了同步。

3.3 火車票

仍是相似於訂票,由於上面電影院的部分咱們都使用 同步塊 的方式鎖定某個對象,這裏使用同步方法來加深上鎖的理解。

模仿第一種電影院訂票的初始不加鎖寫法。

public class Happy12306 {
    public static void main(String[] args) {
        Railway railway = new Railway(20, "京西G12138");
        new Thread(new Passenger(railway,2)).start();
        new Thread(new Passenger(railway,1)).start();
    }
}
/**
* 鐵路系統,提供訂票方法
*/
class Railway{
    int available;
    String name;

    public Railway(int available, String name) {
        this.available = available;
        this.name = name;
    }
    //提供購票方法
    public boolean bookTickets(int seats){
        System.out.println("可用位置爲:"+available);
        if (seats > available){
            return false;
        }
        available -= seats;
        return true;
    }
}
/**
* 顧客,有多個顧客,模仿多線程
*/
class Passenger implements Runnable{
    Railway railway;
    int seats;
    public Passenger(Railway railway, int seats) {
        this.railway = railway;
        this.seats = seats;
    }

    @Override
    public void run() {
        boolean flag = railway.bookTickets(seats);
        if (flag){
            System.out.println("出票成功,"+Thread.currentThread().getName()+"買了 "+seats+" 張票");
        }else{
            System.out.println("出票失敗,"+Thread.currentThread().getName()+"買票,但位置不足 ");
        }
    }
}

如今開始給方法加鎖,考慮這個問題:

  1. 原本的 run 方法寫了 同步塊 對一個資源加鎖,這個資源是 票所在的 鐵路系統(上一個例子的電影院);
  2. 因此若是鎖 run 方法,咱們前面說過的,鎖成員方法至關於鎖的 this,也就是鎖了 乘客 類,是沒有用的,由於被修改的資源不在這裏
  3. 應該將這個方法放到 鐵路系統 類裏,而後對這個方法上鎖。

這樣會帶來新的問題,模擬多個線程的線程體應該來源於 乘客 ,不能是鐵路系統,因此乘客類也要繼續修改,繼承 Thread 類,自己做爲一個代理,去找到目標接口的實現類:鐵路系統 ,而後start。

public class Happy12306 {
    public static void main(String[] args) {
        Railway railway = new Railway(5, "京西G12138");
        new Passenger(5,railway,"乘客B").start();
        new Passenger(2,railway,"乘客A").start();
    }
}
/**
* 鐵路系統,提供訂票方法,自己就是一個線程,
*/
class Railway implements Runnable{
    int available;
    String name;

    public Railway(int available, String name) {
        this.available = available;
        this.name = name;
    }
    //提供購票方法,加入同步
    public synchronized boolean bookTickets(int seats){
        System.out.println("可用位置爲:"+available);
        if (seats > available){
            return false;
        }
        available -= seats;
        return true;
    }
    //run方法從 顧客類裏 挪過來,
    @Override
    public void run() {
        //運行時須要知道哪一個線程在操做本身,也就是seats的來源
        Passenger p = (Passenger) Thread.currentThread();
        boolean flag = this.bookTickets(p.seats);
        if (flag){
            System.out.println("出票成功,"+Thread.currentThread().getName()+"買了 "+p.seats+" 張票");
        }else{
            System.out.println("出票失敗,"+Thread.currentThread().getName()+"買票,但位置不足 ");
        }
    }
}
/**
* 顧客,做爲代理,是 Thread 的子代理
*/
class Passenger extends Thread{
    int seats;
    public Passenger(int seats, Runnable target, String name) {
        super(target,name);//用父類方法找到目標,也就是鐵路系統
        this.seats = seats;
    }
}

總結:

  1. synchronized 修飾成員方法鎖定的是 this,因此要加入鐵路系統類,
  2. 鐵路系統經過 Thread.currentThread() 方法 肯定當前的線程,同時獲取到訂票信息
  3. 乘客變成了 代理,是 Thread 的子類,在這個基礎上加入訂票信息
  4. 最後調用的時候,本應該使用 Thread 做爲代理去執行,改成用乘客類,起到了一個系統用多個不一樣線程的做用

乘客自己做爲代理子類可能比較難理解。

可是咱們回頭看看,對於上一種方式:

new Thread(new Passenger(railway,2)).start();
        new Thread(new Passenger(railway,1)).start();

雖然這麼寫的,可是其實傳入的一個 Runnable的實現類,在 Thread 源碼裏面調用了構造方法:

能夠看到,傳入一個 Runnable ,這個構造器加上了額外的信息,因此其實咱們這種作法:

public Passenger(int seats, Runnable target, String name) {
    super(target,name);//用父類方法找到目標,也就是鐵路系統
    this.seats = seats;
}

是模擬了源碼的寫法而已。


4、多線程死鎖的產生與解決


4.1 問題

死鎖:當多個線程各自佔有一些共享資源,而且互相等待其餘線程佔有的資源才能進行,從而致使兩個或者多個線程都在等待對方釋放資源,都中止執行的狀況。

最簡單的,某一個同步塊同時擁有「兩個以上的對象的鎖」的時候,就可能會發生死鎖問題。

  • 若是兩個線程,那就是塗口紅、照鏡子的問題,每一個人都想先拿了一個再拿另外一個;
  • 若是是多個線程,對應哲學家就餐問題,每一個人都想左手拿刀、右手拿叉。

口紅鏡子問題示例:

/**
* 死鎖的產生
*/
public class DeadLock {
    public static void main(String[] args) {
        Makup makup = new Makup(1,"女孩1");
        Makup makup1 = new Makup(0,"女孩2");
        makup.start();
        makup1.start();
    }
}
/**
* 口紅
*/
class Lipstick{ }
/**
* 鏡子
*/
class Mirror{ }
/**
* 化妝
*/
class Makup extends Thread{
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice;//選擇
    String girl;
    public Makup(int choice, String girl){
        this.choice = choice;
        this.girl = girl;
    }

    @Override
    public void run() {
        makeup();
    }
    //相互持有對方的對象鎖
    private void makeup(){
        if (choice == 0){
            synchronized (lipstick){
                System.out.println(this.girl + "得到口紅");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror){
                    System.out.println(this.girl + "而後得到鏡子");
                }
            }
        }else{
            synchronized (mirror){
                System.out.println(this.girl + "得到鏡子");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick){
                    System.out.println(this.girl + "而後得到口紅");
                }
            }
        }
    }
}

能夠發現程序停不下來了,死鎖已經產生。

其中的過程就是:

  1. 女孩 1 先拿到了鏡子,對其上鎖;
  2. 女孩 1 休息的時候,女孩 2 先拿到了口紅,對其上鎖;
  3. 女孩 2 休息的時候,女孩 1 休息結束,想要獲取口紅,但此時口紅上鎖,所以等待;
  4. 女孩 2 休息結束,想要獲取鏡子,但此時鏡子上鎖,所以等待。

4.2 解決

解決這個問題的方法:

不要出現 鎖的 嵌套 ,將等待後獲取另外一個鎖的代碼放到第一個加鎖的後面就能夠解決這個問題了:

private void makeup(){
        if (choice == 0){
            synchronized (lipstick){
                System.out.println(this.girl + "得到口紅");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (mirror){
               System.out.println(this.girl + "而後得到鏡子");
            }
        }else{
            synchronized (mirror){
                System.out.println(this.girl + "得到鏡子");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lipstick){
                System.out.println(this.girl + "而後得到口紅");
            }
        }
    }

總結:儘可能不要讓 一個同步代碼塊 同時擁有「兩個以上的對象的鎖」。

相關文章
相關標籤/搜索