多線程|深刻淺出線程核心知識

1.多線程簡介

1.1 什麼是線程

線程是操做系統可以進行運算調度的最小單位,它被包含在進程中,是進程中的實際運做單位。程序員能夠經過它進行多處理器編程。你能夠經過使用多線程對運算密集的任務提速。好比,若是一個線程完成一個任務須要100毫秒,那麼用十個線程完成此任務只需10毫秒。Java在語言層面對多線程提供了卓越的支持,他是一個很好的賣點。java

1.2 進程與線程的區別

  • 進程是資源分配的基本單位,線程是調度和執行的基本單位
  • 它們的關係是包含的,一個進程中包含多個線程
  • 當操做系統分配資源時會爲進程分配資源不會爲線程分配資源,線程使用的資源是所在進程的資源
  • 線程間切換的資源開銷比較小,而進程之間切換資源開銷比較大。

1.3 爲何要使用多線程

當一個網站遇到併發量很大的問題時,普通的系統很快就會達到性能瓶頸,而使用多線程能夠輕鬆的解決性能問題。程序員

2.線程的實現方式有幾種?

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started. For example, a thread that computes primes larger than a stated value could be written as follows數據庫

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started. The same example in this other style looks like the following編程

上面的兩段引用出自Oracle官方文檔,將這兩段英文翻譯過來意思就是說實現線程的方式有兩個種,第一種是繼承Thread類的方式,另外一種就是實現Runnable接口。緩存

2.1 實現多線程

繼承Thread類實現多線程

/**
 * 用Thread方式實現線程
 */
public class ThreadStyle extends Thread {
    public static void main(String[] args) {
        ThreadStyle thread = new ThreadStyle();
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("用Thread類實現線程");
    }
}
複製代碼

實現Runnable接口實現多線程

/**
 * Runnable方式建立線程
 */
public class RunnableStyle implements Runnable {
    public static void main(String[] args) {
        Thread thread = new Thread(new RunnableStyle());
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("用Runnable方式實現線程");
    }
}
複製代碼

2.2 多線程實現方式對比

  • 實現Runnable接口方式能夠下降代碼耦合度
  • 繼承Thread類後就沒法再繼承別的類,下降了類的擴展性
  • 若是使用Thread類須要每實現一個線程類就進行一次建立,形成了較大的資源開銷。

總結:綜上所述,實現線程採用Runnable接口的方式比較好。安全

2.3 同時用兩種方式會是怎樣

/**
 * 同時使用Runnable和Thread兩種方式實現多線程
 */
public class BothRunnableThread {

    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();
    }
}
複製代碼

運行結果bash

分析:

實現Runnable接口實現多線程的方式

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

實現Runnable接口方式會要求重寫run()方法,因此會執行其中的三行代碼,其中target是一個private Runnable target;,由於後面又覆蓋了一次Thread類的run方法,因此if判斷也就消失了就會直接執行本身在run方法中的打印語句。多線程

總結:建立線程的方式只有構造Thread類一種方法,可是實現Thread類中的run方法有兩種方式。併發

3.線程的啓動

3.1 啓動線程的正確方式

/**
 * 對比start和run這兩種啓動線程的方式
 */
public class StartAndRunMethod {

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

        new Thread(runnable).start();
    }
}
複製代碼

運行結果dom

總結:啓動線程須要使用start()方法

3.2 start()方法原理解讀

  • start()方法含義

調用start()方法意味着向JVM發起通知,若是有空能夠來我這裏執行一下麼,本質也就是經過調用start()方法請求JVM運行此線程,可是調用該方法以後不必定就會當即運行,而是須要等到JVM有空執行時纔會執行。

  • start()方法源碼解析
public synchronized void start() {
    //進行線程狀態的檢查,默認值是0
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
        
    //加入線程組
    group.add(this);

    boolean started = false;
    try {
        //執行線程的方法,是一個native方法
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
複製代碼

總結:執行start()要經歷的步驟

  • 檢查線程狀態
  • 加入線程組
  • 執行start0()方法

3.3 run()方法原理解讀

@Override
public void run() {
    //判斷傳入的Runnable是否爲空
    if (target != null) {
        //不爲空則啓動
        target.run();
    }
}
複製代碼

從這裏能夠看出上面直接調用run()方法爲何會在主線程中執行,這是由於target是空的因此不會啓動,這樣就和調用一個普通的方法沒有區別了。

4.線程的中止

4.1 線程中止的原則

在Java中中止線程的最好方式是使用interrupt,可是這樣僅僅會對須要中止的線程進行通知而不是直接停掉,線程是否的中止的權利屬於須要被中止的線程(什麼時候中止以及是否中止),這就須要請求中止方和被中止方都遵循一種編碼規範。

4.2 線程一般會在什麼狀況下中止

  • run()方法中的代碼運行完畢(最多見)
  • 有異常出現可是沒有進行捕獲

4.3 正確的中止方法——使用interrupt

  • 在普通狀況下中止
/**
 * run方法內沒有sleep或wait方法時,中止線程
 */
public class RightWayStopThreadWithoutSleep 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();
    }
}
複製代碼
  • 若是線程阻塞如何中止
/**
 * 帶有sleep的中斷線程的中止方法
 */
public class RightWayStopThreadWhthSleep {

    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++;
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}
複製代碼
  • 線程每次迭代後都阻塞如何中止
/**
 * 若是在每次循環中都會sleep或wait,須要如何中止線程
 */
public class RightWayStopThreadWithSleepEveryLoop implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadWithSleepEveryLoop());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }

    @Override
    public void run() {
        try {
            int num = 0;
            while (num <= 30){
                if (num % 10 == 0){
                    System.out.println(num + "是10的倍數");
                }
                num++;
                Thread.sleep(50);
            }
            System.out.println("任務完成了");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

注意:若是每次迭代都有阻塞狀態,這樣就不須要判斷是否收到中斷請求,由於在sleep過程當中會對中斷進行響應

  • whiletry/catch的問題
/**
 * 若是while裏面放try/catch,會致使中斷失效
 */
public class CantInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () ->{
            int num = 0;
            while (num <= 10000){
                if (num % 100 == 0 && !Thread.currentThread().isInterrupted()){
                    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();
    }
}
複製代碼

注意:在while循環中加入try/catch沒法中止線程,由於在try/catch中異常被捕獲後仍是不知足跳出循環的條件,interrupt標記位被清除也就沒法檢查到被中斷的跡象,因此會繼續執行線程

4.4 實際開發中中止的兩種最佳實踐

原則:

  • 優先選擇:傳遞中斷
  • 不想或沒法傳遞:恢復中斷
  • 不該屏蔽中斷

拋出式

/**
 * 最佳實踐:catch住InterruptedException後優先選擇在方法簽名中拋出異常,
 * 那麼在run()方法中就會強制try/catch
 */
public class RightWayStopThreadInProd implements Runnable {
    @Override
    public void run() {
        try {
            while (true){
                System.out.println("go");
                throwInMethod();
            }
        } catch (InterruptedException e) {
            //保存日誌
            //中止程序
            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 RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
複製代碼

注意:在方法中遇到異常應該首先選擇拋出,異常由run方法進行處理,這樣能夠增長代碼的健壯性

恢復中斷式

/**
 * 最佳實踐2:在catch語句中調用Thread.currentThread.interrupt()
 * 來恢復中斷狀態,以便於在後續的執行中依然可以檢查到剛纔發生了中斷
 */
public class RightWayStopThreadInProd2 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 RightWayStopThreadInProd2());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
複製代碼

注意:若是在調用的方法中不拋出異常的話也能夠在catch塊再次調用Thread.currentThread().interrupt();,這樣能夠從新設置中斷表示已經有中斷髮生,從而讓run方法感知

4.5 響應中斷的方法列表總結

  • Object.wait()/wait(long)/wait(long,int)
  • Thread.sleep(long)/sleep(long,int)
  • Thread.join()/join(long)/join(long,int)
  • java.util.concurrent.BlockingQueue.take()/put(E)
  • java.util.concurrent.locks.Lock.lockInterruptibly()
  • java.util.concurrent.CountDownLatch.await()
  • java.util.concurrent.CyclicBarrier.await()
  • java.util.concurrent.Exchanger.exchange(V)
  • java.nio.channels.InterruptibleChannel相關方法
  • java.nio.channels.Selector的相關方法

4.6 錯誤的中止線程方法

  • 使用stop方法中止
/**
 * 錯誤的中止方法:用stop中止線程,會致使線程運行一半忽然中止,這樣沒有辦法完成一個基本單位(一個連隊)的操做,
 * 會形成髒數據(有的連隊多領取或少領取裝備)
 */
public class StopThread implements Runnable {
    @Override
    public void run() {
        /**
         * 模擬指揮軍隊:一個5個連隊,每一個連隊10人,以連隊爲單位發放彈藥,叫到號
         * 的士兵去領取
         */
        for (int i = 0; i < 5; i++) {
            System.out.println("連隊" + i + "開始領取裝備");
            for (int j = 0; j < 10; j++) {
                System.out.println(j);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("連隊" + i + "領取完畢");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        //1s以後戰爭爆發須要奔赴戰場
        Thread.sleep(1000);
        //中止領取
        thread.stop();
    }
}
複製代碼
  • volatile設置boolean標記位
/**
 * 演示用volatile的侷限part2  陷入阻塞時volatile沒法中止
 * 此例中生產者的生產速度很快,可是消費者的消費速度很慢,因此阻塞隊列滿了之後,
 * 生產者會阻塞,生產者會等待消費者進一步消費
 */
public class WrongWayVolatileCantStop {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> storage = new ArrayBlockingQueue<>(10);

        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()){
            System.out.println(consumer.storage.take() + "被消費了");
            Thread.sleep(100);
        }
        System.out.println("消費者不須要更多數據了");

        //一旦消費者不須要更多數據了,咱們應當讓消費者也停下來,可是實際狀況。。。
        producer.canceled = true;
        System.out.println(producer.canceled);
    }
}

class Producer implements Runnable {

    public volatile boolean canceled = false;
    BlockingQueue<Integer> storage;

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

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    //是100的倍數時,將num放入阻塞隊列
                    storage.put(num);
                    System.out.println(num + "是100的倍數,被放入阻塞隊列");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生產者中止運行");
        }
    }
}

class Consumer {

    BlockingQueue<Integer> storage;

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

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }

}
複製代碼

注意:若是線程長時間阻塞,這種方法就會失效

對上面方式的修復

/**
 * 用中斷修復剛纔一直等待的問題
 */
public class WrongWayVolatileFixed {

    public static void main(String[] args) throws InterruptedException {
        WrongWayVolatileFixed body = new WrongWayVolatileFixed();

        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        Producer producer = body.new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);

        Consumer consumer = body.new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消費了");
            Thread.sleep(100);
        }

        System.out.println("消費者不須要更多數據了");

        producerThread.interrupt();

    }
    class Producer implements Runnable {

        BlockingQueue storage;

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

        @Override
        public void run() {
            int num = 0;
            try {
                while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                    //若是num是100的倍數,就將他添加到阻塞隊列
                    if (num % 100 == 0) {
                        storage.put(num);
                        System.out.println(num + "是100的倍數,被放入阻塞隊列");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生產者線程阻塞");
            }
        }
    }

    class Consumer {

        BlockingQueue storage;

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

        public boolean needMoreNums() {

            if (Math.random() > 0.95) {
                return false;
            }
            return true;
        }
    }
}
複製代碼

4.7 沒法響應中斷時如何中止線程

答:須要根據狀況的不一樣而採起不一樣的方法

若是線程阻塞是由於調用了sleep()、wait()或join()致使的,能夠經過拋出InterruptException異常來喚醒,可是不能響應InterruptException異常則沒法經過這種方法進行喚醒。 可是咱們能夠利用其餘能夠響應中斷的方法,好比: ReentrantLock.lockInterruptibly() ,關閉套接字使線程當即返回等方法來達到目的。 因此如何處理不可中斷的阻塞要視狀況而定

5.線程的生命週期

5.1 線程的6種狀態及轉化路徑

  • New(新生):已建立可是尚未啓動的新線程,例如:線程建立出來尚未執行start()方法
  • Runnable(可運行):一旦從New調用了start()方法就會進入Runnable狀態。
  • Blocked(阻塞):當線程進入到被synchronized修飾的代碼後,而且該鎖已經被其它線程拿走了就會進入阻塞狀態。
  • Waiting(等待):沒有設置timeout參數的wait()方法,須要等待喚醒不然不會醒來
  • Timed Waiting(計時等待):爲等待設置了時間,若是在時間結束前進行了喚醒,線程能夠甦醒,若是不進行喚醒,等到時間結束也會自動甦醒。
  • Terminated(死亡):線程執行完成或者run()方法被意外終止。

注意:通常而言把Blocked(被阻塞)、Waiting(等待)、Timed_Waiting(計時等待)都稱爲阻塞,而不只僅是Blocked

6.Thread類與Object類線程相關的方法

6.1 wait()、notify()、notifyAll()方法的使用

做用:wait()方法會讓線程進入等待狀態,若是想讓線程繼續執行必須知足一下四種方式中的一種

  • 調用notify()方法,本線程正好被喚醒
  • 調用notifyAll()方法,全部線程都會被喚醒
  • 設置的wait(long timout)達到了參數的時間,若是傳入0會進入永久等待
  • 調用interrupt()方法進行喚醒
/**
 * 展現wait和notify的基本用法
 * 1.研究代碼執行順序
 * 2.證實wait是釋放鎖的
 */
public class Wait {

    public static Object object = new Object();

    static class Thread1 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"開始執行");

                try {
                    object.wait();  //等待期間若是遇到中斷會拋出InterruptedException
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("線程"+Thread.currentThread().getName()+"獲取到了鎖");
            }
        }
    }

    static class Thread2 extends Thread{
        @Override
        public void run() {
            synchronized (object){
                object.notify();
                System.out.println("線程"+Thread.currentThread().getName()+"調用了notify()");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread1().start();
        Thread.sleep(200);
        new Thread2().start();
    }
}
複製代碼

總結:wait()方法會釋放對象鎖,只有釋放了對象鎖其餘線程才能夠進入synchronized代碼塊

/**
 * 3個線程,線程1和線程2首先被阻塞,線程3去喚醒線程1和線程2
 * start先執行不表明線程先啓動
 */
public class WaitNotifyAll implements Runnable {

    private static final Object resourceA = new Object();

    @Override
    public void run() {
        synchronized (resourceA){
            System.out.println(Thread.currentThread().getName()+"獲得對象鎖");
            try {
                System.out.println(Thread.currentThread().getName()+"等待下一次開始");
                resourceA.wait();
                System.out.println(Thread.currentThread().getName()+"立刻運行結束了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyAll waitNotifyAll = new WaitNotifyAll();
        Thread threadA = new Thread(waitNotifyAll);
        Thread threadB = new Thread(waitNotifyAll);

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA){
                    resourceA.notifyAll();
                    System.out.println("線程C已經成功notify了");
                }
            }
        });
        threadA.start();
        threadB.start();
        Thread.sleep(200);
        threadC.start();
    }
}
複製代碼

總結:notify只會喚醒等待線程中的一個而notifyAll則會喚醒全部等待線程,在線程啓動時必定要等到線程進入等待狀態以後再進行喚醒

/**
 * 證實wait只釋放當前那把鎖
 */
public class WaitNotifyReleaseOwnMonitor {

    private static Object resourceA = new Object();
    private static Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resourceA) {
                    System.out.println("ThreadA got resourceA lock.");
                    synchronized (resourceB) {
                        System.out.println("ThreadA got resourceB lock.");
                        try {
                            System.out.println("ThreadA releases resourceA lock.");
                            resourceA.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resourceA){
                    System.out.println("ThreadB got resourceA lock.");
                    System.out.println("ThreadB tries to ResourceB lock.");
                    synchronized (resourceB){
                        System.out.println("ThreadB got resourceB lock.");
                    }
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}
複製代碼

總結:wait()只釋放當前monitor

6.2 wait方法的原理

最初線程都會在入口集中,當線程進入到 synchronized時就會獲取到對象鎖,若是執行了 wait方法後就會進入到等待集,在等待集中若是對某個線程執行了 notify操做它就會再次回到入口集,若是使用的是 notifyAll那麼等待集中的所有線程都會進入到入口集。

6.3 使用wait方法實現生產者消費者模式

/**
 * 用wait和notify來實現
 */
public class ProducerConsumerModel {

    public static void main(String[] args) {
        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);

        Thread thread1 = new Thread(producer);
        Thread thread2 = new Thread(consumer);
        thread1.start();
        thread2.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();
    }
}
複製代碼

總結:生產者生產到10的時候就會進入wait狀態,不然就會進行生產,消費者將隊列中的商品消費到0時就會進入wait狀態,就這就會消費,當生產者生產出了商品就會notify消費者,將其喚醒。反之消費者就會notify喚醒生產者

6.4 交替打印100之內的奇偶數

  • synchronized方式
/**
 * 兩個線程交替打印0到100奇偶數
 */
public class WaitNotifyPrintOddEvenSyn {

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

    //新建2個線程
    //1個只處理偶數,第2個處理奇數(用位運算)
    //用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);
                            count++;
                        }
                    }
                }
            }
        },"偶數").start();

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

總結:這樣實現有可能形成同一個線程老是拿到對象鎖,可是if判斷只會進一次,這樣就影響了效率,可使用wait/notify方式解決

  • wait/notify方法
/**
 * 兩個線程交替打印0到100的奇偶數,使用wait/notify
 */
public class WaitNotifyPrintOddEvenWait {

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

    //1.一旦拿到鎖就打印
    //2.打印完,喚醒其餘線程,而後再休眠
    static class TurningRunner implements Runnable{

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

        @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();
                        }
                    }
                }
            }
        }
    }
}
複製代碼

總結:拿到鎖就打印,打印完就喚醒若是知足條件就等待,這樣能夠提升程序的效率

6.5 sleep方法詳解

做用:讓線程進入阻塞狀態,睡眠時不會佔用cpu資源。

注意:sleep方法不會釋放synchronizedlock

  • 使用synchronized演示
/**
 * 展現線程sleep的時候不釋放synchronized的monitor,
 * 等sleep的時間到了之後,正常結束後纔會釋放鎖
 */
public class SleepDontReleaseMonitor implements Runnable {

    public static void main(String[] args) {
        SleepDontReleaseMonitor sleepDontReleaseMonitor = new SleepDontReleaseMonitor();
        new Thread(sleepDontReleaseMonitor).start();
        new Thread(sleepDontReleaseMonitor).start();
    }

    @Override
    public void run() {
        syn();
    }

    private synchronized void syn() {
        System.out.println("線程" + Thread.currentThread().getName() + "獲取到了monitor.");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("線程" + Thread.currentThread().getName() + "退出了同步代碼塊.");
    }
}
複製代碼
  • 使用ReentrantLock
/**
 * 演示sleep不釋放lock(lock自己也須要手動釋放)
 */
public class SleepDontReleaseLock implements Runnable {

    private static final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        lock.lock();
        System.out.println("線程" +Thread.currentThread().getName()+ "獲取到了鎖");
        try {
            Thread.sleep(5000);
            System.out.println("線程" +Thread.currentThread().getName()+ "睡眠結束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
        new Thread(sleepDontReleaseLock).start();
        new Thread(sleepDontReleaseLock).start();
    }
}
複製代碼

6.6 對比wait和sleep方法的異同

相同:

  • wait和sleep都會讓線程進入阻塞狀態
  • wait和sleep均可以響應中斷

不一樣:

  • 使用位置不一樣,wait只能在synchronized代碼塊中使用,sleep不須要
  • 所屬的類不一樣,wait屬於Object類,sleep屬於Thread類
  • wait能夠釋放對象鎖,sleep不會釋放對象鎖
  • 阻塞時間長短有差別,wait若是不傳入參數就會永久等待直到對其進行喚醒,sleep只須要等到參數時間到了就會甦醒

6.7 join方法學習

做用:由於新的線程加入了咱們,因此咱們要等他執行完再出發 用法:main等待thread1執行完畢,主線程等待子線程

  • 普通用法
/**
 * 演示join用法,注意語句輸出順序是否會變化
 */
public class Join {

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

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "已經執行完畢");
            }
        });

        thread1.start();
        thread2.start();
        System.out.println("開始等待子線程運行完畢");
//        thread1.join();
//        thread2.join();
        System.out.println("全部子線程執行完畢");
    }
}
複製代碼
  • 在join期間遇到中斷
/**
 * 演示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("子線程已經運行完畢");
    }
}
複製代碼
  • join原理分析

join源碼

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
複製代碼

當調動join方法默認傳入的millis是0,此時會進入永久等待,可是因爲JVM的緣由在Thread類中每一個方法結束都會有一個notify操做,因此join纔不須要手動進行喚醒。

由於join方法的底層是wait因此使用以下代碼能夠與join等價

synchronized (thread1){
    thread1.wait();
}
複製代碼

Thread類的對象加鎖,在結束時會自動進行釋放,因此能夠達到join的效果。

6.8 yield方法詳解

做用:釋放個人CPU時間片,但不會釋放鎖也不會進入阻塞。

定位:JVM不保證遵循yield

  • yield和sleep的區別

sleep期間線程進入阻塞狀態因此不會再被調度。而yield只是暫時做出讓步但還能夠處於競爭狀態。

7.線程各類屬性

7.1 線程id

線程id是不可修改的,主線程的id從1開始

private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}
複製代碼

線程的id是先++後返回,因此主線程的id爲1

查看子線程的id

/**
 * Id從1開始,JVM運行起來以後,咱們本身建立的線程Id早已不是0
 */
public class Id {
    public static void main(String[] args) {
        Thread thread = new Thread();
        System.out.println("主線程的ID:" + Thread.currentThread().getId());
        System.out.println("子線程的ID:" + thread.getId());
    }
}
複製代碼

會發現子線程的id爲11,這是爲何呢? 經過debug的方式能夠發現,main線程啓動後會建立不少線程,因此執行到子線程時id已經不會是2了

7.2 線程名字

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}
複製代碼

經過Thread類的構造方法能夠發現,若是不傳入線程的名字就會默認在Thread-後面添加一個從0開始的數字,由於有synchronized因此不會出現線程重名的狀況。

7.3 守護線程

做用:給用戶線程提供服務(一共有用戶線程和守護線程兩大類)

若是程序中有用戶線程,JVM不會中止工做,可是若是程序中只有守護線程,那麼守護線程也就沒有了守護的對象,因此只有守護線程的狀況下JVM會中止工做。例如:垃圾處理器就是守護線程。

守護線程的3個特性

  • 線程類型默認繼承自父線程
  • 被誰啓動,當程序啓動時main線程是用戶線程,其餘的都是守護線程
  • 不影響JVM退出,若是隻有守護線程JVM會結束

守護線程和用戶線程的區別

由於都是線程因此總體沒什麼區別

  • 主要就是對JVM的影響,在有用戶線程的狀況下JVM不會退出,若是隻有守護線程存在JVM會退出
  • 用戶線程用來執行任務,守護線程服務於用戶線程

應該把用戶線程設置爲守護線程嗎?

不該該。若是把用戶線程設置爲守護線程了,那麼在執行任務時JVM發現此時沒有用戶線程,這樣就會中止虛擬機,從而致使數據不一致的狀況。

7.4 線程優先級

能夠經過設置優先級來增長某個線程的運行次數,優先級最高能夠設置爲10,默認是5,最低是1。

注意:程序設計不該該依賴於優先級

  • 由於不一樣操做系統不同
  • 優先級會被操做系統改變,例如:Windows中有一個優先級推動器,當它發現某個線程執行很積極,那麼他就會越過優先級多爲這個線程分配執行時間。另外一種狀況就是若是將線程的優先級設置的太低,那麼操做系統可能就不爲這個線程分配優先級,這樣線程就有可能被「餓死」。

8.線程異常處理

8.1 爲何要處理線程異常

在程序的運行中有不少異常是不可預料的,若是在返回以前不被攔截而是直接返回給用戶的話這樣可能會引起安全性的問題。

8.2 怎麼處理線程異常

使用UncaughtExceptionHandler

8.3 使用 UncaughtExceptionHandler的3個理由

  • 主線程能夠輕鬆發現異常,子線程卻不行
/**
 * 單線程,拋出,處理,有異常堆棧
 * 多線程狀況下子線程發生異常,會有什麼不一樣?
 */
public class ExceptionInChildThread implements Runnable {

    public static void main(String[] args) {
        new Thread(new ExceptionInChildThread()).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println(i);
        }
    }

    @Override
    public void run() {
        throw new RuntimeException();
    }
}
複製代碼

當子線程拋出異常時,不會對主線程的運行產生影響。在真實的生產環境中由於有大量日誌的產生,可能會忽略子線程中出現的問題。

  • 子線程中的異常沒法用傳統的方法捕獲
    • 不加try-catch時拋出4個異常
/**
 * 1.不加try catch時拋出4個異常,都帶線程名
 * 2.若是加了try catch,但願能夠捕獲到第一個線程的異常並處理,線程234不該該再運行,
 *      但願看到打印出的Caught Exception
 * 3.執行時發現,根本沒有Caught Exception,線程234依然運行,而且還拋出異常
 *
 * 說明線程的異常不能用傳統方法捕獲
 */
public class CantCatchDirectly implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        new Thread(new CantCatchDirectly(), "MyThread-1").start();
        Thread.sleep(300);
        new Thread(new CantCatchDirectly(), "MyThread-2").start();
        Thread.sleep(300);
        new Thread(new CantCatchDirectly(), "MyThread-3").start();
        Thread.sleep(300);
        new Thread(new CantCatchDirectly(), "MyThread-4").start();
    }
    @Override
    public void run() {
        throw new RuntimeException();
    }
}
複製代碼

  • 添加try-catch依然拋出異常
/**
 * 1.不加try catch時拋出4個異常,都帶線程名
 * 2.若是加了try catch,但願能夠捕獲到第一個線程的異常並處理,線程234不該該再運行,
 *      但願看到打印出的Caught Exception
 * 3.執行時發現,根本沒有Caught Exception,線程234依然運行,而且還拋出異常
 *
 * 說明線程的異常不能用傳統方法捕獲
 */
public class CantCatchDirectly implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        try {
            new Thread(new CantCatchDirectly(), "MyThread-1").start();
            Thread.sleep(300);
            new Thread(new CantCatchDirectly(), "MyThread-2").start();
            Thread.sleep(300);
            new Thread(new CantCatchDirectly(), "MyThread-3").start();
            Thread.sleep(300);
            new Thread(new CantCatchDirectly(), "MyThread-4").start();
        } catch (RuntimeException e) {
            System.out.println("Caught Exception");
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        throw new RuntimeException();
    }
}
複製代碼

注意:由於try-catch語句塊添加在了主線程中,可是拋出異常的位置位於子線程,因此沒法捕獲

  • 若是不能捕獲異常子線程會中止

8.4 解決子線程拋出異常問題

  • 方案1(不推薦):手動在每一個run()方法中進行try-catch
@Override
public void run() {
    try {
        throw new RuntimeException();
    } catch (RuntimeException e) {
        System.out.println("Caught Exception");
    }
}
複製代碼

  • 方案2(推薦):利用UncaughtExceptionHandler

8.5 本身實現並處理異常

實現方案

  • 給程序統一設置
  • 給每一個線程單獨設置
  • 給線程池設置

建立MyUncaughtExceptionHandler

