Java高併發編程一--什麼是線程

1.什麼是線程

    在講解java高併發時必需要先聊聊線程,那麼什麼是線程呢?下面是網上的答案:java

  • 1.線程:進程中負責程序執行的執行單元
  •              線程自己依靠程序進行運行
  •              線程是程序中的順序控制流,只能使用分配給程序的資源和環境
  • 2.進程:執行中的程序
  •              一個進程至少包含一個線程
  • 3.單線程:程序中只存在一個線程,實際上主方法就是一個主線程
  • 4.多線程:在一個程序中運行多個任務
  •                 目的是更好地使用CPU資源

我這裏通俗一點講其實就是:一個程序裏頭不一樣的執行路徑,能夠放在不一樣的cpu裏面同步運行git

2.如何啓動一個線程

2.1繼承Thread

//在java.lang包中定義, 繼承Thread類必須重寫run()方法
class MyThread extends Thread{
    private static int num = 0;
 
    public MyThread(){
        num++;
    }
 
    @Override
    public void run() {
        System.out.println("主動建立的第"+num+"個線程");
    }
}
/*建立好了本身的線程類以後,就能夠建立線程對象了,而後經過start()方法去啓動線程。
注意,不是調用run()方法啓動線程,run方法中只是定義須要執行的任務,若是調用run方法,
即至關於在主線程中執行run方法,跟普通的方法調用沒有任何區別,
此時並不會建立一個新的線程來執行定義的任務。*/
public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}
 
class MyThread extends Thread{
    private String name;
 
    public MyThread(String name){
        this.name = name;
    }
 
    @Override
    public void run() {
        System.out.println("name:"+name+" 子線程ID:"+Thread.currentThread().getId());
    }

2.2實現Runnable接口

//在Java中建立線程除了繼承Thread類以外,還能夠經過實現Runnable接口來實現相似的功能。
//實現Runnable接口必須重寫其run方法。
//下面是一個例子:
public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }
 
    @Override
    public void run() {
        System.out.println("子線程ID:"+Thread.currentThread().getId());
    }
}
//Runnable的中文意思是「任務」,顧名思義,經過實現Runnable接口,咱們定義了一個子任務,
//而後將子任務交由Thread去執行。注意,這種方式必須將Runnable做爲Thread類的參數,
//而後經過Thread的start方法來建立一個新線程來執行該子任務。若是調用Runnable的run方法的話,
//是不會建立新線程的,這根普通的方法調用沒有任何區別。

//事實上,查看Thread類的實現源代碼會發現Thread類是實現了Runnable接口的。

//在Java中,這2種方式均可以用來建立線程去執行子任務,具體選擇哪種方式要看本身的需求。
//直接繼承Thread類的話,可能比實現Runnable接口看起來更加簡潔,可是因爲Java只容許單繼承,
//因此若是自定義類須要繼承其餘類,則只能選擇實現Runnable接口。

2.3使用ExecutorService、Callable、Future實現有返回結果的多線程

多線程後續會講到,這裏暫時先知道一下有這種方法便可。編程

/**
* 有返回值的線程 
*/ 
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序開始運行----");  
   Date date1 = new Date();  
 
   int taskSize = 5;  
   // 建立一個線程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 建立多個有返回值的任務  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 執行任務並獲取Future對象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 關閉線程池  
   pool.shutdown();  
 
   // 獲取全部併發任務的運行結果  
   for (Future f : list) {  
    // 從Future對象上獲取任務的返回值,並輸出到控制檯  
    System.out.println(">>>" + f.get().toString());  
   }  
 
   Date date2 = new Date();  
   System.out.println("----程序結束運行----,程序運行時間【" 
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  
 
class MyCallable implements Callable<Object> {  
private String taskNum;  
 
MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  
 
public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任務啓動");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任務終止");  
   return taskNum + "任務返回運行結果,當前任務時間【" + time + "毫秒】";  
}
}
/*代碼說明:
上述代碼中Executors類,提供了一系列工廠方法用於創先線程池,返回的線程池都實現了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
建立固定數目線程的線程池。

public static ExecutorService newCachedThreadPool()
建立一個可緩存的線程池,調用execute 將重用之前構造的線程(若是線程可用)。
若是現有線程沒有可用的,則建立一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。

public static ExecutorService newSingleThreadExecutor()
建立一個單線程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
建立一個支持定時及週期性的任務執行的線程池,多數狀況下可用來替代Timer類。

ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,
返回Future。若是Executor後臺線程池尚未完成Callable的計算,
這調用返回Future對象的get()方法,會阻塞直到計算完成。*/

