多線程基礎核心知識

文章首發於公衆號,歡迎訂閱
在這裏插入圖片描述

實現多線程的方法

方法一:繼承Thread

public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        // ......
    }
}

// 使用MyThread
MyThread thread = new MyThread();
thread.start();

方法二:實現Runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // ......
    }
}

// 使用MyRunnable
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

使用方法二的好處是:

  • 從代碼耦合角度看,run方法代碼中的任務應該與Thread類解耦,因此方法二更好。
  • 從資源節約角度看,方法一每次想新建一個任務都需要新建一個獨立的線程,而新建一個獨立的線程損耗比較大,需要創建、執行、銷燬。使用方法二的話,後續可以使用線程池這樣的工具提高性能。
  • 方法一不支持多繼承,如果線程類已經有一個父類了,這時不能再繼承自Thread類了,因爲 Java 不支持多繼承,但是可以實現Runnable接口來處理。

兩種方法本質對比:

  • 方法一:最終調用的是target.run()
  • 方法二:最終調用的是我們自己重寫的run()

問題:同時使用方法一和方法二會發生什麼?

請看如下代碼:

public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("我來自Runnable");
        }
    }) {
        @Override
        public void run() {
            System.out.println("我來自Thread");
        }
    }.start();
}
輸出:
我來自Thread

爲什麼只執行下面這個?如果沒有下面這個,那麼必然執行上面 run 方法,因爲在 Thread 類中的 run 方法中,有這樣三行代碼:

private Runnable target;

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

此時 target 確實不爲 null,那麼就執行上面的 run 方法。但是如果下面用戶重寫了 Threadrun 方法,那麼此時就執行子類的 run 方法了。

準確說,創建線程只有一種方式那就是構造Thread類,而實現線程的執行單元有兩種方式:

  • 方法一:實現 Runnable 接口的 run 方法,並把 Runnable 實例傳給 Thread
  • 方法二:繼承 Thread 類,重寫 Thread類 的 run 方法

線程的啓動

請看如下代碼:

public static void main(String[] args) {
    Runnable runnable = () -> {
        System.out.println(Thread.currentThread().getName());
    };

    // run啓動
    runnable.run();

    // start啓動
    new Thread(runnable).start();
}
輸出:
main
Thread-0

start() 方法原理解讀

  1. 啓動新線程檢查線程狀態
  2. 加入線程組
  3. 調用start0()

run() 方法原理解讀

上面提到的三行代碼

問題:一個線程兩次調用start方法會出現什麼情況?爲什麼?

會拋出異常,原因是start方法會檢查線程狀態threadStatusthreadStatus初始值爲0,啓動後threadStatus就不是 0 了,如果第二次繼續調用start方法的話,由於threadStatus不爲 0 就會拋出異常。

線程的停止

停止線程使用interrupt來通知,而不是強制。

線程停止的三種情況

  • 一般情況

通過Thread.currentThread().isInterrupted()方法來判斷。

public class S1 implements Runnable {

    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE / 2) {
            if (num % 10000 == 0) {
                System.out.println(num + "是10000的倍數");
            }
            num++;
        }
        System.out.println("任務運行結束了");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
    }
}
  • 線程被阻塞的情況

通過try...catch捕獲可以響應interrupt中斷的方法。

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () -> {
        int num = 0;
        try {
            while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍數");
                }
                num++;
            }
            // 可以響應interrupt中斷
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(500);
    thread.interrupt();
}
  • 線程在每次迭代後阻塞