/**
 * 實現本身的UncaughtExceptionHandler
 */
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    private String name;
    public MyUncaughtExceptionHandler(String name) {
        this.name = name;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.WARNING, "線程異常,終止啦 : " + t.getName());
        System.out.println(name + " 捕獲了" + t.getName() + "的"+ e +"異常");
    }
}
複製代碼

使用MyUncaughtExceptionHandler

/**
 * 使用本身建立的UncaughtExceptionHandler
 */
public class UseOwnUncaughtExceptionHandler implements Runnable {

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

        Thread.setDefaultUncaughtExceptionHandler(new
                MyUncaughtExceptionHandler("捕獲器1"));
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start();
        Thread.sleep(300);
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start();
        Thread.sleep(300);
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start();
        Thread.sleep(300);
        new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-4").start();

    }

    @Override
    public void run() {
        throw new RuntimeException();
    }
}
複製代碼

9.多線程的缺點

線程是一把雙刃劍,他在提升程序執行效率的同時也會存在一些弊端,好比線程安全問題,這會致使數據發生錯亂,還有就是性能問題,好比服務響應慢、吞吐率低、資源開銷大。使用線程的目的就是爲了讓程序更好的運行,若是這些問題不解決就本末倒置了,在這裏學習一下如何解決吧!

9.1 什麼是線程安全

當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方在進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那麼這個對象就是線程安全的。

9.2 線程安全問題如何避免

線程安全問題主要分爲以下兩種狀況

  • 數據徵用:兩個數據同時去寫,這就有可能致使有一方的數據要麼被丟棄要麼寫入錯誤
  • 競爭條件:競爭條件主要體如今順序上,好比一個線程對文件進行讀取,讀取發生在寫入以前,這就會引發順序上的錯誤。

9.3 線程安全——運行結果問題(i++加的次數會莫名消失)

/**
 * 第一種狀況:運行結果出錯
 * 演示計數不許確(減小),找出具體出錯的位置
 */
public class MultiThreadError implements Runnable {

    private int index = 0;
    static MultiThreadError instance = new MultiThreadError();

    @Override
    public void run() {

        for (int i = 0; i < 10000; i++) {
            index++;
        }
    }

    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);
    }
}
複製代碼

其實每次執行i++操做須要三步,若是線程1執行了 i+1的操做後線程進行了切換,線程2不知道線程1已經執行了 +1操做,依然執行+1,加完以後又切換到線程1,此時線程1的加操做已經完成了,i變成了2,線程2再次執行後,i的值也變成了2,這就是致使i出現少加的狀況的緣由。

9.4 i++錯誤的解決

/**
 * 第一種狀況:運行結果出錯
 * 演示計數不許確(減小),找出具體出錯的位置
 */
public class MultiThreadError implements Runnable {

    private int index = 0;
    static AtomicInteger realIndex = new AtomicInteger();
    static AtomicInteger wrongCount = new AtomicInteger();
    static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
    static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);

    static MultiThreadError instance = new MultiThreadError();
    final boolean[] marked = new boolean[10000000];

    @Override
    public void run() {
        marked[0] = true;
        for (int i = 0; i < 10000; i++) {
            try {
                cyclicBarrier2.reset();
                cyclicBarrier1.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            index++;
            try {
                cyclicBarrier1.reset();
                cyclicBarrier2.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            realIndex.incrementAndGet();
            synchronized (instance){
                if (marked[index] && marked[index-1]){
                    System.out.println("發生了錯誤" + index);
                    wrongCount.incrementAndGet();
                }
                marked[index] = true;
            }
        }
    }

    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());
    }
}
複製代碼

9.5 線程安全——活躍性問題(死鎖、活鎖、飢餓)

死鎖的實現

/**
 * 第二種線程安全問題,演示死鎖
 */
public class MultiThreadError2 implements Runnable {

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

    public static void main(String[] args) {
        MultiThreadError2 r1 = new MultiThreadError2();
        MultiThreadError2 r2 = new MultiThreadError2();
        r1.flag = 1;
        r2.flag = 0;

        new Thread(r1).start();
        new Thread(r2).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){  //想拿到o2卻始終拿不到
                    System.out.println("1");
                }
            }
        }
        if (flag == 0){
            synchronized (o2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1){  //想拿到o1卻始終拿不到
                    System.out.println("0");
                }
            }
        }
    }
}
複製代碼

9.6 線程安全——對象發佈和初始化時的安全問題

什麼是發佈

對象可讓超出範圍以內的類進行使用,好比在方法結束後return 一個對象,從而讓外部可使用這個對象,這就是對象的發佈。

什麼是逸出

逸出是指將對象發佈到了不應發佈的地方,好比:

  • 方法返回一個private的對象(private正常只能在本類中使用)
  • 還未完成初始化(構造函數沒徹底執行完畢)就把對象提供給外界,好比: 構造函數中未初始化完畢就this賦值,隱式逸出——註冊監聽事件,構造函數中運行線程

