Java多線程之搞定最後一公裡詳解

緒論

上期介紹瞭多線程的概念、優勢、創建方法以及幾個常用的關鍵字。有瞭之前的基礎過後,我們來討論討論線程安全問題以及其他線程進階知識。

一:線程安全問題

1.1 提出問題

首先,給大傢看一下這個代碼:

public class yy1 {
    private static class Counter {
        private long n = 0;
        public void increment() {
            n++;
        }
        public void decrement() {
            n--;
        }
        public long value() {
            return n;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final int COUNT = 1000_0000;
        Counter counter = new Counter();
        Thread thread = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                counter.increment();
            }
        }, "李四");
        thread.start();
        for (int i = 0; i < COUNT; i++) {
            counter.decrement();
        }
        thread.join();
// 期望最終結果應該是 0
        System.out.println(counter.value());
    }
}

大傢看結果:

大傢觀察下是否適用多線程的現象是否一致?同時嘗試思考下為什麼會有這樣的現象發生呢?

想給出一個線程安全的確切定義是復雜的,但我們可以這樣認為:

如果多線程環境下代碼運行的結果是符合我們預期的,即在單線程環境應該的結果,則說這個程序是線程安全的。

1.2 不安全的原因

1.2.1 原子性

舉個簡單的例子,當我i們買票的時候,如果車站剩餘票數大於0,就可以買。反之,買完一張票後,車站的票數也會自動減一。假設出現這種情況,兩個人同時來買票,隻剩最後一張票,前面那個人把最後一張票買瞭,但是短時間內票數還沒減一也就是清零,這時另外一個人看到還有一張票,於是提交訂單,但是其實已經沒有多餘的票瞭,那麼問題就來瞭。這時我們引入原子性:

我們把一段代碼想象成一個房間,每個線程就是要進入這個房間的人。如果沒有任何機制保證, A 進入房間之後,還 沒有出來; B 是不是也可以進入房間,打斷 A 在房間裡的隱私。這個就是不具備原子性的。 那我們應該如何解決這個問題呢?是不是隻要給房間加一把鎖, A 進去就把門鎖上,其他人是不是就進不來瞭。這樣 就保證瞭這段代碼的原子性瞭。 有時也把這個現象叫做同步互斥,表示操作是互相排斥的。 不保證原子性, 如果一個線程正在對一個變量操作,中途其他線程插入進來瞭,如果這個操作被打斷瞭,結果就可能是錯誤的。

1.2.2 代碼“優化”

一段代碼是這樣的:

1. 去前臺取下 U 盤

2. 去教室寫 10 分鐘作業

3. 去前臺取下快遞

如果是在單線程情況下, JVM 、 CPU 指令集會對其進行優化,比如,按 1->3->2 的方式執行,也是沒問題,可以少跑 一次前臺。這種叫做指令重排序。 剛才那個例子中,單線程情況是沒問題的,優化是正確的,但在多線程場景下就有問題瞭,什麼問題呢。可能快遞是 在你寫作業的10 分鐘內被另一個線程放過來的,或者被人變過瞭,如果指令重排序瞭,代碼就會是錯誤的。

二:如何解決線程不安全的問題

2.1 通過synchronized關鍵字

synchronized 的底層是使用操作系統的 mutex lock 實現的。 當線程釋放鎖時, JMM 會把該線程對應的工作內存中的共享變量刷新到主內存中 當線程獲取鎖時, JMM 會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內 存中讀取共享變量 synchronized 用的鎖是存在 Java對象頭裡的。 synchronized 同步快對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題; 同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入。

鎖的 SynchronizedDemo 對象

public class SynchronizedDemo {
public synchronized static void methond() {
}
public static void main(String[] args) {
method();
// 進入方法會鎖 SynchronizedDemo.class 指向對象中的鎖;出方法會釋放
SynchronizedDemo.class 指向的對象中的鎖
}
}

鎖的 SynchronizedDemo 類的對象

public class SynchronizedDemo {
public synchronized static void methond() {
}
public static void main(String[] args) {
method();
// 進入方法會鎖 SynchronizedDemo.class 指向對象中的鎖;出方法會釋放
SynchronizedDemo.class 指向的對象中的鎖
}
}

明確鎖的對象

public class SynchronizedDemo {
public synchronized static void methond() {
}
public static void main(String[] args) {
method();
// 進入方法會鎖 SynchronizedDemo.class 指向對象中的鎖;出方法會釋放
SynchronizedDemo.class 指向的對象中的鎖
}
}
public class SynchronizedDemo {
public void methond() {
// 進入代碼塊會鎖 SynchronizedDemo.class 指向對象中的鎖;出代碼塊會釋放
SynchronizedDemo.class 指向的對象中的鎖
synchronized (SynchronizedDemo.class) {
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
demo.method();
}
}

2.2 volatile

這裡提一下volatile:

首先,被volatile關鍵字修飾的變量,編譯器與運行時都會註意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證瞭每次讀變量都從內存中讀,跳過 CPU cache 這一步

三:wait和notify關鍵字

3.1 wait方法

其實 wait() 方法就是使線程停止運行。

1. 方法 wait() 的作用是使當前執行代碼的線程進行等待, wait() 方法是 Object 類的方法,該方法是用來將當前線程 置入 “ 預執行隊列 ” 中,並且在 wait() 所在的代碼處停止執行,直到接到通知或被中斷為止。

2. wait() 方法隻能在同步方法中或同步塊中調用。如果調用 wait() 時,沒有持有適當的鎖,會拋出異常。

3. wait() 方法執行後,當前線程釋放鎖,線程與其它線程競爭重新獲取鎖。

public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println(" 等待中 ...");
object.wait();
System.out.println(" 等待已過 ...");
}
System.out.println("main 方法結束 ...");
}

