Java多線程編程核心技術(一)Java多線程技能

一、進程和線程

一個程序就是一個進程,而一個程序中的多個任務則被稱爲線程。html

進程是表示資源分配的基本單位,線程是進程中執行運算的最小單位,亦是調度運行的基本單位。java

舉個例子:編程

打開你的計算機上的任務管理器,會顯示出當前機器的全部進程,QQ,360等,當QQ運行時,就有不少子任務在同時運行。好比,當你邊打字發送表情,邊好友視頻時這些不一樣的功能均可以同時運行,其中每一項任務均可以理解成「線程」在工做。安全

二、使用多線程

在Java的JDK開發包中,已經自帶了對多線程技術的支持,能夠很方便地進行多線程編程。實現多線程編程的方式有兩種,一種是繼承 Thread 類,另外一種是實現 Runnable 接口。使用繼承 Thread 類建立線程,最大的侷限就是不能多繼承,因此爲了支持多繼承,徹底能夠實現 Runnable 接口的方式。須要說明的是,這兩種方式在工做時的性質都是同樣的,沒有本質的區別。以下所示:多線程

1.繼承 Thread 類併發

public class MyThread extends Thread {

    @Override
    public void run() {
        //...
    }
    
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

2.實現 Runnable 接口異步

public static void main(String[] args) throws InterruptedException {
     new Thread(new Runnable() {
         @Override
         public void run() {
             //...
         }
     }).start();
 }

Thread.java 類中的start()方法通知「線程規劃器」此線程已經準備就緒,等待調用線程對象的run()方法。這個過程其實就是讓系統安排一個時間來調用 Thread 中的 run() 方法,也就是使線程獲得運行,多線程是異步的,線程在代碼中啓動的順序不是線程被調用的順序。ide

Thread構造方法

Thread()
分配新的 Thread 對象。
Thread(Runnable target)
分配新的 Thread 對象。
Thread(Runnable target, String name)
分配新的 Thread 對象。
Thread(String name)
分配新的 Thread 對象。
Thread(ThreadGroup group, Runnable target)
分配新的 Thread 對象。
Thread(ThreadGroup group, Runnable target, String name)
分配新的 Thread 對象,以便將 target 做爲其運行對象,將指定的 name 做爲其名稱,並做爲 group 所引用的線程組的一員。
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
分配新的 Thread 對象,以便將 target 做爲其運行對象,將指定的 name 做爲其名稱,做爲 group 所引用的線程組的一員,並具備指定的堆棧大小
Thread(ThreadGroup group, String name)
分配新的 Thread 對象。

三、實例變量與線程安全

自定義線程類中的實例變量針對其餘線程能夠有共享與不共享之分。當每一個線程都有各自的實例變量時,就是變量不共享。共享數據的狀況就是多個線程能夠訪問同一個變量。來看下面的示例:學習

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    public void run() {
        count--;
        System.out.println("線程"+Thread.currentThread().getName()+" 計算 count = "+count);
    }
}

以上代碼定義了一個線程類,實現count變量減一的效果。運行類Runjava代碼以下:測試

public class Ruu {

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        Thread a = new Thread(myThread,"A");
        Thread b = new Thread(myThread,"B");
        Thread c = new Thread(myThread,"C");
        Thread d = new Thread(myThread,"D");
        Thread e = new Thread(myThread,"E");
        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

打印結果以下:

線程C 計算 count = 3
線程B 計算 count = 3
線程A 計算 count = 2
線程D 計算 count = 1
線程E 計算 count = 0

線程C,B的打印結果都是3,說明C和B同時對count進行了處理,產生了「非線程安全問題」。而咱們想要的獲得的打印結果卻不是重複的,而是依次遞減的。

在某些JVM中,i--的操做要分紅以下3步:

  1. 取得原有變量的值。
  2. 計算i-1。
  3. 對i進行賦值。

在這三個步驟中,若是有多個線程同時訪問,那麼必定會出現非線程安全問題。

解決方法就是使用 synchronized 同步關鍵字 使各個線程排隊執行run()方法。修改後的run()方法:

public class MyThread implements Runnable {
    private int count = 5;

