【JAVA併發編程-黑馬】第一章


一、創建線程的幾種方式


二、查看進程的方法


三、線程運行原理–棧楨Debug


棧與棧幀

Java Virtual Machine Stacks (Java 虛擬機棧)

我們都知道 JVM 中由堆、棧、方法區所組成,其中棧內存是給誰用的呢?其實就是線程,每個線程啓動後,虛擬機就會爲其分配一塊棧內存。

  • 每個棧由多個棧幀(Frame)組成,對應着每次方法調用時所佔用的內存
  • 每個線程只能有一個活動棧幀,對應着當前正在執行的那個方法
  • 棧楨以線程爲單位,相互之間是獨立的
public class Test {
    public static void main(String[] args) {
        method1(10);
    }

    public static void method1(int x){
        int y = x + 1;
        Object o = method2();
        System.out.println(o);

    }

    public static Object method2(){
        Object o = new Object();
        return o;
    }
}

下圖,走到斷點時,產生了一個main棧楨,棧楨裏面有一個局部變量:
在這裏插入圖片描述
方法走到下圖標記的位置的時候,有兩個棧楨,method1棧楨是新加入的,也有局部變量:
在這裏插入圖片描述
走到下圖標記的位置時,添加了method2棧楨,有三個棧楨:
在這裏插入圖片描述
走到下圖標記的時候,只有兩個棧在楨了,因爲走完method2,method2棧楨釋放了,同時,也說明一個問題,棧是後進先出的:
在這裏插入圖片描述
debug到這裏已經將問題說明白了,就不再繼續了.


四、線程運行原理圖解


4.1 類加載


加載字節碼文件,將字節碼文件加載到方法區的內存中,這裏爲了好理解,就沒有寫二進制的代碼了,寫的是java代碼.
在這裏插入圖片描述


4.2 啓動main線程


類加載完成後,JVM會啓動main線程,並且分配一塊棧內存給它。接下來這個線程就交給了任務調度器去調度執行,如果搶到CPU了,main方法是方法的執行入口,會給main方法分配一個棧楨內存.
在這裏插入圖片描述
棧內存中有局部變量表、返回地址、鎖記錄、操作數棧。main棧楨的局部變量表是args,返回地址是程序的退出地址。
程序計數器:記錄下一次該執行什麼命令,例如,記錄了下一個執行的方法method1(10)

繼續執行:
在這裏插入圖片描述
在這裏插入圖片描述
現在methd2方法被執行完了,需要釋放掉內存:
在這裏插入圖片描述
然後method1執行結束釋放內存,main執行完成,釋放內存。


五、線程上下文切換(Thread Context Switch)


因爲以下一些原因導致 cpu 不再執行當前的線程,轉而執行另一個線程的代碼

  • 線程的 cpu 時間片用完
  • 垃圾回收
  • 有更高優先級的線程需要運行
  • 線程自己調用了 sleep、yield、wait、join、park、synchronized、lock 等方法
    當 Context Switch 發生時,需要由操作系統保存當前線程的狀態,並恢復另一個線程的狀態,Java 中對應的概念就是程序計數器(Program Counter Register),它的作用是記住下一條 jvm 指令的執行地址,是線程私有的
  • 狀態包括程序計數器、虛擬機棧中每個棧幀的信息,如局部變量、操作數棧、返回地址等
  • Context Switch 頻繁發生會影響性能

六、常用方法


6.1 run和start


  • 直接調用 run 是在主線程中執行了 run,沒有啓動新的線程
  • 使用 start 是啓動新的線程,通過新的線程間接執行 run 中的代碼

6.2 sleep和yield


sleep

  1. 調用 sleep 會讓當前線程從 Running 進入 Timed Waiting 狀態(阻塞)
  2. 其它線程可以使用 interrupt 方法打斷正在睡眠的線程,這時 sleep 方法會拋出 InterruptedException
  3. 睡眠結束後的線程未必會立刻得到執行
  4. 建議用 TimeUnit 的 sleep 代替 Thread 的 sleep 來獲得更好的可讀性

yield

  1. 調用 yield 會讓當前線程從 Running 進入 Runnable 就緒狀態,然後調度執行其它線程
  2. 具體的實現依賴於操作系統的任務調度器

對比
1.就緒狀態,還是有機會被任務調度器調用的,但是阻塞狀態,任務調度器是不會分配時間片給這種狀態的線程的
2.sleep是有具體的等待時間可設置的,而yield幾乎是沒有等待時間。

sleep打斷

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                System.out.println("enter sleep...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        Thread.sleep(1000);
        System.out.println("interrupt...");
        t1.interrupt();
    }

