天天看點

java多線程:線程基礎

文章目錄

      • 線程建立
      • 線程通知與等待
        • 虛假喚醒
        • wait()、wait(long timeout)、wait(long timeout,int nanous)函數
        • notify()、notifyAll()函數
      • 等待線程執行終止的join方法
      • 讓線程睡眠的sleep方法
        • Sleep方法與wait方法差別
      • 讓出CPU執行權的yield方法
        • sleep()與yield()差別
      • 線程中斷
      • 線程死鎖
        • 為什麼會産生死鎖??
        • 如何避免線程死鎖?
      • 守護線程與使用者線程
      • 線程組和線程優先級
        • 線程組
        • 線程的優先級
        • 線程組的常用方法及資料結構
      • ThreadLocal

線程建立

線程建立有三種方式:

  • 實作Runnable接口的run方法
  • 繼承Thread類并重寫run方法
  • 實作Callable接口,使用FutureTask方式
繼承Thread類并重寫run方法
package threadtxt;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {
    /***
     *  繼承Thread 類并重寫run方法
     */
    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("i am a child thread");
        }
    }

    public static void main(String[] args) {
        //        建立線程
                MyThread myThread = new MyThread();
        //        啟動線程
                myThread.start();
    }
}

           

注意點:

當線程建立完thread對象後該線程并沒有啟動,直到調用了start方法才真正的啟動了線程。調用start方法後線程沒有馬上執行,而是進入了就緒狀态,這個時候已經擷取了除CPU資源外的其他資源,等待擷取CPU資源後才會真正處于運作狀态。run方法執行完畢之後,該線程就處于終止狀态。

具體可以參考 : Java多線程:概念

實作Runnable接口的run方法
package threadtxt;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
    /***
     * 實作Runnable接口
      */
    public static class RunableTask implements Runnable{
        @Override
        public void run() {
            System.out.println("i am a child thread");
        }
    }
    public static void main(String[] args) {
        RunableTask runableTask = new RunableTask();
        new Thread(runableTask).start();
        new Thread(runableTask).start();
    }
}

           
實作Callable接口,使用FutureTask方式
package threadtxt;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadTest {
    /***
     * 使用FutureTask方式實作
     */
    public static class CallerTask implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "hello";
        }
    }

    public static void main(String[] args) {

//        建立異步任務
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//       建立線程并啟動
        new Thread(futureTask).start();

        try {
//            等待任務執行完畢,傳回結果
            String rtn = futureTask.get();
            System.out.println(rtn);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }


    }
}

           

知識點

在程式裡面調用start方法後,虛拟機會先為我們建立一個線程,然後等到這個線程第一次得到時間片時再調用run()方法。但是不可多次調用start方法,在第一次調用start方法後,再次調用start方法會抛出異常,因為start方法中在使用前會先判斷threadStatus變量是否為0,如果不為0則抛出異常

線程通知與等待

Java多線程的等待/通知機制是基于Object類的wait()方法和notify()、notifyAll()方法來實作。

notify()方法會随機喚醒一個正在等待的線程,而notifyAll()會叫醒所有正在等待的線程

線程調用wait方法時,該線程就會被阻塞挂起,直到發生下面幾件才會傳回:

  • 其他線程調用了該共享對象的notify()或者notifyAll()方法
  • 其他線程調用了該線程的interrupt()方法,該線程抛出InterruptedException異常傳回

知識點

調用wait方法的線程沒有事先擷取該對象的螢幕鎖,則調用wait方法時,會抛出illegalMonitorStateException異常

問題1:一個線程如何才能擷取一個共享變量的螢幕鎖?

(1) 執行synchronized同步代碼塊時,使用該共享變量作為參數

synchronized(共享變量){
    //業務邏輯
}
           

(2)調用該共享變量的方法,并且該犯法使用了synchronized修飾

synchronized void add(int a,int b){
    //業務邏輯
}
           

虛假喚醒

一個線程從挂起狀态變為運作狀态(被喚醒),即使該線程沒有被其他線程調用notify()、notifyAll()方法進行通知,或者是被中斷、或者等待逾時

問題1:怎麼防止虛假喚醒?

​ 不停的去測試該線程被喚醒的條件是否滿足,不滿足則繼續等待,即是在一個循環中調用wait方法進行防範。退出循環的條件是滿足了喚醒該線程的條件。

