【多線程併發編程】六 什麼是線程安全?

程序猿學社的GitHub,歡迎Star
https://github.com/ITfqyd/cxyxs
本文已記錄到github,形成對應專題。

前言

在學習多線程的道路上,我們會經常看到線程安全這類詞彙,面試官也經常問,本文就來說一說什麼是線程安全。

1.什麼是線程安全?

多個線程同一時刻對同一個全局變量(同一份資源)做寫操作(讀操作不會涉及線程安全)時,如果跟我們預期的結果一樣,我們就稱之爲線程安全,反之,線程不安全。

  • git應該大家都用過把,有github倉庫,還有本地庫,在項目開發過程中,我們經常會遇到衝突的問題,就是因爲,多個人同時對同一份資源進行了操作。

2.經典案例

代碼模擬業務

大家都搶過票,知道一到春運、過節的時候,票就很難搶,下面我們通過一段代碼,來模擬一下搶票的業務。

package com.cxyxs.thread.six;

/**
 * Description:轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/2/24 14:56
 * Modified By:
 */
public class MyThread implements  Runnable {
    private  int count=50;

    @Override
    public void run() {
        while (count > 0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":  搶到第"+count--+"張");
        }
    }
}
package com.cxyxs.thread.six;

/**
 * Description:轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/2/24 14:58
 * Modified By:
 */
public class Test {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread,"程序猿學社");
        Thread thread1 = new Thread(myThread,"隔壁老王");
        Thread thread2 = new Thread(myThread,"小張");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

測試結果

小張:  搶到第50張
隔壁老王:  搶到第48張
程序猿學社:  搶到第49張
程序猿學社:  搶到第47張
小張:  搶到第46張
隔壁老王:  搶到第45張
程序猿學社:  搶到第44張
隔壁老王:  搶到第43張
小張:  搶到第42張
程序猿學社:  搶到第41張
隔壁老王:  搶到第40張
小張:  搶到第40張
程序猿學社:  搶到第39張
小張:  搶到第38張
隔壁老王:  搶到第38張
程序猿學社:  搶到第37張
隔壁老王:  搶到第36張
小張:  搶到第35張
程序猿學社:  搶到第34張
隔壁老王:  搶到第33張
小張:  搶到第33張
程序猿學社:  搶到第32張
小張:  搶到第31張
隔壁老王:  搶到第30張
程序猿學社:  搶到第29張
小張:  搶到第28張
隔壁老王:  搶到第27張
程序猿學社:  搶到第26張
隔壁老王:  搶到第25張
小張:  搶到第24張
程序猿學社:  搶到第23張
隔壁老王:  搶到第22張
小張:  搶到第21張
隔壁老王:  搶到第20張
程序猿學社:  搶到第19張
小張:  搶到第18張
程序猿學社:  搶到第17張
隔壁老王:  搶到第16張
小張:  搶到第15張
程序猿學社:  搶到第14張
隔壁老王:  搶到第13張
小張:  搶到第12張
隔壁老王:  搶到第11張
程序猿學社:  搶到第10張
小張:  搶到第9張
隔壁老王:  搶到第8張
小張:  搶到第7張
程序猿學社:  搶到第6張
隔壁老王:  搶到第5張
小張:  搶到第4張
程序猿學社:  搶到第3張
隔壁老王:  搶到第2張
程序猿學社:  搶到第1張
小張:  搶到第1張
隔壁老王:  搶到第0

通過上面的測試結果,三個線程,同時搶票,有時候會搶到同一張票?爲什麼會有這種問題發現?
在回答這個問題之前,我們應該瞭解一下 Java 的內存模型(JMM),劃重點,也是面試官經常會問的一個問題。

什麼是JMM?

**JMM(Java Memory Model),**是一種基於計算機內存模型,保證了Java程序在各種平臺下對內存的訪問都能保證效果一致的機制及規範。保證共享內存的原子性、可見性、有序性(這三個也是多線程的三大特性,劃重點,面試經常問)。
本文就瞭解可見性就可。

