Web全堆疊~31.并發
上一期
行程和執行緒的關系
執行緒(英語:thread)是作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,一條執行緒指的是行程中一個單一順序的控制流,一個行程中可以并發多個執行緒,每條執行緒并行執行不同的任務,在Unix System V及SunOS中也被稱為輕量行程(lightweight processes),但輕量行程更多指內核執行緒(kernel thread),而把用戶執行緒(user thread)稱為執行緒,
執行緒是獨立調度和分派的基本單位,執行緒可以為作業系統內核調度的內核執行緒,如Win32執行緒;由用戶行程自行調度的用戶執行緒,如Linux平臺的POSIX Thread;或者由內核與用戶行程,如Windows 7的執行緒,進行混合調度,
同一行程中的多條執行緒將共享該行程中的全部系統資源,如虛擬地址空間,檔案描述符和信號處理等等,但同一行程中的多個執行緒有各自的呼叫堆疊(call stack),自己的暫存器環境(register context),自己的執行緒本地存盤(thread-local storage),
一個行程可以有很多執行緒,每條執行緒并行執行不同的任務,
在多核或多CPU,或支持Hyper-threading的CPU上使用多執行緒程式設計的好處是顯而易見,即提高了程式的執行吞吐率,在單CPU單核的計算機上,使用多執行緒技術,也可以把行程中負責I/O處理、人機互動而常被阻塞的部分與密集計算的部分分開來執行,撰寫專門的workhorse執行緒執行密集計算,從而提高了程式的執行效率,
并行與并發
并發當有多個執行緒在操作時,如果系統只有一個CPU,則它根本不可能真正同時進行一個以上的執行緒,它只能把CPU運行時間劃分成若干個時間段,再將時間 段分配給各個執行緒執行,在一個時間段的執行緒代碼運行時,其它執行緒處于掛起狀,.這種方式我們稱之為并發(Concurrent),
并行:當系統有一個以上CPU時,則執行緒的操作有可能非并發,當一個CPU執行一個執行緒時,另一個CPU可以執行另一個執行緒,兩個執行緒互不搶占CPU資源,可以同時進行,這種方式我們稱之為并行(Parallel),
區別:并發和并行是即相似又有區別的兩個概念,并行是指兩個或者多個事件在同一時刻發生;而并發是指兩個或多個事件在同一時間間隔內發生,在多道程式環境下,并發性是指在一段時間內宏觀上有多個程式在同時運行,但在單處理機系統中,每一時刻卻僅能有一道程式執行,故微觀上這些程式只能是分時地交替執行,倘若在計算機系統中有多個處理機,則這些可以并發執行的程式便可被分配到多個處理機上,實作并行執行,即利用每個處理機來處理一個可并發執行的程式,這樣,多個程式便可以同時執行,
通俗的來說,并行其實就是,多個任務用多個CPU同時運行,而并發則是多個任務由一個CPU通過切換時間片運行,并發并不是真的再同時運行他們,只是給人的感覺,效果上看起來像是,
創建執行緒
繼承Thread
Java中java.lang.Thread這個類表示執行緒,一個類可以繼承Thread并重寫其run方法來實作一個執行緒
public class Test extends Thread{
@Override
public void run() {
System.out.println("run");
}
public static void main(String[] args) {
Thread thread = new Test();
thread.start();
}
}
Test這個類繼承了Thread,并重寫了run方法,run方法的方法簽名是固定的,public,沒有引數,沒有回傳值,不能拋出受檢例外,run方法類似于單執行緒程式中的main方法,執行緒從run方法的第一條陳述句開始執行直到結束,定義了這個類不代表代碼就會開始執行,執行緒需要被啟動,啟動需要先創建一個Test物件,然后呼叫Thread的start方法
start表示啟動該執行緒,使其成為一條單獨的執行流,作業系統會分配執行緒相關的資源,每個執行緒會有單獨的程式執行計數器和堆疊,作業系統會把這個執行緒作為一個獨立的個體進行調度,分配時間片讓它執行,執行的起點就是run方法,
//每個Thread都有一個id和name:
public class Test extends Thread{
@Override
public void run() {
System.out.println("run");
System.out.println(Thread.currentThread().getId());
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread thread = new Test();
thread.start();
}
}
實作Runnable介面
通過繼承Thread來實作執行緒雖然比較簡單,但Java中只支持單繼承,每個類最多只能有一個父類,如果類已經有父類了,就不能再繼承Thread,這時,可以通過實作java.lang.Runnable介面來實作執行緒,Runnable介面的定義很簡單,只有一個run方法,當然,僅僅實作Runnable是不夠的,要啟動執行緒,還是要創建一個Thread物件,
public class Test implements Runnable{
public static void main(String[] args) {
Runnable runnable = new Test();
Thread thread = new Thread(runnable);
thread.start();
}
@Override
public void run() {
System.out.println("run");
}
}
執行緒的基本屬性和方法
id 和 name
每個執行緒都有一個id和name,id是一個遞增的整數,每創建一個執行緒就加一,name的默認值是Thread-后跟一個編號,name可以在Thread的構造方法中進行指定,也可以通過setName方法進行設定,給Thread設定一個友好的名字,可以方便除錯,
優先級
執行緒有一個優先級的概念,在Java中優先級從1到10,默認為5
public final void setPriority(int newPriority)
public final int getPriority()
這種優先級會被映射到作業系統中的執行緒優先級,不過并不是所有作業系統都是10個優先級,Java中不同的優先級可能會被映射到作業系統中相同的優先級,另外,優先級對作業系統而言主要是給了一點建議和提示,并不是強制如此,
狀態
執行緒還有一個狀態的概念,Thread有一個方法用于獲取執行緒的狀態,
public State getState()
public enum State {
//NEW:沒有呼叫start的執行緒狀態為NEW,
NEW,
/*
RUNNABLE:呼叫start后執行緒在執行run方法且沒有阻塞時狀態為RUNNABLE,不過,RUNNABLE不代表CPU一定在執行該執行緒的代碼,可能正在執行也可能在等待作業系統分配時間片,只是它沒有在等待其他條件,
*/
RUNNABLE,
/*
BLOCKED、WAITING、TIMED_WAITING:都表示執行緒被阻塞了,在等待一些條件
*/
BLOCKED,
WAITING,
TIMED_WAITING,
//TERMINATED:執行緒運行結束后狀態為TERMINATED,
TERMINATED;
}
Thread中還有一個方法可以檢查出執行緒是否還或者,當執行緒啟動后,run方法運行結束前,它的回傳值都是true
public final native boolean isAlive()
daemon執行緒
啟動執行緒會啟動一條單獨的執行流,整個程式只有在所有執行緒都結束的時候才退出,但daemon執行緒是例外,當整個程式中剩下的都是daemon執行緒的時候,程式就會退出,
Thread有一個是否daemon執行緒的屬性
public final void setDaemon(boolean on)
public final boolean isDaemon()
daemon一般是其他執行緒的輔助執行緒,在它輔助的主執行緒退出的時候,它就沒有存在的意義了,在我們運行一個即使最簡單的"hello world"型別的程式時,實際上,Java也會創建多個執行緒,除了main執行緒外,至少還有一個負責垃圾回收的執行緒,這個執行緒就是daemon執行緒,在main執行緒結束的時候,垃圾回收執行緒也會退出,
sleep
Thread有一個靜態的sleep方法,呼叫該方法會讓當前執行緒睡眠指定的時間,單位是毫秒
public static native void sleep(long millis) throws InterruptedException;
睡眠期間,該執行緒會讓出CPU,但睡眠的時間不一定是確切的給定毫秒數,可能有一定的偏差,偏差與系統定時器和作業系統調度器的準確度和精度有關,睡眠期間,執行緒可以被中斷,如果被中斷,sleep會拋出InterruptedException
yield()方法
//可以讓出CPU:
public static native void yield();
這也是一個靜態方法,呼叫該方法,是告訴作業系統的調度器:我現在不著急占用CPU,你可以先讓其他執行緒運行,不過,這對調度器也僅僅是建議,調度器如何處理是不一定的,它可能完全忽略該呼叫,
join()方法
可以讓呼叫join的執行緒等待該執行緒結束
public final void join() throws InterruptedException
在等待執行緒結束的程序中,這個等待可能被中斷,如果被中斷,會拋出Interrupted-Exception,join方法還有一個變體,可以限定等待的最長時間,單位為毫秒,如果為0,表示無期限等待
public final synchronized void join(long millis) throws InterruptedException
如果希望main執行緒在子執行緒結束后再退出,main方法可以改為
public class Test implements Runnable{
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Test();
Thread thread = new Thread(runnable);
thread.start();
thread.join();
}
@Override
public void run() {
System.out.println("run");
}
}
共享記憶體
每個執行緒表示一條單獨的執行流,有自己的程式計數器,有自己的堆疊,但執行緒之間可以共享記憶體,它們可以訪問和操作相同的物件,
public class Test{
private static int shared = 0;
private static void incrShared(){
shared++;
}
static class ChildThread extends Thread {
List<String> list;
public ChildThread(List<String> list) {
this.list = list;
}
@Override
public void run() {
incrShared();
list.add(Thread.currentThread().getName());
}
}
public static void main(String[]args) throws InterruptedException {
List<String> list = new ArrayList<String>();
Thread t1 = new ChildThread(list);
Thread t2 = new ChildThread(list);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(shared);
System.out.println(list);
}
}
在這里有三條執行流,一條執行main方法,另外兩條執行ChildThread的run方法,不同執行流可以訪問和操作相同的變數,比如這里面的shared和list變數,不同執行流可以執行相同的程式代碼,就像這里面的incrShared方法,ChildThread的run方法,被兩條ChildThread執行流執行,incrShared方法是在外部定義的,但被ChildThread的執行流執行,當多條執行流執行相同的程式代碼時,每條執行流都有單獨的堆疊,方法中的引數和區域變數都有自己的一份,
當多條執行流可以操作相同的變數時,可能會出現一些意料之外的結果,包括競態條件和記憶體可見性問題
競態條件
當多個執行緒訪問和操作同一個物件時,最終執行結果與執行時序有關,可能正確也可能不正確,
public class Test extends Thread{
private static int counter = 0;
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
counter++;
}
}
public static void main(String[]args) throws InterruptedException {
int num = 1000;
Thread[]threads = new Thread[num];
for(int i = 0; i < num; i++) {
threads[i]= new Test();
threads[i].start();
}
for(int i = 0; i < num; i++) {
threads[i].join();
}
System.out.println(counter);
}
}
有一個共享靜態變數counter,初始值為0,在main方法中創建了1000個執行緒,每個執行緒對counter回圈加1000次,main執行緒等待所有執行緒結束后輸出counter的值, 期望的結果是100萬,但實際執行,發現每次輸出的結果都不一樣,一般都不是100萬,經常是99萬多,為什么會這樣呢?因為counter++這個操作不是原子操作
這個問題可能就需要使用synchronized關鍵字,或者使用顯式鎖、原子變數等方法來解決了
記憶體可見性
多個執行緒可以共享訪問和操作相同的變數,但一個執行緒對一個共享變數的修改,另一個執行緒不一定馬上就能看到,甚至永遠也看不到,
public class Test extends Thread{
private static boolean shutdown = false;
static class My{
public void run() {
while(!shutdown){
}
System.out.println("exit hello");
}
}
public static void main(String[]args) throws InterruptedException {
new Test().start();
Thread.sleep(1000);
shutdown = true;
System.out.println("exit main");
}
}
在這個程式中,有一個共享的boolean變數shutdown,初始為false,My在shutdown不為true的情況下一直死回圈,當shutdown為true時退出并輸出"exit hello",main執行緒啟動My后休息了一會兒,然后設定shutdown為true,最后輸出"exit main", 期望的結果是兩個執行緒都退出,但實際執行時,很可能會發現HelloThread永遠都不會退出,也就是說,在My執行流看來,shutdown永遠為false,即使main執行緒已經更改為了true,
這就是記憶體可見性問題,在計算機系統中,除了記憶體,資料還會被快取在CPU的暫存器以及各級快取中,當訪問一個變數時,可能直接從暫存器或CPU快取中獲取,而不一定到記憶體中去取,當修改一個變數時,也可能是先寫到快取中,稍后才會同步更新到記憶體中,在單執行緒的程式中,這一般不是問題,但在多執行緒的程式中,尤其是在有多CPU的情況下,這就是嚴重的問題,一個執行緒對記憶體的修改,另一個執行緒看不到,一是修改沒有及時同步到記憶體,二是另一個執行緒根本就沒從記憶體讀,
解決這個問題可以使用volatile關鍵字或者synchronized
執行緒的成本
創建執行緒需要消耗作業系統的資源,作業系統會為每個執行緒創建必要的資料結構、堆疊、程式計數器等,創建也需要一定的時間,此外,執行緒調度和切換也是有成本的,當有大量可運行執行緒的時候,作業系統會忙于調度,為一個執行緒分配一段時間,執行完后,再讓另一個執行緒執行,一個執行緒被切換出去后,作業系統需要保存它的當前背景關系狀態到記憶體,背景關系狀態包括當前CPU暫存器的值、程式計數器的值等,而一個執行緒被切換回來后,作業系統需要恢復它原來的背景關系狀態,整個程序稱為背景關系切換,這個切換不僅耗時,而且使CPU中的很多快取失效,
如果執行緒中實際執行的事情比較多,這些成本是可以接受的;另外,如果執行的任務都是CPU密集型的,即主要消耗的都是CPU,那創建超過CPU數量的執行緒就是沒有必要的,并不會加快程式的執行,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/256787.html
標籤:java