當然,推薦使用這樣的方式進行睡眠,代碼可讀性更好

TimeUnit.SECONDS.sleep(2);

執行結果
在這裏插入圖片描述


6.3 線程優先級

  • 線程優先級會提示(hint)調度器優先調度該線程,但它僅僅是一個提示,調度器可以忽略它
  • 如果 cpu 比較忙,那麼優先級高的線程會獲得更多的時間片,但 cpu 閒時,優先級幾乎沒作用

6.4 sleep方法的一個應用


        while(true) {
            try {
                //Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

上面代碼在單核CPU下運行,CPU會佔用到100%,如果將註釋的代碼放開,即加上sleep方法,CPU只有3%左右。找一臺單核的linux虛擬機,使用top命令查看。

6.5 join方法

join方法:等待線程結束,誰來調用這個方法,就等待誰的線程結束。

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        System.out.println("開始");
        Thread t1 = new Thread(() -> {
            System.out.println("開始");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("結束");
            r = 10;
        });
        t1.start();
        System.out.println("結果爲:" + r);
        System.out.println("結束");
    }

執行結果:
在這裏插入圖片描述
如果我們希望結果是10呢?

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        System.out.println("開始");
        Thread t1 = new Thread(() -> {
            System.out.println("開始");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("結束");
            r = 10;
        });
        t1.start();
        t1.join();
        System.out.println("結果爲:" + r);
        System.out.println("結束");
    }

上面代碼在start之後,添加了join方法,表示等t1線程結果返回,才能繼續往下執行。體現了同步應用

6.6 join同步應用

加入兩個依賴:

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

在測試類上添加註解:

@Slf4j(topic = "c.Test")
    static int r = 0;
    static int r1 = 0;
    static int r2 = 0;
	private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        Thread t2 = new Thread(() -> {
            try {
                sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;
        });
        t1.start();
        t2.start();
        long start = System.currentTimeMillis();
        log.debug("join begin");
        t2.join();
        log.debug("t2 join end");
        t1.join();
        log.debug("t1 join end");
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

執行結果:在這裏插入圖片描述
如果將上面的兩個join方法調用位置,執行結果還是3ms。

6.7 join限時同步

    public static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });

        long start = System.currentTimeMillis();
        t1.start();

        // 線程執行結束會導致 join 結束
        log.debug("join begin");
        t1.join(1000);
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
    }

執行結果:
在這裏插入圖片描述
只在1003ms結束,所以r1的值還是0。
如果將t1.join(3000);
打印的結果爲:
在這裏插入圖片描述
r1的值已經是10了,耗時2000ms,說明join中的參數時間,如果大於線程的執行時間,就以線程執行完畢爲準,如果小於線程執行時間,就以設置的時間爲準,所以是限時同步。

6.8 interrupt打斷阻塞

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打斷標記:{}", t1.isInterrupted());
    }

執行結果:
在這裏插入圖片描述
如果在sleep時被打斷,被標記爲true,但是sleep方法會清除標記,導致標記爲false

視頻中說打斷標記爲false,但是這裏的結果是true,此處存疑!

解惑
觀察上面結果,打斷標記的輸出,在異常拋出之前就輸出了

調試過程
首先,我在catch塊中加入了System.out.println(Thread.currentThread().isInterrupted());發現打印的結果是false,說明打斷標記確實爲false,再結合上面輸出結果,發現:打印語句其實在catch代碼塊執行之前執行了。所以,我們如果想要看到正確的結果,需要在打印語句之後休眠一段時間,完整代碼如下:

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().isInterrupted());
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        Thread.sleep(1000);
        log.debug("打斷標記:{}", t1.isInterrupted());
    }

執行結果如下:
在這裏插入圖片描述

6.9 interrupt打斷正常運行的線程

        Thread t1 = new Thread(() -> {
            while(true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    log.debug("被打斷了, 退出循環");
                    break;
                }
            }
        }, "t1");
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();

執行結果:
在這裏插入圖片描述
使用這種方式可以優雅的終止一個線程,並不是立刻將線程殺死,而是給了線程一個料理後事的機會。

6.10 線程設計模式之兩段終止模式

在一個線程 T1 中如何“優雅”終止線程 T2?這裏的【優雅】指的是給 T2 一個料理後事的機會。

錯誤思路

使用線程對象的 stop() 方法停止線程

  • stop 方法會真正殺死線程,如果這時線程鎖住了共享資源,那麼當它被殺死後就再也沒有機會釋放鎖,其它線程將永遠無法獲取鎖
  • 使用 System.exit(int) 方法停止線程目的僅是停止一個線程,但這種做法會讓整個程序都停止

兩階段終止模式
在這裏插入圖片描述

@Slf4j(topic = "c.Test")
public class Test {
    public static void main(String[] args) throws InterruptedException {
        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();
    }
}

@Slf4j(topic = "c.Test")
class TwoPhaseTermination {
    // 監控線程
    private Thread monitorThread;
    // 停止標記
    private volatile boolean stop = false;
    // 判斷是否執行過 start 方法
    private boolean starting = false;

    // 啓動監控線程
    public void start() {
        synchronized (this) {
            if (starting) { // false
                return;
            }
            starting = true;
        }
        monitorThread = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 是否被打斷
                if (current.isInterrupted()) {
                    log.debug("料理後事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("執行監控記錄");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt();//再次調用,將false變爲true,執行下一次循環時,發現標記是true,執行料理後事的代碼
                }
            }
        }, "monitor");
        monitorThread.start();
    }

    // 停止監控線程
    public void stop() {
        stop = true;
        monitorThread.interrupt();
        System.out.println(Thread.currentThread().isInterrupted());
    }
}

執行結果:
在這裏插入圖片描述
如果在sleep時被打斷,被標記爲true,但是sleep方法會清除標記,導致標記爲false,會拋出異常,進入catch代碼,執行catch代碼後,標記會記爲true。
如果在執行監控記錄時被打斷,不會拋出代碼,打斷標記被記爲true。

6.11 靜態的Thread.interrupted()

  • Thread.interrupted();也是判斷線程是否被打斷,但是它會清除打斷標記
  • isInterrupted方法判斷線程是否被打斷,但是它不會清除打斷標記

6.12 interrupt打斷park

打斷標記爲true的情況下,park會失效。

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打斷狀態:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();
        sleep(1);
        t1.interrupt();

    }

執行結果:
在這裏插入圖片描述
上面的結果可見:輸出park後,由於調用了park方法,暫停了1s,後來執行了t1.interrupt();打斷標記變爲true,導致了park失效,繼續執行後面的代碼。

當然,如果再是打斷標記爲false,park方法立即會生效。
例如將Thread.currentThread().isInterrupted()變爲Thread.currentThread().interrupt()

6.13 過時的方法

  • stop() 停止線程運行
  • suspend() 掛起(暫停)線程運行
  • resume() 恢復線程運行

6.14 守護線程

只要有一個線程運行,整個JAVA進程都不會結束

有一種特殊的線程叫做守護線程,只要其它非守護線程運行結束了,即使守護線程的代碼沒有執行完,也會強制結束。

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
            log.debug("結束");
        }, "t1");
        t1.setDaemon(true);
        t1.start();

        Thread.sleep(1000);
        log.debug("結束");
    }

結果:
在這裏插入圖片描述
主線程結束了,即使t1線程沒有執行完,也會被結束。

  • 垃圾回收器線程就是一種守護線程,如果程序停止了,垃圾回收線程也會被強制停止
  • Tomcat 中的 Acceptor 和 Poller 線程都是守護線程,所以 Tomcat 接收到 shutdown 命令後,不會等待它們處理完當前請求

6.15 線程的五種狀態

這是從 操作系統 層面來描述的
在這裏插入圖片描述

  • 【初始狀態】僅是在語言層面創建了線程對象,還未與操作系統線程關聯
  • 【可運行狀態】(就緒狀態)指該線程已經被創建(與操作系統線程關聯),可以由 CPU 調度執行
  • 【運行狀態】指獲取了 CPU 時間片運行中的狀態
    當 CPU 時間片用完,會從【運行狀態】轉換至【可運行狀態】,會導致線程的上下文切換
  • 【阻塞狀態】如果調用了阻塞 API,如 BIO 讀寫文件,這時該線程實際不會用到 CPU,會導致線程上下文切換,進入【阻塞狀態】等 BIO 操作完畢,會由操作系統喚醒阻塞的線程,轉換至【可運行狀態】。與【可運行狀態】的區別是,對【阻塞狀態】的線程來說只要它們一直不喚醒,調度器就一直不會考慮
    調度它們
  • 【終止狀態】表示線程已經執行完畢,生命週期已經結束,不會再轉換爲其它狀態

6.16 六種狀態

這是從 Java API 層面來描述的
根據 Thread.State 枚舉,分爲六種狀態
在這裏插入圖片描述

  • NEW 線程剛被創建,但是還沒有調用 start() 方法,五種狀態的劃分是重疊的。
  • RUNNABLE 當調用了 start() 方法之後,注意,Java API 層面的 RUNNABLE 狀態涵蓋了 操作系統 層面的
    【可運行狀態】、【運行狀態】和【阻塞狀態】(由於 BIO 導致的線程阻塞,在 Java 裏無法區分,仍然認爲是可運行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 層面對【阻塞狀態】的細分,後面會在狀態轉換一節詳述
  • TERMINATED 當線程代碼運行結束

6.17 六種狀態的演示

public static void main(String[] args) throws IOException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while(true) { // runnable 既有可能分到時間片,又可能沒有分到,都是runable狀態

                }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
        t3.start();

        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized (Test.class) {
                    try {
                        Thread.sleep(1000000); // timed_waiting
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    t2.join(); // waiting  等待t2線程執行完成
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized (Test.class) { // blocked 由於t4線程獲得了鎖,沒有釋放,導致t6一直獲取不到鎖
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 state {}", t1.getState());
        log.debug("t2 state {}", t2.getState());
        log.debug("t3 state {}", t3.getState());
        log.debug("t4 state {}", t4.getState());
        log.debug("t5 state {}", t5.getState());
        log.debug("t6 state {}", t6.getState());
        System.in.read();
    }

執行結果:
在這裏插入圖片描述

6.18 臨界區與競態條件

一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲臨界區
例如,下面代碼中的臨界區

static int counter = 0;
static void increment()
// 臨界區
{
	counter++;
}
static void decrement()
// 臨界區
{
	counter--;
}

競態條件 Race Condition
多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件。

七、線程安全問題分析

使用全局變量list:

public class Test {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}
class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            method2();
            method3();
        }
    }

    private void method2() {
        list.add("1");
    }

    private void method3() {
        list.remove(0);
    }
}

執行結果:
在這裏插入圖片描述
使用局部變量list:

public class Test {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadSafe test = new ThreadSafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
        list.add("1");
    }

    private void method3(ArrayList<String> list) {
        System.out.println(1);
        list.remove(0);
    }
}

這個是線程安全的,沒有報錯。

下面這個例子同樣是使用局部變量,但是method方法是public的,被繼承重寫了:

public class Test {
    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadSafeSubClass test = new ThreadSafeSubClass();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + (i+1)).start();
        }
    }
}

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }

    public void method2(ArrayList<String> list) {
        list.add("1");
    }

    public void method3(ArrayList<String> list) {
        System.out.println(1);
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        System.out.println(2);
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

執行結果:
在這裏插入圖片描述
因爲子類重新開啓了一個線程,和之前的線程共享list,導致了線程安全問題,所以最好就是將method3方法變成私有的,不讓子類重寫。

常見的線程安全類

String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的類

案例分析:

public class MyServlet extends HttpServlet {
	// 是否安全?
	Map<String,Object> map = new HashMap<>();  //no
	// 是否安全?
	String S1 = "..."; //yes
	// 是否安全?
	final String S2 = "..."; //yes
	// 是否安全?
	Date D1 = new Date(); //no
	// 是否安全?
	final Date D2 = new Date(); //no
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
	// 使用上述變量
	}
}

servlet是運行在tomcat上的一個實例,是單實例的,被tomcat多個線程共享使用。

public class MyServlet extends HttpServlet {
	// 是否安全?
	private UserService userService = new UserServiceImpl(); //no
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		userService.update(...);
	}
}
	public class UserServiceImpl implements UserService {
	// 記錄調用次數
	private int count = 0;  //no
	public void update() {
	// ...
	count++;
	}
}
@Aspect
@Component
public class MyAspect {
	// 是否安全?
	private long start = 0L; //no,MyAspect單例,多個線程可能共享這個變量
	@Before("execution(* *(..))")
	public void before() {
		start = System.nanoTime();
	}
	@After("execution(* *(..))")
	public void after() {
		long end = System.nanoTime();
		System.out.println("cost time:" + (end-start));
	}
}

上例最好用環繞通知,做成局部變量。

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl(); //yes,不可變,沒有提供修改
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();//yes,雖然是成員變量,但是沒提供修改
    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全
        try (Connection conn = DriverManager.getConnection("","","")){ //yes
        // ...
        } catch (Exception e) {
        // ...
        }
    }
}
public abstract class Test {
	public void bar() {
	// 是否安全
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		foo(sdf);	
	}
	public abstract foo(SimpleDateFormat sdf);
	public static void main(String[] args) {
		new Test().bar();
	}
}

其中 foo 的行爲是不確定的,可能導致不安全的發生,被稱之爲外星方法

public void foo(SimpleDateFormat sdf) {
	String dateStr = "1999-10-11 00:00:00";
	for (int i = 0; i < 20; i++) {
		new Thread(() -> {
		try {
			sdf.parse(dateStr);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		}).start();
	}
}

上例泄露引用。

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