可見性:

  • 多線程操作共享內存時,執行結果能夠及時的同步到共享內存,確保其他線程對此結果及時可見。
    看到這裏是不是還是有點懵,別急,我們通過圖,把之前搶票的業務畫出來。
    在這裏插入圖片描述
    同一進程下的多個線程,內存資源是共享的。主內存的count纔是共享資源。程序猿學社、隔壁老王、小張,實際上不是直接對主內存的count進行寫入操作。實際上,程序運行過程中,他們每個人,都有各自的工作內存。實際上就是把主內存的count,每個人,都copy一份,對各自的工作內存的變量進行操作。操作完後,再把對應的結果通知到主內存。
  • 再回顧一下我之前git案例。有本地庫(工作內存),有github庫(主內存)。
  • 在多個人同時過程中,組長會新建一個項目,其他的組員,是不是需要把代碼拉取下來,到本地。
  • 我們開發完一個功能後,需要先提交本地庫,再提交到github(把工作內存的結果,提交給github。
  • 提交代碼的時候,我們根本就無法知道,我這份代碼是不是最新的,所有有時候一提交,就報錯(可見性)。

說了這麼多,我們這時候應該知道之前寫的模擬搶票demo爲什麼會有線程安全問題了把。就是因爲各自都操作自己的工作內存,拿到主內存的值就開始操作。假設,這時候count爲40,同一時間,來了三個線程,那這三個線程的工作內存拿到的值都是40,這樣就會導致,這三個線程,都會搶到39這張票。
我們應該如何解決這個問題勒?

怎麼解決線程安全問題?

要想解決線程安全的問題,我們就需要解決一個問題,就是線程之間進行同步交互。瞭解可見性後,我們知道是沒有辦法相互操作對方的工作內存的。
一般有如下幾種方法
synchronized關鍵字(放在方法上)
同步代碼塊
jdk1.5的Lock

synchronized關鍵字(放在方法上)

package com.cxyxs.thread.six;

/**
 * Description:通過同步代碼塊解決線程安全問題
 * 轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/2/24 14:56
 * Modified By:
 */
public class MySynchronizedThread implements  Runnable {
    private  int count=50;

    @Override
    public  void run() {
        while(true){
            buy();
        }

    }
    public synchronized  void  buy(){
        if(count>0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":  搶到第"+count--+"張,"+System.currentTimeMillis());
        }
     }
}
package com.cxyxs.thread.six;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Description:轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/2/24 14:58
 * Modified By:
 */
public class Test {
    public static void main(String[] args) {
        //線程不安全
        //MyThread myThread = new MyThread();
        MySynchronizedThread myThread = new MySynchronizedThread();
        Thread thread = new Thread(myThread,"程序猿學社");
        Thread thread1 = new Thread(myThread,"隔壁老王");
        Thread thread2 = new Thread(myThread,"小張");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

測試結果
在這裏插入圖片描述
通過圖片我們可以發現,同一時間,搶票的間隔差不多都是50ms,爲什麼,不是說多線程嗎(前提不是單核)
因爲在搶票的方法上,增加了synchronized,導致同一時候,只能有一個線程運行,需要等這個線程運行完後,下一個線程才能運行。

  • 可以理解爲,有一個茅坑,裏面有四個坑,隔壁小王這個人,就怕別人偷窺他,直接把進茅坑的們直接鎖上,意思就是我在茅坑的時候,其他的都不能進茅坑,需要等隔壁小王,出來後,其他人才能進入。這樣的結果就會導致,大家都有意見,所以這種方式,一般很少使用。

同步代碼塊

這種方式就是利用synchronized+鎖對象

package com.cxyxs.thread.six;

/**
 * Description:通過同步代碼塊解決線程安全問題
 * 轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/2/24 14:56
 * Modified By:
 */
public class SynchronizedBlockThread implements  Runnable {
    private  int count=50;
    private Object object = new Object();

    @Override
    public  void run() {
        while(true){
            buy();
        }

    }
    public   void  buy(){
        synchronized (object){
            if(count>0){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":  搶到第"+count--+"張,"+System.currentTimeMillis());
            }
        }
     }
}
  • 這種方式相對於前一種方式,性能有提升,只鎖了代碼塊,而不是把這個方法都鎖咯。

jdk1.5的Lock

package com.cxyxs.thread.six;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Description:轉發請註明來源  程序猿學社 - https://ithub.blog.csdn.net/
 * Author: 程序猿學社
 * Date:  2020/2/24 16:52
 * Modified By:
 */
public class LockThread implements Runnable {
    private  int count=50;
    //定義鎖對象
    private Lock lock = new ReentrantLock();
    @Override
    public  void run() {
        while(true){
            buy();
        }

    }
    public   void  buy(){
        lock.lock();
        if(count>0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":  搶到第"+count--+"張,"+System.currentTimeMillis());
        }
        lock.unlock();
    }
}

jdk1.5lock重要的兩個方法

  • lock(): 獲取鎖。
  • unlock():釋放鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章