和上面相比,可以省略!Thread.currentThread().isInterrupted()的判斷。

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () -> {
        int num = 0;
        try {
            while (num <= 10000) {
                if (num % 100 == 0) {
                    System.out.println(num + "是100的倍數");
                }
                num++;
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(5000);
    thread.interrupt();
}

下面這種情況是錯誤的,while 內 try…catch 會導致線程無法停止。

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = () -> {
        int num = 0;
        while (num <= 10000 && !Thread.currentThread().isInterrupted()) {
            if (num % 100 == 0) {
                System.out.println(num + "是100的倍數");
            }
            num++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    Thread thread = new Thread(runnable);
    thread.start();
    Thread.sleep(5000);
    thread.interrupt();
}

因爲當 sleep 方法響應中斷後,就會清除中斷標誌,所以 while 中判斷無法退出。

解決方法是在 catch 中加入一行Thread.currentThread().interrupt();

最佳實踐

  • 傳遞中斷
public class S1 implements Runnable {

    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()) {
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("保存日誌");
                e.printStackTrace();
            }
        }
    }

    private void throwInMethod() throws InterruptedException {
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new S1());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
  • 恢復中斷
public class S2 implements Runnable {

    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted,程序運行結束");
                break;
            }
            reInterrupt();
        }
    }

    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new S2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

可以響應中斷的方法

  1. Object.wait
  2. Thread.sleep
  3. Thread.join
  4. java.util.concurrent.BlockingQueue.take/put
  5. java.util.concurrent.locks.Lock.locklnterruptibly
  6. java.util.concurrent.CountDownLatch.await
  7. java.util.concurrent.CyclicBarrier.await
  8. java.util.concurrent.Exchanger.exchange
  9. java.nio.channels.InterruptibleChannel
  10. java.nio.channels.Selector

線程停止的一種錯誤方式

這裏我們不談stop/suspend,因爲這兩個方法已經被禁用。

我們講的是用volatile設置boolean標記位的方法。這個方法在一般情況下使用是沒有問題的,但是如果用在比如像消費者生產者這種場景下,由於BlockingQueue.take/put方法有可能會一直阻塞,從而導致線程無法停止,而take/put方法是可以響應interrupt中斷的,因此只推薦使用interrupt方法作爲線程停止的方式。

如何處理不可中斷的阻塞

按照特定的方法,比如像ReentrantLock本身的lock方法無法響應中斷,但是可以使用能響應中斷的lockInterruptibly方法。

線程的生命週期

線程的 6 個狀態:

  • New 已創建但還尚未啓動的新線程
  • Runnable 可運行
  • Blocked 被阻塞
  • Waiting 等待
  • Timed waiting 限期等待
  • Terminated 終止

轉化示意圖如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kGBxVe9A-1586831784737)(https://i.loli.net/2020/02/20/XUTgNWwmzhC9oqR.png)]

一般而言,把 Blocked (被阻塞)、Waiting (等待)、Timed_waiting (計時等待)都稱爲阻塞狀態

Thread 和 Object 類中的重要方法詳解

wait、notify、notifyAll 方法

wait 方法遇到以下 4 種情況時會被喚醒:

  • 另一個線程調用這個對象的 notify 方法且剛好被喚醒的是本線程;
  • 另一個線程調用這個對象的 notifyAll 方法;
  • 過了wait(time) 規定的超時時間,如果傳入 0 就是永久等待;
  • 線程自身調用了 interrupt

notify 方法只應該被擁有該對象 monitor 的線程調用。一旦線程被喚醒,線程便會從對象的“等待線程集合”中被移除,所以可以重新參與到線程調度當中。

案例:

1、生產者消費者模型

public class ProducerConsumerModel {

    public static void main(String[] args) {

        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {

    private EventStorage storage;

    public Producer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

class Consumer implements Runnable {

    private EventStorage storage;

    public Consumer(EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

class EventStorage {

    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    public synchronized void put() {
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("倉庫裏有了" + storage.size() + "個產品。");
        notify();
    }

    public synchronized void take() {
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了" + storage.poll() + ",現在倉庫還剩下" + storage.size());
        notify();
    }
}

2、兩個線程交替打印0~100的奇偶數

  • synchronized 關鍵字實現
public class PrintOddEvenSyn1 {

    private static int count;

    private static final Object lock = new Object();

    // 新建2個線程
    // 1個只處理偶數,第二個只處理奇數(用位運算)
    // 用synchronized來通信
    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        // 位運算提高效率
                        if ((count & 1) == 0) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                        }
                    }
                }
            }
        }, "偶數").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (count < 100) {
                    synchronized (lock) {
                        if ((count & 1) == 1) {
                            System.out.println(Thread.currentThread().getName() + ":" + count++);
                        }
                    }
                }
            }
        }, "奇數").start();
    }
}