synchronized(obj){
    while(條件是否滿足){
        obj.wait();
    }
}
           

以上代碼中的while()循環,則不能用if代替,如果使用if則隻會判斷一次,當下次條件不滿足時 ,線程也會喚醒。是以等待應該出現在循環當中

一般并發線程操作的步驟:

  • 判斷等待
  • 業務操作
  • 通知

知識點

目前線程調用共享變量wait方法後隻會釋放目前變量上的鎖,如果目前線程還持有其他共享變量鎖,則這些鎖是不會被釋放的。

wait()、wait(long timeout)、wait(long timeout,int nanous)函數

wait(long timeout)相比wait()方法多了一個逾時參數,如果一個線程調用共享對象的該方法挂起後,沒有在指定的timeout ms 時間内被其他線程調用該共享變量的notify()或者notifyAll()方法喚醒,那麼該函數還是會因為逾時而傳回。

知識點

wait(0)方法和wait()方法效果一樣,因為在wait方法内部就是調用了wait(0)。需要注意的是,如果在調用函數時,傳遞了一個負的timeout則會抛出illegalArgumentException異常。

notify()、notifyAll()函數

線程調用notify()方法後,會喚醒一個在該共享變量上調用wait系列方法後被挂起的線程。

知識點

  • 一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是随機的。
  • 被喚醒的線程不能馬上從wait方法傳回并繼續執行,它必須在擷取了共享對象的螢幕鎖後才可以傳回,也就是喚醒它的線程釋放了共享變量上的螢幕鎖後,被喚醒的線程也不一定會擷取到該共享對象的螢幕鎖,因為其他線程會和目前線程一起競争該鎖,隻有該線程競争到了共享變量的螢幕鎖後才可以繼續執行。
  • 隻有目前線程擷取到了共享變量的螢幕鎖後,才可以調用該共享變量的notify方法,否會抛出illegalMontitorStateException異常。

notifyAll方法則會喚醒所有在該共享變量上由于調用wait系列方法而被挂起的線程。

等待線程執行終止的join方法

怎麼快速了解下線程中join()方法???

想象一下,你現在在排隊買奶茶,快要到你的時候,突然來個非常漂亮的妹子說:帥哥,可以讓我先買嗎?(這個時候想,長得還可以,讓你先買吧,等你買完就到我了,說不定讓你先買,還能留個微信号,交個朋友),然後你說:可以啊,于是妹子站在你前面買奶茶,可是這個時候,她的七大姑八大姨都來了,都排在那個妹子前面,你從前面的第三位,直接變成第N位。這個時候隻能感歎,早知道我就先買了。線程中的join方法就是這個道理了。讓其他的線程先執行完畢後,然後自己在執行操作。

舉個栗子

package threadtxt2;

import java.util.concurrent.TimeUnit;
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread tA = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" child tA over!!! ");
        });
        Thread tB = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(" child tB over!!! ");
        });
        tA.start();
        tB.start();
        System.out.println("wait all child");
        //等待子線程執行完畢,傳回
        tA.join();
        tB.join();
        System.out.println("over all child");
    }

}

           

傳回結果

wait all child

child tB over!!!

child tA over!!!

over all child

知識點

join()方法是Thread 類的一個執行個體方法。它的作用是讓目前線程陷入“等待”狀态,等待join的這個線程執行完成後,再繼續執行目前線程。

有時候,主線程建立并啟動了子線程,如果子線程需要進行大量的耗時運算,主線程往往将早于子線程結束之前結束。

如果主線程想等待子線程執行完畢後,獲得子線程中的處理完的某個資料,就要用到join方法

注意點

join()方法由兩個重載方法,一個是join(long),一個是join(long,int)。

實際上,通過源碼發現,join()方法及其重載方法底層都是利用了wait(long timeout)這個方法

對于join(long,int),通過檢視源碼(JDK1.8)發現,底層并沒有精确到納秒,而是對第二個參數做了簡單的判斷和處理

讓線程睡眠的sleep方法

當一個執行中的線程調用了Thread的sleep方法後,調用線程會暫時讓出指定時間的執行權,也就是在這期間不參與CPU的排程,但是該線程所擁有的螢幕資源,比如鎖還是持有狀态。

指定的睡眠時間到了後該函數就會正常傳回,線程處于就緒狀态,然後參與CPU排程,擷取CPU資源後就可以繼續運作了。

知識點