    @Override
    synchronized public void run() {
        count--;
        System.out.println("線程"+Thread.currentThread().getName()+" 計算 count = "+count);
    }
}

打印結果:

線程B 計算 count = 4
線程C 計算 count = 3
線程A 計算 count = 2
線程E 計算 count = 1
線程D 計算 count = 0

關於System.out.println()方法

先來看System.out.println()方法源碼:

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

雖然println()方法內部使用 synchronized 關鍵字,但以下所示的代碼在執行時仍是有可能出現非線程安全問題的。

System.out.println("線程"+Thread.currentThread().getName()+" 計算 count = "+count--);

緣由在於println()方法內部同步,但 i-- 操做倒是在進入 println()以前發生的,因此有發生非線程安全問題的機率。

四、多線程方法

1. currentThread()方法

currentThread()方法可返回代碼段正在被哪一個線程調用的信息。

Thread.currentThread().getName()

2. isAlive()方法

方法isAlive()的功能是判斷當前的線程是否處於活動狀態。

thread.isAlive();

3. sleep()方法

方法sleep()的做用是在指定的毫秒數內讓當前"正在執行的線程"休眠(暫停執行)。這個"正在執行的線程"是指this.currentThread()返回的線程。

Thread.sleep()

4. getId()方法

getId()方法的做用是取得線程的惟一標識。

thread.getId()

五、中止線程

中止線程是在多線程開發時很重要的技術點。中止線程並不像break語句那樣乾脆,須要一些技巧性的處理。

在Java中有如下3種方法能夠終止正在運行的線程:

1)使用退出標誌,使線程正常退出,也就是當run()方法完成後線程中止。

2)使用stop()方法強行終止線程,可是不推薦使用這個方法,由於該方法已經做廢過時,使用後可能產生不可預料的結果。

3)使用interrupt()方法中斷線程。

1.暴力法中止線程

調用stop()方法時會拋出 java.lang.ThreadDeath 異常,但在一般的狀況下,此異常不須要顯示地捕捉。

try {
            myThread.stop();
        } catch (ThreadDeath e) {
            e.printStackTrace();
        }

方法stop()已經被做廢,由於若是強制讓線程中止線程則有可能使一些清理性的工做得不到完成。另一個狀況就是對鎖定的對象進行了「解鎖」,致使數據得不到同步的處理,出現數據不一致的狀況。示例以下:

public class UserPass {
    private String username = "aa";
    private String password = "AA";

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    synchronized public void println(String username, String password){
        this.username = username;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        UserPass userPass = new UserPass();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                userPass.println("bb","BB");
            }
        });
        thread.start();
        Thread.sleep(500);
        thread.stop();
        System.out.println(userPass.getUsername()+" "+userPass.getPassword());
    }

}

運行結果:

bb AA

2.異常法中止線程

使用interrupt()方法並不會真正的中止線程,調用interrupt()方法僅僅是在當前線程中打了一箇中止的標記,並非真的中止線程。

那咱們如何判斷該線程是否被打上了中止標記,Thread類提供了兩種方法。

interrupted() 測試當前線程是否已經中斷。
isInterrupted() 測試線程是否已經中斷。

interrupted() 方法 不止能夠判斷當前線程是否已經中斷,並且能夠會清除該線程的中斷狀態。而對於isInterrupted() 方法,只會判斷當前線程是否已經中斷,不會清除線程的中斷狀態。

僅靠上面的兩個方法能夠經過while(!this.isInterrupted()){}對代碼進行控制,但若是循環外還有其它語句,程序仍是會繼續運行的。這時能夠拋出異常從而使線程完全中止。示例以下:

public class MyThread extends Thread {
    @Override
    public void run() {
        try {
            for (int i=0; i<50000; i++){
                if (this.isInterrupted()) {
                    System.out.println("已是中止狀態了!");
                    throw new InterruptedException();
                }
                System.out.println(i);
            }
            System.out.println("不拋出異常,我會被執行的哦!");
        } catch (Exception e) {
//            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(100);
        myThread.interrupt();
    }
    
}

打印結果:

...
2490
2491
2492
2493
已是中止狀態了!

注意

若是線程在sleep()狀態下被中止,也就是線程對象的run()方法含有sleep()方法,在此期間又執行了thread.interrupt() 方法,則會拋出java.lang.InterruptedException: sleep interrupted異常,提示休眠被中斷。

3.return法中止線程

return法很簡單,只須要把異常法中的拋出異常更改成return便可。代碼以下:

public class MyThread extends Thread {
    @Override
    public void run() {
        
        for (int i=0; i<50000; i++){
            if (this.isInterrupted()) {
                System.out.println("已是中止狀態了!");
                return;//替換此處
            }
            System.out.println(i);
        }
        System.out.println("不進行return,我會被執行的哦!");
       
    }
}

不過仍是建議使用「拋異常」來實現線程的中止,由於在catch塊中能夠對異常的信息進行相關的處理,並且使用異常能更好、更方便的控制程序的運行流程,不至於代碼中出現多個return,形成污染。

六、暫停線程

暫停線程意味着此線程還能夠恢復運行。在Java多線程中,可使用 suspend() 方法暫停線程,使用 resume()方法恢復線程的執行。

這倆方法已經和stop()同樣都被棄用了,由於若是使用不當,極易形成公共的同步對象的獨佔,使得其餘線程沒法訪問公共同步對象。示例以下:

public class MyThread extends Thread {
    private Integer i = 0;