這種方法雖然能實現功能,但是效率太低,因爲每次都是第一個線程或者第二個線程中的其中一個搶到鎖,有可能其中一個一直搶到鎖,那麼就白白等待浪費時間。

  • waitnotify 方法實現
public class PrintOddEvenSyn2 {

    private static int count = 0;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(new TurningRunner(), "偶數").start();
        new Thread(new TurningRunner(), "奇數").start();
    }

    // 1.拿到鎖,我們就打印
    // 2.打印完,喚醒其他線程,自己就休眠
    static class TurningRunner implements Runnable {

        @Override
        public void run() {
            while (count <= 100) {
                synchronized (lock) {
                    // 拿到鎖就打印
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    lock.notify();
                    if (count <= 100) {
                        try {
                            // 如果任務還沒結束,就讓出當前的鎖,並休眠
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

問題:爲什麼線程通信的 wait、notify 和 notifyAll 被定義在 Object 類中,而 sleep 被定義在 Thread 類中?

因爲 waitnotifynotifyAll 是鎖級別的操作,而鎖是綁定到某個對象中的,而不是綁定到線程 Thread。我們經常會遇到某個線程持有多個鎖,並且這些鎖之間是相互配合的,如果把 waitnotifynotifyAll 方法定義在 Thread 中,那麼就無法靈活配合了。

sleep 方法

sleep 方法可以讓線程進入 Waiting 狀態,並且不佔用 CPU 資源,但是不會釋放鎖,直到規定時間後再執行,休眠期間如果被中斷,會拋出異常並清除中斷標誌。

sleep 方法和 wait 方法比較

相同:

  • 都會阻塞當前線程
  • 都可以響應中斷

不同:

  • wait 必須在同步方法中執行,而 sleep 不需要
  • wait 會釋放鎖,而 sleep 不會
  • wait 可以不傳入參數,表示直到自己被喚醒,而sleep 必須傳入參數
  • wait 屬於 Object 類,sleep 屬於 Thread

join 方法

在很多情況下,主線程創建並啓動子線程,如果子線程中要進行大量的耗時運算,主線程往往將早於子線程結束之前結束。這時,如果主線程想等待子線程執行完成之後再結束,比如子線程處理一個數據,主線程要取得這個數據中的值,就要用到 join 方法。

join 方法的作用是使所屬的線程對象 x 正常執行 run 方法中的任務,而使當前線程 z 進行無限期的阻塞 ,等待線程 x 銷燬後再繼續執行線程 z 後面的代碼。

join 期間,當前線程處於 Waiting 狀態。

在部使用 wait 方法進行等待,synchronized 使用的是對象監視器做同步。

join(time)sleep(time) 的區別:join 內部使用 wait 等待,最終會底層會調用 notifyAll 釋放鎖,而 sleep 不能。

join 過程中,如果當前線程對象被中斷則當前線程出現異常。

public class JoinInterrupt {
    
    public static void main(String[] args) {
        
        Thread mainThread = Thread.currentThread();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 主線程被中斷
                    mainThread.interrupt();
                    Thread.sleep(5000);
                    System.out.println("Thread1 finished.");
                } catch (InterruptedException e) {
                    System.out.println("子線程中斷");
                }
            }
        });
        thread1.start();
        System.out.println("等待子線程運行完畢");
        try {
            thread1.join();
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName()+"主線程中斷了");
            // 由於主線程已經中斷,所以讓子線程也中斷防止發生意外
            thread1.interrupt();
        }
        System.out.println("子線程已運行完畢");
    }
}

synchronized + wait 實現 join 的功能:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "執行完畢");
        }
    });

    thread.start();
    System.out.println("開始等待子線程運行完畢");
    // thread.join();
    
    // 使用Thread對象作爲鎖
    synchronized (thread) {
    	thread.wait();
    }
    System.out.println("所有子線程執行完畢");
}

這是因爲 Thread 對象會在自己的線程死亡時調用 this.notifyAll(),因此一般不建議使用 Thread 的對象作爲鎖。

yield 方法

作用:釋放自己剩餘的時間片。JVM 不保證遵循,因此我們一般不使用。

sleepyield 方法的區別在於,當線程調用 sleep 方法時調用線程會被阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程 。 而調用 yield 方法時,線程只是讓出自己剩餘的時間片,並沒有被阻塞掛起,而是處於就緒狀態,線程調度器下一次調度時就有可能調度到當前線程執行 。

線程屬性

線程 ID

標識不同的線程,無法修改。

線程 Name

清晰有意義的名字。

守護線程(Daemon)

守護線程是一種特殊的線程,典型的守護線程就是垃圾回收線程,當進程中不存在非守護線程了,則垃圾回收線程就會自動銷燬。當最後一個非守護線程結束,守護線程才隨着 JVM 一同結束工作。

普通線程和守護線程區別:

普通線程是執行我們的邏輯的,守護線程是服務於我們的,且隨着 JVM 一起結束工作。

線程優先級

Java 中線程的優先級分爲 1~10 這 10 個等級,如果小於 1 或大於 10,那麼會拋出異常。

使用 3 個常量來預置定義優先級的值

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
  • 繼承性

在 Java 中,線程優先級具有繼承性,當 A 線程啓動 B 線程,那麼 B 線程的優先級和 A 線程相同。當優先級被修改後再繼承的話,會繼承修改後的優先級

  • 規則性

高優先級總是大部分先執行完

  • 隨機性

高優先級總是大部分先執行完,但不代表高優先級一定全部都先執行完

線程的異常處理

對於 checked exception 我們一般在程序中直接 try…catch 即可,但是遇到 unchecked exception,我們就需要自定義一個異常處理器專門去處理非檢查型異常,示例如下:

public class MyThreadExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("An exception has been captured");
        System.out.printf("Exception: %s: %s\n", e.getClass().getName(), e.getMessage());
        System.out.println("Stack Trace");
        e.printStackTrace(System.out);
        System.out.printf("Thread status: %s\n", t.getState());
    }
}

public class MyThread implements Runnable {

    @Override
    public void run() {
        int a = Integer.parseInt("a");
        System.out.println(a);
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.setUncaughtExceptionHandler(new MyThreadExceptionHandler());
        thread.start();
    }
}

//輸出
An exception has been captured
Exception: java.lang.NumberFormatException: For input string: "a"
Stack Trace
java.lang.NumberFormatException: For input string: "a"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.parseInt(Integer.java:615)
	at t10.MyThread.run(MyThread.java:11)
	at java.lang.Thread.run(Thread.java:748)
Thread status: RUNNABLE

線程組的異常處理如下:

public class MyThreadGroup extends ThreadGroup {
    public MyThreadGroup(String name) {
        super(name);
    }

    // ThreadGroup默認已經實現了Thread.UncaughtExceptionHandler
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        super.uncaughtException(t, e);
        // 一旦線程組中有線程發生異常,就中斷整個線程組
        this.interrupt();

    }
}
public class MyThread extends Thread {
    private String num;

    public MyThread(ThreadGroup group, String name, String num) {
        super(group, name);
        this.num = num;
    }

    @Override
    public void run() {
        int numInt = Integer.parseInt(num);
        while (this.isInterrupted() == false) {
            System.out.println("while循環中:" + Thread.currentThread().getName());
        }
    }
}
public class Run {
    public static void main(String[] args) {
        MyThreadGroup group = new MyThreadGroup("我的線程組");
        MyThread[] myThread = new MyThread[10];
        for (int i = 0; i < myThread.length; i++) {
            myThread[i] = new MyThread(group, "線程" + (i + 1), "1");
            myThread[i].start();
        }
        MyThread thread = new MyThread(group, "報錯線程", "a");
        thread.start();
    }
}

只要有一個線程發生異常,那麼此線程組中的所有線程都會中斷。

多線程安全問題

什麼是線程安全

當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在 ,調用這個對象的行爲都可以獲得正確的結果,那這個對象是線程安全的。

多線程安全問題三要素

  • 是否存在多線程環境
  • 是否存在共享的數據
  • 是否有多條語句同時操作這條數據

案例1-計數出錯

public class CountingError implements Runnable {

    static final CountingError INSTANCE = new CountingError();
    int index = 0;
    static AtomicInteger realIndex = new AtomicInteger();
    static AtomicInteger wrongCount = new AtomicInteger();
    static volatile CyclicBarrier cyclicBarrier = new CyclicBarrier(2);

    final boolean[] marked = new boolean[10000000];

    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(INSTANCE);
        Thread thread2 = new Thread(INSTANCE);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("表面上結果是" + INSTANCE.index);
        System.out.println("真正運行的次數" + realIndex.get());
        System.out.println("錯誤次數" + wrongCount.get());

    }

    @Override
    public void run() {
        marked[0] = true;
        // 等待直到兩個線程都忙完了再繼續下一次
        for (int i = 0; i < 100000; i++) {
            try {
                cyclicBarrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
            index++;
            realIndex.incrementAndGet();
            synchronized (INSTANCE) {
                // 由於synchronized也保證線程可見性,所以只憑marked[index]無法判斷,需要根據前一位的
                // 舉例:
                // 假設沒有發生多線程問題,即index++沒有問題
                // 線程2先使得index++,線程2 index爲1,線程1 index爲2
                // 線程1先進入synchronized代碼塊,使得marked[2] = true;
                // 由於synchronized間接保證可見性,線程2此時看到的index爲2。那麼marked[index]就會誤判錯誤,其實應該判斷marked[index - 1]
                if (marked[index] && marked[index - 1]) {
                    System.out.println("發生錯誤" + index);
                    wrongCount.incrementAndGet();
                }
                marked[index] = true;
            }
        }
    }
}

案例2-死鎖

死鎖的產生具備以下四個條件:

  • 互斥條件:指線程對己經獲取到的資源進行排它性使用, 即該資源同時只由一個線程佔用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至佔有資源的線程釋放該資源。
  • 請求並持有條件: 指一個線程己經持有了至少一個資源 , 但又提出了新的資源請求 ,而新資源己被其他線程佔有,所 以當前線程會被阻塞,但阻塞的同時並不釋放自己己經獲取的資源。
  • 不可剝奪條件: 指線程獲取到的資源在自己使用完之前不能被其他線程搶佔,只有在自己使用完畢後才由自 己釋放該資源。
  • 環路等待條件:指在發生死鎖時,必然存在一個線程→資源的環形鏈,即線程集合 { T0,T1,T2 ,…,Tn} 中的 T0 正在等待一個 T1 佔用的資源,T1 正在等待 T2 佔用的資源,……Tn 正在等待己被 T0 佔用的資源。

示例1:

public class DeadLockThread implements Runnable {
    public String name;
    public Object lock1 = new Object();
    public Object lock2 = new Object();

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        if (name.equals("a")) {
            synchronized (lock1) {
                System.out.println("name=" + name);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("lock1->lock2");
                }
            }
        }
        if (name.equals("b")) {
            synchronized (lock2) {
                System.out.println("name=" + name);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("lock2->lock1");
                }
            }
        }

    }
}

public class Run {
    public static void main(String[] args) {
        try {
            DeadLockThread deadLockThread = new DeadLockThread();
            deadLockThread.setName("a");
            Thread thread1 = new Thread(deadLockThread);
            thread1.start();
            Thread.sleep(1000);
            deadLockThread.setName("b");
            Thread thread2 = new Thread(deadLockThread);
            thread2.start();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

示例2:

public class DeadLockThread implements Runnable {

    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        DeadLockThread d1 = new DeadLockThread();
        DeadLockThread d2 = new DeadLockThread();
        d1.flag = 1;
        d2.flag = 0;
        new Thread(d1).start();
        new Thread(d2).start();
    }

    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("0");
                }
            }
        }
    }
}

對象逸出

1、方法返回一個 private 對象(private 的本意是不讓外部訪問)

public class Error1 {

    private Map<String, String> states;

    public Error1() {
        states = new HashMap<>();
        states.put("1", "週一");
        states.put("2", "週二");
        states.put("3", "週三");
        states.put("4", "週四");
    }

    public Map<String, String> getStates() {
        return states;
    }

    public Map<String, String> getStatesImproved() {
        return new HashMap<>(states);
    }

    public static void main(String[] args) {
        Error1 error1 = new Error1();
        Map<String, String> states = error1.getStates();
        
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
}

2、還未完成初始化(構造函數沒完全執行完畢)就把對象提供給外界

  • 在構造函數中未初始化完畢就進行 this 賦值
public class Error2 {

    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        // 因爲時間不多,導致結果不同
        //        Thread.sleep(10);
        Thread.sleep(105);
        if (point != null) {
            System.out.println(point);
        }
    }
}

class Point {

    private final int x, y;

    public Point(int x, int y) throws InterruptedException {
        this.x = x;
        Error2.point = this;
        Thread.sleep(100);
        this.y = y;
    }

    @Override
    public String toString() {
        return x + "," + y;
    }
}

class PointMaker extends Thread {

    @Override
    public void run() {
        try {
            new Point(1, 1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 隱式逸出—註冊監聽事件
public class Error3 {

    int count;

    public Error3(MySource source) {
        source.registerListener(new EventListener() {
            @Override
            public void onEvent(Event e) {
                // 0
                System.out.println("\n我得到的數字是" + count);
            }

        });
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }

    public static void main(String[] args) {
        MySource mySource = new MySource();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mySource.eventCome(new Event() {
                });
            }
        }).start();
        Error3 error3 = new Error3(mySource);
    }

    static class MySource {

        private EventListener listener;

        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }

        void eventCome(Event e) {
            if (listener != null) {
                listener.onEvent(e);
            } else {
                System.out.println("還未初始化完畢");
            }
        }

    }

    interface EventListener {

        void onEvent(Event e);
    }

    interface Event {

    }
}
  • 構造函數中運行線程
public class Error4 {

    private Map<String, String> states;

    public Error4() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                states = new HashMap<>();
                states.put("1", "週一");
                states.put("2", "週二");
                states.put("3", "週三");
                states.put("4", "週四");
            }
        }).start();
    }

    public Map<String, String> getStates() {
        return states;
    }

    public static void main(String[] args) throws InterruptedException {
        Error4 error4 = new Error4();
        // 如果沒有等待,就會因爲還沒有初始化完成導致空指針異常
        Thread.sleep(1000);
        System.out.println(error4.getStates().get("1"));
    }
}

解決這兩類逸出的方法:

1、返回副本

public class Solution1 {

    private Map<String, String> states;

    public Solution1() {
        states = new HashMap<>();
        states.put("1", "週一");
        states.put("2", "週二");
        states.put("3", "週三");
        states.put("4", "週四");
    }