3.線程的狀態

  • 建立(new)狀態: 準備好了一個多線程的對象
  • 就緒(runnable)狀態: 調用了start()方法, 等待CPU進行調度
  • 運行(running)狀態: 執行run()方法
  • 阻塞(blocked)狀態: 暫時中止執行, 可能將資源交給其它線程使用
  • 終止(dead)狀態: 線程銷燬

當須要新起一個線程來執行某個子任務時,就建立了一個線程。可是線程建立以後,不會當即進入就緒狀態,由於線程的運行須要一些條件(好比內存資源,在前面的JVM內存區域劃分一篇博文中知道程序計數器、Java棧、本地方法棧都是線程私有的,因此須要爲線程分配必定的內存空間),只有線程運行須要的全部條件知足了,才進入就緒狀態。  當線程進入就緒狀態後,不表明馬上就能獲取CPU執行時間,也許此時CPU正在執行其餘的事情,所以它要等待。當獲得CPU執行時間以後,線程便真正進入運行狀態。  線程在運行狀態過程當中,可能有多個緣由致使當前線程不繼續運行下去,好比用戶主動讓線程睡眠(睡眠必定的時間以後再從新執行)、用戶主動讓線程等待,或者被同步塊給阻塞,此時就對應着多個狀態:time waiting(睡眠或等待必定的事件)、waiting(等待被喚醒)、blocked(阻塞)。  當因爲忽然中斷或者子任務執行完畢,線程就會被消亡。緩存

在有些教程上將blocked、waiting、time waiting統稱爲阻塞狀態,這個也是能夠的,只不過這裏我想將線程的狀態和Java中的方法調用聯繫起來,因此將waiting和time waiting兩個狀態分離出來。安全

注:sleep和wait的區別:多線程

  • sleepThread類的方法,waitObject類中定義的方法.
  • Thread.sleep不會致使鎖行爲的改變, 若是當前線程是擁有鎖的, 那麼Thread.sleep不會讓線程釋放鎖.
  • Thread.sleepObject.wait都會暫停當前的線程. OS會將執行時間分配給其它線程. 區別是, 調用wait後, 須要別的線程執行notify/notifyAll纔可以從新得到CPU執行時間.

4.上下文切換

對於單核CPU來講(對於多核CPU,此處就理解爲一個核),CPU在一個時刻只能運行一個線程,當在運行一個線程的過程當中轉去運行另一個線程,這個叫作線程上下文切換(對於進程也是相似)。併發

因爲可能當前線程的任務並無執行完畢,因此在切換時須要保存線程的運行狀態,以便下次從新切換回來時可以繼續切換以前的狀態運行。舉個簡單的例子:好比一個線程A正在讀取一個文件的內容,正讀到文件的一半,此時須要暫停線程A,轉去執行線程B,當再次切換回來執行線程A的時候,咱們不但願線程A又從文件的開頭來讀取。ide

所以須要記錄線程A的運行狀態,那麼會記錄哪些數據呢?由於下次恢復時須要知道在這以前當前線程已經執行到哪條指令了,因此須要記錄程序計數器的值,另外好比說線程正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候須要知道以前掛起時變量的值時多少,所以須要記錄CPU寄存器的狀態。因此通常來講,線程上下文切換過程當中會記錄程序計數器、CPU寄存器狀態等數據。高併發

說簡單點的:對於線程的上下文切換實際上就是 存儲和恢復CPU狀態的過程,它使得線程執行可以從中斷點恢復執行測試

