線程、線程池


 

進程

進程是是應用程序運行的載體,是程序的一次執行過程,是臨時的、有生命週期的,由程序、數據和進程控制塊三部分組成,進程之間相互獨立。
 

進程的三種基本狀態

  • 就緒(ready)狀態:進程準備就緒,獲得CPU時間片後可立即運行。就緒的進行放在就緒隊列中,操作系統按指定的調度策略分配cpu時間片。
  • 運行(running)狀態:進程獲取到cpu的時間片,開始執行代碼。時間片用完後,回到就緒狀態。
  • 阻塞(blocked )狀態:進程暫停執行,等待某一事件的發生,比如等待IO完成、用戶輸入等。

在這裏插入圖片描述
這三種只是進程的基本狀態,進程並不止這三種狀態。

 

線程

線程是程序執行的最小單元,一個進程可以有一個或多個線程(至少有一個),同一進程中的多個線程共享所在進程的內存空間。
 

進程、線程的區別

  • 進程是操作系統分配資源的基本單位,線程是程序執行的最小單位
  • 進程之間相互獨立,同一進程下的線程共享進程的內存空間
  • 線程上下文切換比進程上下文切換快得多

 

線程的6種狀態

1、New 新建
已創建線程,但尚未調用start()啓動線程

 

2、RUNNABLE 可運行
Java把就緒(ready)、運行(running)兩種狀態合併爲一種狀態:可運行(runnable)。調用了start()啓動線程後,線程就處於可運行狀態(就緒);線程獲取到時間片開始執行代碼,就處於運行(running)狀態。

操作系統只給就緒隊列中(就緒狀態)的線程分配時間片。

 

3、BLOCKED 阻塞
正在執行的線程需要等待特定事件的發生、完成,就會進入阻塞狀態,常見的有:①等待獲取鎖,②等待IO完成。線程進入阻塞狀態後會讓出cpu使用權,等待的事件發生、完成後線程進入就緒狀態,進入調度隊列,等待操作系統分配時間片。

 

4、WAITING (無限期)等待
正在執行的線程中調用某些方法會讓線程進入無限期等待狀態,進入無限期等待狀態的線程會讓出cpu使用權,被其它線程顯式喚醒後,進入就緒狀態。

進入無限期等待 喚醒
Object.wait() Object.notify() 或 Object.notifyAll()
Thread.join() 被調用的線程執行完畢
LockSupport.park() LockSupport.unpark(currentThread)

 

5、TIMED_WAITING 限時等待
在正在執行的線程中調用某些方法會讓線程進入限時等待狀態,進入限時等待狀態的線程會讓出cpu使用權,在指定時間後會自動被喚醒,也可以中途被喚醒,喚醒後進入就緒狀態。

進入限時等待 喚醒
Thread.sleep(time) sleep時間結束
Object.wait(time) wait時間結束,或者調用Object.notify() / notifyAll()
LockSupport.parkNanos(time)/parkUntil(time) park時間結束,或者調用LockSupport.unpark(currentThread)

 

6、TERMINATED 消亡
線程執行完畢,或者線程執行時發生異常、錯誤無法繼續執行,會進入消亡狀態。

 

線程的生命週期

在這裏插入圖片描述

 

線程的2種創建方式

1、繼承Thread類,重寫run()方法

public class Thread1 extends Thread{

    @Override
    public void run() {
        //.....  //要執行的代碼
    }

}
 Thread1 thread1 = new Thread1();
 thread1.start();

 

2、實現Runnable接口

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        //.....   //要執行的代碼
    }

}
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);  //要使用Thread來包裝
thread.start();

Runnable是函數式接口,可使用lambda表達式

Thread thread = new Thread(() -> {
    //......
});

 

實際開發中一般使用第二種

  • java只支持單繼承,第一種繼承Thread後不能再繼承其它類;使用第二種在實現接口的基礎上可以繼承類
  • 第二種代碼可以共享
     

start()、run()的區別

  • start()會在jvm中創建一條新線程,分配線程所需的資源,並調用run()方法執行run()方法中的代碼,實現了多線程
  • run()只是一個普通方法,直接調用run()方法只是在當前線程中執行run()方法中的代碼,並不會創建新線程,不能實現多線程

 