    @Override
    public void run() {
        while (true) {
            i++;
            System.out.println(i);
        }
    }

    public Integer getI() {
        return i;
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread =new MyThread();
        myThread.start();
        Thread.sleep(100);
        myThread.suspend();
        System.out.println("main end");
    }
    
}

打印結果:

...
3398
3399
3400
3401

執行上段程序永遠不會打印main end。出現這樣的緣由是,當程序運行到 println() 方法內部中止時,PrintStream對象同步鎖未被釋放。方法 println() 源代碼以下:

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

這致使當前PrintStream對象的println() 方法一直呈「暫停」狀態,而且鎖未被myThread線程釋放,而主線程中的代碼System.out.println("main end") 還在傻傻的排隊等待,致使遲遲不能運行打印。


使用 suspend() 和 resume() 方法也容易由於線程的暫停而致使數據不一樣步的狀況,示例以下:

public class UserPass2 {
    private String username = "aa";
    private String password = "AA";

    public String getUsername() {
        return username;
    }


    public String getPassword() {
        return password;
    }


    public void setValue(String username, String password){
        this.username = username;
        if (Thread.currentThread().getName().equals("a")) {
            Thread.currentThread().suspend();
        }
        this.password = password;
    }

    public static void main(String[] args) throws InterruptedException {
        UserPass2 userPass = new UserPass2();
        new Thread(new Runnable() {
            @Override
            public void run() {
                userPass.setValue("bb","BB");
            }
        },"a").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(userPass.getUsername()+" "+userPass.getPassword());
            }
        },"b").start();

    }

}

打印結果:

bb AA

七、yield()方法

yield() 方法的做用是放棄當前的CPU資源,將它讓給其餘的任務去佔用CPU執行時間。但放棄的時間不肯定,有可能剛剛放棄,立刻又得到CPU時間片。

public static void yield()  暫停當前正在執行的線程對象,並執行其餘線程。

八、線程的優先級

在操做系統中,線程能夠劃分優先級,優先級較高的線程獲得的CPU資源較多,也就是CPU優先執行優先級較高的線程對象中的任務。

設置線程優先級有助於幫「線程規劃器」肯定在下一次選擇哪個線程來優先執行。

設置線程優先級使用setPriority()方法,此方法的JDK源碼以下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

在Java中,線程優先級劃分爲1 ~ 10 這10個等級,若是小於1或大於10,則JDK拋出異常。

從JDK定義的3個優先級常量可知,線程優先級默認爲5。

public final static int MIN_PRIORITY = 1;

    public final static int NORM_PRIORITY = 5;

    public final static int MAX_PRIORITY = 10;

線程優先級具備繼承性,好比A線程啓動B線程,則B線程的優先級與A是同樣的。

線程優先級具備規則性,線程的優先級與在代碼中執行start()方法的順序無關,與優先級大小有關。

線程優先級具備隨機性,CPU儘可能使線程優先級較高的先執行完,但沒法百分百確定。也就是說,線程優先級較高的不必定比線程優先級較低的先執行。

九、守護線程

在Java中有兩種線程,一種是用戶線程,一種守護線程。

什麼是守護線程?守護線程是一種特殊的線程,當進程中不存在非守護線程了,則守護線程自動銷燬。典型的守護線程就是垃圾回收線程,當進程中沒有非守護線程了,則垃圾回收線程也就沒有了存在的必要了,自動銷燬。能夠簡單地說:任何一個守護線程都是非守護線程的保姆。

如何設置守護線程?經過Thread.setDaemon(false)設置爲用戶線程,經過Thread.setDaemon(true)設置爲守護線程。若是不設置屬性,默認爲用戶線程。

thread.setDaemon(true);

示例以下:

public class MyThread extends Thread {
    private int i = 0;
    @Override
    public void run() {
        try {
            while (true){
                i++;
                System.out.println("i="+i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.setDaemon(true);
        thread.start();
        Thread.sleep(5000);
        System.out.println("我離開後thread對象也就再也不打印了");
    }
}

打印結果:

i=1
i=2
i=3
i=4
i=5
我離開後thread對象也就再也不打印了

參考與總結

《Java多線程編程核心技術》高洪巖 著

本文主要介紹了Thread類的API,算是爲學習多線程更深層次知識打下一些基礎,文章如有錯誤請在評論區指正。

擴展

Java多線程編程核心技術(二)對象及變量的併發訪問

Java多線程編程核心技術(三)多線程通訊

Java多線程核心技術(四)Lock的使用

Java多線程核心技術(五)單例模式與多線程

Java多線程核心技術(六)線程組與線程異常

相關文章
相關標籤/搜索