    public Map<String, String> getStates() {
        // 返回states的副本
        return new HashMap<>(states);
    }

    public static void main(String[] args) {
        Solution1 solution1 = new Solution1();

        System.out.println(solution1.getStates().get("1"));
        solution1.getStates().remove("1");
        System.out.println(solution1.getStates().get("1"));
    }
}

2、工廠模式

隱式逸出—註冊監聽事件修改爲工廠模式:

public class Error3Fixed {

    int count;
    private EventListener listener;

    private Error3Fixed(MySource source) {
        listener = new EventListener() {
            @Override
            public void onEvent(Error3Fixed.Event e) {
                System.out.println("\n我得到的數字是" + count);
            }

        };
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }

    public static Error3Fixed getInstance(MySource source) {
        Error3Fixed safeListener = new Error3Fixed(source);
        source.registerListener(safeListener.listener);
        return safeListener;
    }

    public static void main(String[] args) {
        MySource mySource = new MySource();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                mySource.eventCome(new Error3Fixed.Event() {
                });
            }
        }).start();
        Error3Fixed error3Fixed = Error3Fixed.getInstance(mySource);
    }

    static class MySource {

        private EventListener listener;

        void registerListener(EventListener eventListener) {
            this.listener = eventListener;
        }

        void eventCome(Error3Fixed.Event e) {
            if (listener != null) {
                listener.onEvent(e);
            } else {
                System.out.println("還未初始化完畢");
            }
        }

    }

    interface EventListener {

        void onEvent(Error3Fixed.Event e);
    }

    interface Event {

    }
}

需要考慮線程安全的情況

1、訪問共享的變量或資源,會有併發風險,比如對象的屬性、靜態變量、共享緩存、數據庫等。

2、所有依賴時序的操作,即使每一步操作都是線程安全的,還是存在併發問題。

  • read-modify-write 操作:一個線程讀取了一個共享數據,並在此基礎上更新該數據。

  • check-then-act 操作:一個線程讀取了一個共享數據,並在此基礎上決定其下一個的操作。

3、不同的數據之間存在捆綁關係的時候,如 IP 和端口號。

4、我們使用其他類的時候,如果對方沒有聲明自己是線程安全的,那麼大概率會存在併發問題比如 HashMap 沒有聲明自己是併發安全的,所以我們併發調用 HashMap 的時候會出錯。

多線程帶來的性能問題

調度-上下文切換

在多線程編程中,線程個數一般都大於 CPU 個數,而每個 CPU 同一時刻只能被一個線程使用,爲了讓用戶感覺多個線程是在同時執行的, CPU 資源的分配採用了時間片輪轉的策略 ,也就是給每個線程分配一個時間片,線程在時間片內佔用 CPU 執行任務。當前線程使用完時間片後,就會處於就緒狀態並讓出 CPU 讓其他線程佔用 , 這就是上下文切換 ,從當前線程的上下文切換到了其他線程 。 那麼就有一個問題,讓出 CPU 的線程等下次輪到自己佔有 CPU 時如何知道自己之前運行到哪裏了?所以在切換線程上下文時需要保存當前線程的執行現場 , 當再次執行時根據保存的執行現場信息恢復執行現場 。

線程上下文切換時機有:當前線程的 CPU 時間片使用完處於就緒狀態時,當前線程被其他線程中斷時。

如何減少上下文切換

減少上下文切換的方法有無鎖併發編程、CAS 算法、使用最少線程以及使用協程。

  • 無鎖併發編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以使用一些辦法來避免使用鎖,如將數據的 ID 按照 Hash 算法取模分段,不同的線程處理不同段的數據。
  • CAS 算法。Java 的 Atomic 包使用 CAS 算法來更新數據,而不需要加鎖。
  • 使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。
  • 使用協程。在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

協作-內存同步

爲了數據的正確性,同步手段往往會使用禁止編譯器優化、使 CPU 內的緩存失效。


我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章