這樣在執行到 object.wait() 之後就一直等待下去,那麼程序肯定不能一直這麼等待下去瞭。這個時候就需要使用到瞭 另外一個方法喚醒的方法 notify() 。

3.2 notify方法

notify 方法就是使停止的線程繼續運行。

  • 1. 方法 notify() 也要在同步方法或同步塊中調用,該方法是用來通知那些可能等待該對象的對象鎖的其它線程,對 其發出通知 notify ,並使它們重新獲取該對象的對象鎖。如果有多個線程等待,則有線程規劃器隨機挑選出一個 呈 wait 狀態的線程。
  • 2. 在 notify() 方法後,當前線程不會馬上釋放該對象鎖,要等到執行 notify() 方法的線程將程序執行完,也就是退出 同步代碼塊之後才會釋放對象鎖。
class MyThread implements Runnable {
    private boolean flag;
    private Object obj;
    public MyThread(boolean flag, Object obj) {
        super();
        this.flag = flag;
        this.obj = obj;
    }
    public void waitMethod() {
        synchronized (obj) {
            try {
                while (true) {
                    System.out.println("wait()方法開始.. " +
                            Thread.currentThread().getName());
                    obj.wait();
                    System.out.println("wait()方法結束.. " +
                            Thread.currentThread().getName());
                    return;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public void notifyMethod() {
        synchronized (obj) {
            try {
                System.out.println("notifyAll()方法開始.. " +
                        Thread.currentThread().getName());
                obj.notifyAll();
                System.out.println("notifyAll()方法結束.. " +
                        Thread.currentThread().getName());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        } else {
            this.notifyMethod();
        }
    }
}
public class TestThread {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        MyThread waitThread1 = new MyThread(true, object);
        MyThread waitThread2 = new MyThread(true, object);
        MyThread waitThread3 = new MyThread(true, object);
        MyThread notifyThread = new MyThread(false, object);
        Thread thread1 = new Thread(waitThread1, "wait線程A");
        Thread thread2 = new Thread(waitThread2, "wait線程B");
        Thread thread3 = new Thread(waitThread3, "wait線程C");
        Thread thread4 = new Thread(notifyThread, "notify線程");
        thread1.start();
        thread2.start();
        thread3.start();
        Thread.sleep(1000);
        thread4.start();
        System.out.println("main方法結束!!");
    }
}

從結果上來看第一個線程執行的是一個 waitMethod 方法,該方法裡面有個死循環並且使用瞭 wait 方法進入等待狀態 將釋放鎖,如果這個線程不被喚醒的話將會一直等待下去,這個時候第二個線程執行的是 notifyMethod 方法,該方 法裡面執行瞭一個喚醒線程的操作,並且一直將 notify 的同步代碼塊執行完畢之後才會釋放鎖然後繼續執行 wait 結束 打印語句。 註意: wait , notify 必須使用在 synchronized 同步方法或者代碼塊內。

3.3 wait和sleep對比(面試常考)

其實理論上 wait 和 sleep 完全是沒有可比性的,因為一個是用於線程之間的通信的,一個是讓線程阻塞一段時間, 唯一的相同點就是都可以讓線程放棄執行一段時間。用生活中的例子說的話就是婚禮時會吃糖,和傢裡自己吃糖之間 有差別。說白瞭放棄線程執行隻是 wait 的一小段現象。 當然為瞭面試的目的,我們還是總結下:

  • 1. wait 之前需要請求鎖,而 wait 執行時會先釋放鎖,等被喚醒時再重新請求鎖。這個鎖是 wait 對像上的 monitor 
  • lock
  • 2. sleep 是無視鎖的存在的,即之前請求的鎖不會釋放,沒有鎖也不會請求。
  • 3. wait 是 Object 的方法
  • 4. sleep 是 Thread 的靜態方法

四:多線程案例

4.1 餓漢模式單線程

class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}

4.2 懶漢模式單線程

class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

4.3 懶漢模式多線程低性能版

class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

4.4懶漢模式-多線程版-二次判斷-性能高

class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

總結

多線程的部分暫時分享到這裡,但其實還有很多沒有沒有涉及 ,等日後深刻理解後再來分享,碼文不易,多謝大傢支持,感激不盡!

到此這篇關於Java多線程之搞定最後一公裡詳解的文章就介紹到這瞭,更多相關Java 多線程內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: