Java線程學習筆記

一. 概述

使用的線程的目的有如下幾點:

  1. 異步。所謂異步,字義上來講就是同時做多個不同的事。
    例如,你正在和戀人聊QQ,而此時你正在發送一個文件,如果收發消息和上傳文件在同一個線程,那么當你發送文件開始,你便需要等待文件上傳完成后,才能發送下一條消息,如果再加上文件太大、網速很差等等因素,就可能導致你和你戀人在幾個小時內只說了一句話,那你們之間可能就GG了。
  1. 并發。所謂并發,字義上來講就是同時做多個相同的事。
    例如,雙11上淘寶買東西,在同一時間內,阿里的服務器收到一多個購買的請求,如果處理這些請求的方式是處理完一條再處理下一條的話,那么你買個衣服坑你就要等到地老天荒了。

雖然我在這里將線程的目的分成了兩種,但它們的本質是一樣:在同一時間做多個不同的事。值得注意的是,如果你電腦的CPU是單核單線程的話,這個“同一時間”是有事件差的,可能CPU這一毫秒在執行線程A,下一毫秒在執行線程B,但完全同一時間同時執行線程A和線程B是不可能的。

二. 如何創建線程

實現線程有兩種方式,涉及到的類有兩個,分別是:java.lang.Thread和java.lang.Runnable。創建并啟動線程的方式如下:

Thread thread = new Thread();//創建線程實例
thread.start();//啟動線程

但是這僅僅是創建了一條新的線程并啟動了它,該線程并不會執行任何邏輯,那么我們怎么讓它執行相關邏輯呢?

方式1:繼承Thread類,重寫run方法。

public class DemoThread extends Thread {
    public void run() {
        //TODO 線程中需要執行的相關邏輯
    }

    public static void main(String[] args) {
        Thread thread = new DemoThread();//創建線程實例
        thread.start();//啟動線程
    }
}

方式二:實現Runnable接口,并在Thread構造方法中傳入實現的Runnable實例。

public class DemoRunnable implements Runnable {
    public void run() {
        //TODO 線程中需要執行的相關邏輯
    }

    public static void main(String[] args) {
        Runnable runnable = new DemoRunnable();//創建一個實現了Runnable接口的類的實例
        Thread thread = new Thread(runnable);//創建線程實例,并在構造方法中傳入實現了Runnable類的實例
        thread.start();//啟動線程
    }
}

注: 這兩種方法都能讓線程執行我們需要執行的邏輯代碼。當你調用start()函數啟動線程后,程序會在該線程中調用Thread類自己的run()方法,如果你在創建線程時傳入了Runnable的實例,那么在Thread類的run()方法中,會調用Runnable的run()方法。
特別需要注意的是,new Thread()只是創建了Thread類的一個實例,此時并沒有創建出一條線程。而啟動線程是調用start()方法而不是run()方法:直接調用run()方法時你只是調用的一個普通方法去執行相關邏輯代碼,邏輯依然執行再你調用run()方法的線程中;而調用start()方法,jvm才會創建一條新線程,而此時run()才會在該線程中自動被回調執行。

三、線程同步

什么是同步?

同步是某個任務在同一時間只能由一個線程在執行,等這個線程執行完成后,下一個線程才能執行。那么既然線程的目的是為了異步,那么又為什么需要同步呢?這主要是由于數據安全造成的。多個線程在同時操作一個數據,當線程A還沒沒來得急使用該數據時,線程B就改了該數據的狀態或值,這是就可能導致結果的錯誤,甚至是程序運行的異常。就像由此你和朋友都很餓,然后看到一個蘋果,你正準備吃,然后你朋友直接給你搶來吃的還剩核,然后......例子可能不精確,見諒。

實現線程同步主要要使用兩個點:鎖和synchronized關鍵字。

1.鎖。
鎖是java中一種機制,分為對象鎖和類鎖。每個對象/類都有一個單一的鎖(需要注意的是,一個類可以有多個對象,每個對象的鎖也都是獨立且單一的,互不干擾)。當一個線程獲取到某個對象/類的鎖后,除非該鎖被釋放,否則其他線程是不能獲取到該對象/類的鎖,而此時如果其他線程要獲取該對象的鎖,就只能等待。而上面所說的某個任務,便是獲取到同一個鎖的代碼塊,他可能里面的邏輯并不相同,但是獲取的鎖是相同的。

2、synchronized關鍵字用于需要同步的邏輯中,實現方式分為:同步方法和同步塊。synchronized的目的就是獲取某個對象/類的鎖。分為:

同步塊。其中的object參數便是你要獲取的鎖的對象。

synchronized (object) {
    //TODO 這里是同步塊中需要執行的邏輯
}

對象同步方法。該synchronized獲取到的鎖是類A時候化后的對象的鎖。

public class A {
    public synchronized void syncMethod() {
        //TODO 這里是同步方法中需要執行的邏輯
    }
}

類同步方法,即靜態同步方法。該synchronized獲取到的鎖是類A的鎖。

public class A {
    public static synchronized void staicSyncMethod() {

    }
}

同步的例子

public class ThreadSyncDemo {

    private static Object locker = new Object();//需要獲取鎖的對象

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {
                    for (int i = 0; i < 1000; ++i) {
                        System.out.println(i);
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {
                    for (int i = 0; i < 1000; ++i) {
                        System.out.println("aaa");
                    }
                }
            }
        });

        t1.start();
        t2.start();
    }
}

執行這段代碼時,你先將synchronized塊去掉,保留里面的邏輯,然后執行,此時你會返現控制臺輸出中,數字和字母是交叉出現的,說明是異步執行的。然后加上同步快,再執行,你會發現此時數字先打印完后才打印的字母(或者字母先打印完后才打印的字母),說明是同步執行的。對象同步方法和靜態同步方法原理的列子這里就不再舉出,只要知道到底是獲取到那個對象或類的鎖,其他都是類似的。

死鎖

死鎖,顧名思義就是鎖死了,執行不下去了。若有兩個線程A和B,若線程A在執行時獲取到對象X的鎖,在同步塊中又獲取對象Y的鎖,而此時線程B在執行時獲取到對象Y的鎖,而B的同步塊中又在獲取X的鎖。再某種比較的極端情況下,A持有X的鎖,B持有Y的鎖,A執行到獲取Y的鎖時B未執行完,A阻塞等待,然后B又獲取X的鎖,而此時A還在阻塞等待B持有的Y的鎖,未釋放X的鎖,導致B也阻塞等待。A和B都在阻塞等待,然后就沒有然后了~~~~
所以避免死鎖的其中之一便是盡量不要交叉獲取鎖。當然這不是唯一導致死鎖的可能。
下面是一個死鎖的列子:

public class ThreadSyncDemo {

    private static Object locker = new Object();

    public static void main(String[] args) {
        final Thread t1 = new Thread() {
            @Override
            @Deprecated
            public void run() {
                synchronized (locker) {//獲取ThreadSyncDemo的類鎖
                    for (int i = 0; i < 10000; ++i) {
                        System.out.println(i);
                        if (i == 1000) {
                            this.suspend();//掛起該線程,此方法暫停線程執行,但不會釋放鎖
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker) {//獲取ThreadSyncDemo的類鎖
                    for (int i = 0; i < 10000; ++i) {
                        System.out.println("aaa");
                    }
                }
            }
        });

        t1.start();
        try{
            Thread.sleep(50)//暫停下,保證他t1先執行。
        }catch(Exception e){}
        t2.start();
    }
}

上面例子中,線程t1和線程t2的同步塊都是獲取的ThreadSyncDemo.class的鎖,在t1同步塊中,當i=1000時,暫停了t1線程的執行,此時t1的同步塊未釋放鎖,而t2一直在等待t1釋放鎖,如果t1線程不繼續執行,則t2也執行不了。

四. Thread類的相關方法

wait()與notify()/notifyAll()方法

實際上,wait()、notify()、notifyAll()這三個方法并不是Thread類的專有方法,而是Object的方法,也就是說,每個對象都存在這三個方法。

需要注意的是,這三個方法都只能在同步塊/同步方法中執行,其他地方執行時沒有意義的。而且需要同步塊中獲取到的鎖的對象來調用才有效果。

wait()顧名思義讓同步塊暫停執行并等待,此時該同步塊會讓出獲取到的鎖,讓其他線程執行獲取同一把鎖的同步塊。而在其他線程執行完后,調用notify()/notifyAll()方法,之前等待的的同步塊就會繼續執行。但是,如果調用了notify()/notifyAll()之后,后面有長時間任務二導致鎖未被釋放,等待中的同步塊也需要等鎖被釋放后才會繼續往下執行。如果同一個鎖有多個地方等待,就需要使用notifyAll()來全部喚醒,使他們重新爭奪鎖的行列中,誰先獲取到鎖就誰先執行。注意如果等待的是多個,nofity()和nofityAll()后最先獲取的鎖的是那個,由jvm決定。
wait()方法有兩個個重載方法,wait(long timeout)和wait(long timeout, int nanos),其中timeout是等待時間,如果timeout=0,則表示一直等待知道nofity()/nofityAll()被調用切獲取到鎖后繼續執行,timeout>0則表示等待多少時間后,只要獲取到鎖就繼續執行。至于nanos,表示納秒,值在0-999999之間,為了更好的控制時間。

public class ThreadSyncDemo {

    private static Object locker_1 = new Object();

    public static void main(String[] args) {
        final Thread t1 = new Thread() {
            @Override
            @Deprecated
            public void run() {
                synchronized (locker_1) {
                    for (int i = 0; i < 1000; ++i) {
                        System.out.println(i);
                        if (i == 100) {
                            try {
                                locker_1.wait();//當i=100是,執行wait()方法,釋放鎖,進入等待狀態
                            } catch (Exception e) {
                            }
                        }
                    }
                }
            }
        };

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (locker_1) {
                    for (int i = 0; i < 100; ++i) {
                        System.out.println("aaa");
                    }
                    locker_1.notify();//當執行完后,通知等待的線程繼續執行。
                }
            }
        });

        t1.start();
        try{
            Thread.sleep(50) //保證t1先執行
        }catch(Exception e)
        t2.start();
    }
}

上例,控制臺先會打印0-100,當線程t1中i=100時,調用locker_1.wait()使其釋放鎖并進入等待狀態,此時線程t2獲取到鎖,控制臺打印100個aaa,然后調用locker_1.notify()后,線程t1會繼續在控制臺打印101-999。

interrupt(),interrupted(),isInterrupt()方法

interrupt()方法看起來是中斷線程,但實際上,當你調用interrupt()方法后,你發現線程該干嘛還是再干嘛,除非你的線程中存再調用sleep()、wait()等方法的時候,此時會拋出InterruptedException異常,以供手動處理線程停止。isInterrupt()返回線程是否是中斷狀態。interrupted()方法是類方法。從Thread類的源碼看,isInterrupt()和interrupted()都會調用以下方法:
private native boolean isInterrupted(boolean ClearInterrupted);
不同是isInterrupt()傳入的ClearInterrupted=false,
interrupted()方法傳入的ClearInterrupted=true,當ClearInterrupted=true時,線程的中斷狀態會被清除,也就是說此時isInterrupt()方法的返回值是false。

suspend()與resume()方法

已廢棄的方法,用于暫停和重新開始執行線程。和wait()不同的是,suspend()是通過線程的實例調用的,而不是鎖對象,調用也不需要在同步塊中調用,而且suspend()方法調用后并不會釋放鎖。列子:

public class ThreaSuspendResumeDemo {

    public static void main(String[] args) {
        final Timer timer = new Timer();

        final Thread t = new Thread() {
            @Deprecated
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println(i);
                    if (i == 1000) {
                        this.suspend();//暫停線程
                    }
                }
                timer.cancel();
            }
        };
        t.start();

        timer.schedule(new TimerTask() {
            @Override
            @Deprecated
            public void run() {
                t.resume();//3秒后將線程喚醒
            }
        }, 3000);
    }
}

stop()方法

廢棄的方法。暴力終止線程,調用此方法后,線程中未執行的語句將不會再執行。但是isAlive()方法和isInterrupt方法的返回值依然是false,所以,暴力如此,想想都可怕,謹慎使用。


public class ThreadStopDemo {

    public static void main(String[] args) {

        final Thread t = new Thread() {
            @Deprecated
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    System.out.println(i);
                    if (i == 1000) {
                        this.stop();//i=1000時就停止了,控制臺只會打印0-1000。
                    }
                }
                System.out.println("Is this thread alive?" + this.isAlive());//線程已經終止,這條語句是不會執行的
            }
        };
        t.start();

        final Timer timer = new Timer();
        timer.schedule(new TimerTask(){
            @Override
            @Deprecated
            public void run() {
                System.out.println("Is this thread alive?" + t.isAlive());//false
                System.out.println("Is this thread interrupt?" + t.interrupted());//false
            }
        }, 3000);
    }
}

destroy()方法

額,為什么要講這個方法呢?因為我以為會和stop()方法一樣的喪心病狂,但是我錯了。從Thread.destroy()方法的源碼來看,結果讓人發呆流鼻涕。源碼如下:

    /**
     * Throws {@link NoSuchMethodError}.
     *
     * @deprecated This method was originally designed to destroy this
     *     thread without any cleanup. Any monitors it held would have
     *     remained locked. However, the method was never implemented.
     *     If if were to be implemented, it would be deadlock-prone in
     *     much the manner of {@link #suspend}. If the target thread held
     *     a lock protecting a critical system resource when it was
     *     destroyed, no thread could ever access this resource again.
     *     If another thread ever attempted to lock this resource, deadlock
     *     would result. Such deadlocks typically manifest themselves as
     *     "frozen" processes. For more information, see
     *     <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">
     *     Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
     * @throws NoSuchMethodError always
     */
    @Deprecated
    public void destroy() {
        throw new NoSuchMethodError();
    }

是不是亮瞎了鈦合金狗眼!!!!!

五.守護線程

線程分為用戶(User)線程和守護(Daemon)線程,守護(Daemon)線程的實現就是線程在調用start()方法前,先調用setDaemon(true)方法。區別是,普通用戶(User)線程,只要線程還在執行,那么程序就永遠不會退出;而守護(Daemon)線程只要程序主線程執行完后,守護(Daemon)線程也就被終止。守護(Daemon)線程常作為輔佐的的作用。

以上便是此次線程學習的第一部分總結,如有意見或建議歡迎提出,相互探討才能共同成長與進步。后面講繼續研究線程池和線程調度。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容