雖然多線程可使得任務執行的效率獲得提高,可是因爲在線程切換時一樣會帶來必定的開銷代價,而且多個線程會致使系統資源佔用的增長,因此在進行多線程編程時要注意這些因素。

5.線程的經常使用方法

編號 方法 說明
1 public void start() 使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
2 public void run() 若是該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;不然,該方法不執行任何操做並返回。
3 public final void setName(String name) 改變線程名稱,使之與參數 name 相同。
4 public final void setPriority(int priority) 更改線程的優先級。
5 public final void setDaemon(boolean on) 將該線程標記爲守護線程或用戶線程。
6 public final void join(long millisec) 等待該線程終止的時間最長爲 millis 毫秒。
7 public void interrupt() 中斷線程。
8 public final boolean isAlive() 測試線程是否處於活動狀態。
9 public static void yield() 暫停當前正在執行的線程對象,並執行其餘線程。
10 public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操做受到系統計時器和調度程序精度和準確性的影響。
11 public static Thread currentThread() 返回對當前正在執行的線程對象的引用。

6.中止線程

中止線程是在多線程開發時很重要的技術點,掌握此技術能夠對線程的中止進行有效的處理。
中止一個線程可使用Thread.stop()方法,但最好不用它。該方法是不安全的,已被棄用。
在Java中有如下3種方法能夠終止正在運行的線程:

  • 使用退出標誌,使線程正常退出,也就是當run方法完成後線程終止
  • 使用stop方法強行終止線程,可是不推薦使用這個方法,由於stop和suspend及resume同樣,都是做廢過時的方法,使用他們可能產生不可預料的結果。
  • 使用interrupt方法中斷線程,但這個不會終止一個正在運行的線程,還須要加入一個判斷才能夠完成線程的中止。
  • 暫停線程

    interrupt()方法

7.線程的優先級

在操做系統中,線程能夠劃分優先級,優先級較高的線程獲得的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拋出異常throw new IllegalArgumentException()。
//JDK中使用3個常量來預置定義優先級的值,代碼以下:
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是同樣的。
規則性
高優先級的線程老是大部分先執行完,但不表明高優先級線程所有先執行完。
隨機性
優先級較高的線程不必定每一次都先執行完。
*/

8.守護線程

在Java線程中有兩種線程,一種是User Thread(用戶線程),另外一種是Daemon Thread(守護線程)。
Daemon的做用是爲其餘線程的運行提供服務,好比說GC線程。其實User Thread線程和Daemon Thread守護線程本質上來講去沒啥區別的,惟一的區別之處就在虛擬機的離開:若是User Thread所有撤離,那麼Daemon Thread也就沒啥線程好服務的了,因此虛擬機也就退出了。

守護線程並不是虛擬機內部能夠提供,用戶也能夠自行的設定守護線程,方法:public final void setDaemon(boolean on) ;可是有幾點須要注意:

  • thread.setDaemon(true)必須在thread.start()以前設置,不然會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置爲守護線程。 (備註:這點與守護進程有着明顯的區別,守護進程是建立後,讓進程擺脫原會話的控制+讓進程擺脫原進程組的控制+讓進程擺脫原控制終端的控制;因此說寄託於虛擬機的語言機制跟系統級語言有着本質上面的區別)
  • 在Daemon線程中產生的新線程也是Daemon的。 (這一點又是有着本質的區別了:守護進程fork()出來的子進程再也不是守護進程,儘管它把父進程的進程相關信息複製過去了,可是子進程的進程的父進程不是init進程,所謂的守護進程本質上說就是「父進程掛掉,init收養,而後文件0,1,2都是/dev/null,當前目錄到/」)
  • 不是全部的應用均可以分配給Daemon線程來進行服務,好比讀寫操做或者計算邏輯。由於在Daemon Thread還沒來的及進行操做時,虛擬機可能已經退出了

碼雲地址:https://gitee.com/zhangzeli/java-concurrent

相關文章
相關標籤/搜索