在調用sleep方法的線程,睡眠期間其他線程調用了interrupt()方法中斷了該線程,則該線程會在調用sleep方法的地方抛出InterruptedException異常傳回

Sleep方法與wait方法差別

  • wait方法可以指定等待時間,也可以不指定。Sleep方法必須指定
  • wait方法釋放CPU資源,同時釋放鎖。sleep方法釋放cpu資源,但是不釋放鎖。是以容易死鎖。
  • wait方法必須方法在同步塊或同步方法中,而sleep可以放在任意位置

讓出CPU執行權的yield方法

當一個線程調用yield方法時,目前線程會讓出CPU使用權,然後處于就緒狀态,線程排程器會從線程就緒隊列裡面擷取一個線程優先級最高的線程,當然也有可能會排程到剛剛讓出CPU的那個線程。

sleep()與yield()差別

目前線程調用sleep方法時調用線程會被阻塞挂起指定的時間,在這期間線程排程器不會去排程其他線程。

調用yield方法時,線程隻是讓出自己剩餘的時間片,并沒有挂起,而是出于就緒狀态,線程排程器下一次排程時就有可能排程到目前線程執行

線程中斷

java中的線程中斷是一種線程間的協作模式,線程中斷機制是一種協作機制,通過設定線程的中斷标志并不能直接終止該線程的執行,而是被中斷的線程根據中斷狀态自行處理。

Thread類裡提供的關于線程中斷的幾個方法:

  • Thread.interrupt() : 中斷線程。這裡的中斷線程并不會立即停止線程,而是設定線程的中斷狀态為true(預設為false);
  • Thread.interrupted(): 測試目前線程是否被中斷,線程的中斷狀态受這個方法的影響,意思是調用一次使線程中斷設定為true,連續調用兩次會使得這個線程的中斷狀态重新轉為false
  • Thread.isInterrupted() : 測試目前線程是否被中斷,與上面方法不同的是調用這個方法并不會影響線程的中斷狀态。
線上程的中斷機制裡,當其他線程通知需要被中斷的線程後,線程中斷的狀态被設定為true,但是具體被要求中斷的線程要怎麼處理,完全由被中斷線程自己而定,可以在适合的實際進行中中斷請求,也可以完全不處理繼續執行下去。

線程死鎖

死鎖:指兩個或者兩個以上的線程在執行過程中,因争奪資源而造成的互相等待的現象。在無外力作用的情況下,這些線程會一直互相等待而無法繼續運作下去。

java多線程:線程基礎

為什麼會産生死鎖??

死鎖的産生必須滿足以下四個條件:

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

如何避免線程死鎖?

  • 對資源進行有序的配置設定,讓擷取資源有先後順序

守護線程與使用者線程

Java中的線程分為兩類,分别為daemon線程和user線程。

知識點

JVM的退出和使用者線程有關,和守護線程無關。隻要有一個使用者線程沒有結束,正常情況下JVM就不會退出。

守護線程預設的優先級比較低。一個線程預設是非守護線程,可以通過Thread類的setDaemon(boolean on )來設定

線程組和線程優先級

線程組

java中用ThreadGroup來表示線程組,我們可以使用線程組對線程進行批量控制。

每個Thread必然存在于一個ThreadGroup中,Thread不能獨立于ThreadGroup存在。

ThreadGroup管理着它下面的Thread,ThreadGroup是一個标準的向下引用的樹狀結構。這樣設計的原因是防止上級線程被下級線程引用而無法有效地被GC回收。

線程的優先級

Java中線程優先級可以指定,範圍是1~10。但是并不是所有作業系統都支援10級優先級的劃分,java隻是給作業系統一個優先級參考值,線程最終在作業系統的優先級還是由作業系統決定。

知識點

java預設的線程優先級為5,線程的執行順序有排程程式來決定,線程的優先級會線上程調用前設定。通常情況下,高優先級的線程比低優先級有更高的幾率得到執行。我們使用方法Thread類的setPriority()方法來設定線程的優先級

線程組的常用方法及資料結構

擷取目前的線程組名字

複制線程組

//複制一個線程數組到一個線程組
Thread[] threads = new Thread[threadGroup.activeCount()];
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);
           

線程組統一異常處理