return私有對象致使的逸出

/**
 * 發佈逸出
 */
public class MultiThreadError3 {
    private Map<String, String> states;
    public MultiThreadError3(){
        states = new HashMap<>();
        states.put("1", "週一");
        states.put("2", "週二");
        states.put("3", "週三");
        states.put("4", "週四");
    }

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

    public static void main(String[] args) {
        MultiThreadError3 multiThreadError3 = new MultiThreadError3();
        Map<String, String> states = multiThreadError3.getStates();
        System.out.println(states.get("1"));
        states.remove("1");
        System.out.println(states.get("1"));
    }
}
複製代碼

private的本意就是不但願外部訪問到,能夠經過 return以後能夠從外部對數據進行修改,這就很危險了!

構造方法未初始化完成就賦值致使逸出

/**
 * 初始化未完畢就this賦值
 */
public class MultiThreadError4 {

    static Point point;

    public static void main(String[] args) throws InterruptedException {
        new PointMaker().start();
        Thread.sleep(10);
        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;
        MultiThreadError4.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();
        }
    }
}
複製代碼

若是將主線程中 sleep的時間增長到100ms以上便不會發生逸出

註冊監聽器致使逸出

/**
 * 觀察者模式
 */
public class MultiThreadError5 {

    int count;
    public MultiThreadError5(MySource source){
        source.registerListener(new EventListener() {
            @Override
            public void onEvent(Event e) {
                System.out.println("\n我獲得的數字是:" + count);
            }
        });
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }

    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 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();

        MultiThreadError5 multiThreadError5 = new MultiThreadError5(mySource);
    }
}
複製代碼

在構造方法中使用線程致使逸出

/**
 * 構造函數中新建線程
 */
public class MultiThreadError6 {

    private Map<String, String> states;

    public MultiThreadError6() {
        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) {
        MultiThreadError6 multiThreadError6 = new MultiThreadError6();
        Map<String, String> states = multiThreadError6.getStates();
        System.out.println(states.get("1"));
    }
}
複製代碼

9.7 逸出的解決——用「副本」替代「真身」(針對返回私有對象)

/**
 * 發佈逸出
 */
public class MultiThreadError3 {
    private Map<String, String> states;
    public MultiThreadError3(){
        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);   //建立一個states副本
    }

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

        System.out.println(multiThreadError3.getStatesImproved().get("1"));
        multiThreadError3.getStatesImproved().remove("1");
        System.out.println(multiThreadError3.getStatesImproved().get("1"));
    }
}
複製代碼

9.8 逸出的解決——巧用工廠模式(能夠修復監聽器註冊的問題)

/**
 * 用工廠模式解決監聽器註冊問題
 */
public class MultiThreadError7 {

    int count;
    private EventListener listener;

    private MultiThreadError7(MySource source){
        listener = new EventListener() {
            @Override
            public void onEvent(MultiThreadError7.Event e) {
                System.out.println("\n我獲得的數字是:" + count);
            }
        };
        for (int i = 0; i < 10000; i++) {
            System.out.print(i);
        }
        count = 100;
    }

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

    static class MySource {
        private MultiThreadError7.EventListener listener;

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

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


    interface EventListener {
        void onEvent(MultiThreadError7.Event e);
    }

    interface Event {

    }

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

        MultiThreadError7 multiThreadError7 = new MultiThreadError7(mySource);
    }
}
複製代碼

10.何時會有線程安全的問題

  • 併發訪問共享的資源時會產生線程安全問題,好比對象屬性、靜態變量、數據庫、共享緩存等
  • 全部依賴時序的操做,即便每步操做都是線程安全的,也會存在併發問題,其中包括:先讀取後修改(這樣讀取到的內容有可能會失效)、先檢查再執行
  • 不一樣的數據之間存在綁定關係的時候,例如對外發布服務端口號和ip地址必須對應,即保證原子性。
  • 在使用其餘類時,不是線程安全的(比例HashMap在併發中可能會出現問題)

11.爲何多線程會有性能問題

  • 調度:上下文切換

    • 上下文切換能夠認爲是內核在CPU上進行的如下活動:(1)掛起一個進程將進程中的狀態(上下文)存儲在內存中的某處。(2)在內存中檢索下一個進程的狀態將它在CPU寄存器中恢復。(3)跳轉到程序計數器所指向的位置以恢復該線程

    • 緩存開銷:CPU從新緩存

    • 什麼時候會致使密集的上下文切換:頻繁競爭鎖或者由於IO頻繁致使阻塞

  • 協做:內存同步

    • 爲了數據的正確性,同步手段每每會使用禁止編譯器優化、使CPU的緩存失效
相關文章
相關標籤/搜索