線程常用方法

1、線程的基本信息

Thread thread = Thread.currentThread();  //獲取當前線程
Thread thread = new Thread1();


thread.getId(); //線程id
thread.getName(); //線程名
thread.getPriority(); //線程優先級
thread.getState(); //線程狀態
thread.isDaemon();  //是否是守護線程


thread.setName("my_thread"); //設置線程名,默認主線程是main、其它線程以Thread-n的形式命名

thread.setPriority(5); //設置線程優先級,可使用[1,10]上的整數,也可以使用常量,默認5,優先級高的線程優先分配時間片
thread.setPriority(Thread.MIN_PRIORITY); //1
thread.setPriority(Thread.NORM_PRIORITY); //5
thread.setPriority(Thread.MAX_PRIORITY); //10

thread.setDaemon(false); //是否作爲守護線程,默認false

thread.start(); //線程的基本信息要在start()啓動線程之前設置

不同的操作系統,對線程優先級的支持有差異,不要過度依賴於線程優先級,儘量不要設置爲[1,10]上的其它數字,使用預定義的常量(1、5、10)即可。
 

線程分爲2類:用戶線程、守護線程,默認爲用戶線程,main線程默認也是用戶線程。守護線程是用戶線程的守護者,常見的守護線程比如gc線程。只要還有用戶線程在執行,程序就不會終止;如果程序中沒有用戶線程在執行、只有守護線程在執行,則jvm直接退出,程序終止運行。

守護線程不可控,儘量少用守護線程,不要在守護線程中進行讀寫操作、邏輯計算。

 

2、join()
Thread的實例方法,在一個線程中加入另一條已啓動的線程,可用於線程間通信

Thread1 thread1 = new Thread1();
thread1.start();

thread1.join();  //在當前線程中加入thread1

先執行加入的線程thread1,等加入的線程執行完畢,纔會繼續執行當前線程

 

3、yield()、sleep()

Thread.yield();  //線程讓步,當前線程讓出cpu的使用權,進入就緒狀態

Thread.sleep(500); //線程休眠,當前線程休眠指定ms,會讓出cpu的使用權,但不會釋放鎖,指定時間後自動甦醒,進入就緒狀態

都是Thread類的靜態方法,操作的都是當前線程,都會讓出cpu的使用權。

yield()是不可靠的,只是提示線程調度器當前線程願意讓出cpu使用權,由線程調度器決定是否讓出cpu使用權,不是一定會讓出。

 

4、wait()、notify()、notifyAll()
均是Object類的實例方法,常用於線程間通信。

這三個方法使用的前提是已經獲取到了互斥鎖,所以這三個方法都只能在同步代碼塊中使用,且操作的要是同一把鎖(鎖對象的內存地址相同)

public class Thread1 extends Thread {
    public static Object lock = new Object(); //鎖。可以把要使用的公共資源直接作爲鎖,也可以把一個Object對象作爲鎖,獲取到鎖後,纔可以操作公共資源。要是同一把鎖(地址相同)

    @Override
    public void run() {
        //....
        
        //同步代碼塊
        synchronized (lock){  //獲取鎖,獲取到鎖後自動執行裏面的代碼
            //.....  //操作資源

            lock.notify();  //如果同步代碼塊中:釋放鎖後還有代碼要執行,則應該使用notify()、notifyAll()進行等待
            // lock.notifyAll();

            try {
                lock.wait(); //讓出cpu使用權,釋放鎖,進入無限期等待,如果要繼續運行,需要在其它線程中顯式喚醒
         //lock.wait(10000);  //限時等待,指定時間後自動喚醒進入ready狀態
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            //.....  //如果同步代碼塊中、wait()之後還有代碼,喚醒後進入ready狀態,獲取到時間片後、需要重新獲取鎖才能繼續執行後面的代碼
        }
        
        //.....  //如果同步代碼塊後還有代碼,喚醒後進入ready狀態,獲取時間片後就可以繼續執行,無需獲取鎖
        
    }

}

wait()釋放鎖後,鎖分配給等待這個鎖的哪個線程?

等待池WaitSet中存放等待鎖的線程,鎖池EntryList是針對對象鎖而言的,把對象作爲鎖,鎖被釋放後進入鎖池中。
 

notify()、notifyAll()的區別