public static void main(String[] args){
    ThreadGroup threadGroup1 = new ThreadGroup("group1") {
        // 繼承ThreadGroup并重新定義以下⽅法
        // 線上程成員抛出unchecked exception
        // 會執⾏此⽅法
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println(t.getName() + ": " + e.getMessage());
        }
    };
    // 這個線程是threadGroup1的⼀員
    Thread thread1 = new Thread(threadGroup1, new Runnable() {
        public void run() {
            // 抛出unchecked異常
            throw new RuntimeException("測試異常");
        }
    });
    thread1.start();
}
           

線程組的資料結構

線程組還可以包含其他的線程組,不僅僅是線程。

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent; // ⽗親ThreadGroup
    String name; // ThreadGroupr 的名稱
    int maxPriority; // 線程最⼤優先級
    boolean destroyed; // 是否被銷毀
    boolean daemon; // 是否守護線程
    boolean vmAllowSuspension; // 是否可以中斷
    int nUnstartedThreads = 0; // 還未啟動的線程
    int nthreads; // ThreadGroup中線程數⽬
    Thread threads[]; // ThreadGroup中的線程
    int ngroups; // 線程組數⽬
    ThreadGroup groups[]; // 線程組數組
}
           

構造函數:

// 私有構造函數
private ThreadGroup() {
    this.name = "system";
    this.maxPriority = Thread.MAX_PRIORITY;
    this.parent = null;
}
// 預設是以目前ThreadGroup傳⼊作為parent ThreadGroup,新線程組的⽗線程組是⽬前正在運⾏線
public ThreadGroup(String name) {
    this(Thread.currentThread().getThreadGroup(), name);
}
// 構造函數
public ThreadGroup(ThreadGroup parent, String name) {
    this(checkParentAccess(parent), parent, name);
}
// 私有構造函數,主要的構造函數
private ThreadGroup(Void unused, ThreadGroup parent, String name) {
    this.name = name;
    this.maxPriority = parent.maxPriority;
    this.daemon = parent.daemon;
    this.vmAllowSuspension = parent.vmAllowSuspension;
    this.parent = parent;
    parent.add(this);
}
           

第三個構造函數⾥調⽤了 checkParentAccess ⽅法,這⾥看看這個⽅法的源碼:

// 檢查parent ThreadGroup
private static Void checkParentAccess(ThreadGroup parent) {
    parent.checkAccess();
    return null;
}
// 判斷目前運⾏的線程是否具有修改線程組的權限
public final void checkAccess() {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkAccess(this);
    }
}
           

知識點

這⾥涉及到 SecurityManager 這個類,它是Java的安全管理器,它允許應⽤

程式在執⾏⼀個可能不安全或敏感的操作前确定該操作是什麼,以及是否是

在允許執⾏該操作的安全上下⽂中執⾏它。應⽤程式可以允許或不允許該操

作。

⽐如引⼊了第三⽅類庫,但是并不能保證它的安全性。

其實Thread類也有⼀個checkAccess()⽅法,不過是⽤來目前運⾏的線程是

否有權限修改被調⽤的這個線程執行個體。(Determines if the currently running

thread has permission to modify this thread.)

總結來說,線程組是⼀個樹狀的結構,每個線程組下⾯可以有多個線程或者線程

組。線程組可以起到統⼀控制線程的優先級和檢查線程的權限的作⽤。

ThreadLocal

多線程通路同一個共享變量時特别容易出現并發問題,特别是在多個線程需要對一個共享變量寫入時。為了保證線程安全,一般使用者在通路共享變量時需要進行适當的同步。同步的措施是加鎖,這就需要使用者對鎖有一定的了解。

java多線程:線程基礎

問題:有沒有一種方式可以做到,當建立一個變量後,每個線程對其進行通路的時候通路的是自己的變量?

ThreadLocal可以做這個事情,它提供了線程本地變量,也就是如果你建立了一個ThreadLocal變量,那麼通路這個變量的每個線程都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地記憶體裡面的變量,進而避免了線程安全問題。
java多線程:線程基礎

知識點

如果開發者希望将類的某個靜态變量(UserID或者transaction ID )與線程狀态關聯,則可以考慮使用ThreadLocal。最常見的ThreadLocal使用場景為用來解決資料庫連接配接、Session管理等,資料庫連接配接和Session管理涉及多個複雜對象的初始化和關閉,如果在每個線程中聲明一些私有變量來進行操作,那這個線程就變得不那麼輕量了,需要頻繁的建立和關閉連接配接。