  • notify()是在等待池中等待該鎖的所有線程中,隨機選擇一個線程來獲取鎖
  • notifyAll()是讓等待池中等待該鎖的所有線程,都來競爭鎖

 

5、park()、parkUntil()、parkNanos()、unpark()
均爲LockSupport類的靜態方法

//將當前線程掛起,讓出cpu使用權,進入無限期等待狀態
LockSupport.park();

//參數是long型的時間戳,將當前線程掛起,讓出cpu使用權,進入限時等待狀態,到達指定時間自動喚醒,進入就緒狀態
LockSupport.parkUntil(100000000000L);  //ms
LockSupport.parkNanos(100000000000L);   //ns(納秒)

//unpark()可以解處指定線程park()、parkUntil()、parkNanos()的掛起,將線程恢復到運行(就緒)狀態。參數是Thread實例,指定要解除掛起的線程
LockSupport.unpark(thread1); 

park()、parkUntil()、parkNanos()都會讓出cpu使用權,但不釋放鎖。
 

與wait()、notify()相比,LockSupport更加靈活

  • LockSupport的方法不要求寫在同步代碼塊中,線程間不需要使用同步鎖,實現了線程間的解耦
  • unpark()方法可以先於park()方法調用,不用擔心執行的先後順序

 

6、interrupt() 線程中斷

Thread類的實例方法

  • 如果對處於等待、限時等待的線程使用,會中斷等待、限時等待狀態,進入就緒狀態。sleep()、wait()等方法都是放在try中的,如果在等待期間,在其它線程中使用interrupt()中斷,會拋出中斷異常,並立即被喚醒,進入ready狀態。
  • 如果對正在運行的線程使用,會給該線程設置一箇中斷標誌,由該線程自己決定在哪個適合的代碼位置暫停,往往不會立刻停止,需要判斷是否已暫停

① isInterrupted()  Thread類的實例方法,判斷指定線程是否已中斷
② interrupted()  Thread類的靜態方法,判斷當前線程是否已中斷,並清除之前設置的中斷標誌

 

7、使線程進入等待狀態、限時等待狀態的方法對比

方法 類型 使用要求 用途
sleep() Thread類的靜態方法 不釋放鎖 無限制 線程內控制
wait() Object的實例方法 釋放鎖 只能在同步代碼塊中使用、要使用同一把鎖 線程間通信
park()系列 均爲LockSupport類的靜態方法 不釋放鎖 無限制 線程內控制,unpark()可用於線程間通信

均可用interrupt()喚醒

 

線程協作

常見的線程協作模式是生產者/消費者。

一個線程作爲生產者,生產要處理數據,將數據放到倉庫中;可使用juc的LinkedBlockingQueue作爲倉庫,隊列,自然是先進先出的;一個線程作爲消費者,消費|處理倉庫中的數據。LinkedBlockingQueue是阻塞隊列,取不到元素(隊列爲空)時會一直阻塞直到有元素可取。

 

線程死鎖

多條線程彼此都在等待對方持有的鎖,永遠無法繼續往下執行。

 

線程池

爲什麼要使用線程池?
new出一個線程,只是創建了一個對象,並沒有分配線程所需的資源,調用start()纔會分配線程所需的資源,並啓動線程。

線程池預先分配線程所需的資源,把線程放進去,直接使用已分配好的資源;線程執行完畢,線程池回收這條線程的線程資源,留給新放入的線程使用。使用線程池,減少了分配、銷燬線程資源的時間開銷。
 

juc提供了線程池

//創建線程池。前2個參數是核心線程數、最大線程數,中間2個參數指定空閒線程的存活時間,最後一個參數指定隊列
//線程池放不下的會自動放到阻塞隊列中,可指定隊列大小
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 50, 5, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(100));


//把線程放到線程池中,會自動開始執行線程。參數是Runnable接口的實例,Thread已實現Runnable接口
//execute、submit作用相同,只是返回值類型不同
threadPoolExecutor.execute(()->{ });  //返回void,如果隊列也放不下會拋出異常
threadPoolExecutor.submit(()->{ });  //返回Future

 

juc也提供了Executor框架,可以很簡便地創建線程池

//創建並返回一個線程池Executors可以
ExecutorService es = Executors.newSingleThreadExecutor();  //線程池容量爲1,不能擴容
ExecutorService es = Executors.newFixedThreadPool(10);  //指定線程池容量,不能擴容
ExecutorService es = Executors.newScheduledThreadPool(10);  //指定核心線程數,線程池容量不夠時自動擴容。可定時執行
ExecutorService es = Executors.newCachedThreadPool();  //線程池容量不夠時自動擴容。會自動回收空閒線程

Executors創建的線程池不能手動設置隊列容量,隊列容量默認是Integer.MAX_VALUE,如果沒做限流,隊列中大量的線程嚴重影響性能,甚至可能會撐爆內存

核心線程數不宜設置太多,太多會導致切換上下文的時間開銷較大,一般設置爲cpu核心數即可。

 

加入線程池執行的判斷

  • 如果運行的線程數小於核心線程數,即使線程池中有空閒線程,也會創建新線程來處理
  • 如果運行的線程數大於核心線程數、小於線程池最大線程數,則使用空閒線程來處理,如果沒有空閒線程,則創建新線程來處理
  • 如果運行的線程數大於線程池最大線程數,但隊列還未滿,放到隊列中等待
  • 如果隊列已滿,用指定的拒絕策略處理

 

線程池的拒絕(飽和)策略
線程池放不下會放到隊列中,如果隊列滿了,如何處理要加入的線程?

有4種策略

  • AbortPolicy:拋出異常(默認策略)
  • CallerRunsPolicy:用調用者所在線程來執行
  • DiscardPolicy:丟棄要加入的線程
  • DiscardOldestPolicy:丟棄隊列中的第一個線程

可以自定義策略

 

線程池的5種狀態

  • RUNNING:可以接收新線程,可以處理隊列中的線程
  • SHUTDOWN:不接收新線程,但可以處理隊列中的線程
  • STOP:不接收新線程,也不處理隊列中的線程
  • TIDYING:終止所有任務
  • TERMINATED:terminated()方法執行完後進行此狀態
    在這裏插入圖片描述
     

併發編程

併發:同時存在、交替執行
並行:同時存在,同時執行。同時執行依賴多核cpu。
 
一個進程中可以有多條線程,如果是單核cpu,只能交替執行;如果是多核cpu,每個核可以執行1條線程,提高執行效率。
 
多線程在多核cpu下才有優勢,在單核cpu下有線程上下文切換的時間開銷,執行效率反而不如單線程。

 

什麼時候適合使用多線程?

  • 任務會阻塞線程,導致後續的代碼不能儘快執行,eg. 文件IO、大量運算
  • 任務執行時間過長,可以劃分爲互補干擾的子任務,eg. 多線程分段下載
  • 間斷性任務,eg. 定時統計
  • 任務本身需要協作執行,eg. 生產者/消費者模式的線程協作

 

多線程要考慮的問題

  • 硬件資源:cpu核心數、內存、帶寬等
  • 軟件資源:數據庫最大連接數、操作系統允許打開的socket最大數量等
  • 線程安全
  • 線程協作、線程通信

 

頻繁切換上下文,會帶來一定的性能開銷,如何減少上下文切換的開銷?

  • 線程數不宜太多,避免創建不需要的線程
  • 無鎖併發編程。未獲取到鎖會引起線程切換,可以用一些辦法避免使用鎖,減少線程間上下文的切換,eg. 將數組、有序集合按照hashCode取模進行分段,不同的線程處理不同段的數據;使用CAS算法

 

爲什麼不使用多進程而是使用多線程?

  • 線程更加輕量級,啓動、消亡所需時間短,需要的系統資源比進程少得多,且可以共享進程的內存空間
  • 多進程的運行不可預期,且測試困難
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章