首先,我們討論一下進程,進程是操作系統級別下,單獨執行的一個任務。Win32 Unix都是多任務操作系統。多任務,并發執行是一個宏觀概念,實際微觀串行。CPU同一時間刻只能執行一個任務。OS負責進程調度,使用CPU,也就是獲得時間片。在一個進程中,再可以分為多個程序順序執行流,每個執行流就是一個線程。分配CPU時間片的依然是CPU,多線程時,程序會變慢,每個線程分配到的時間片少了。進程與線程的區別,進程是數據獨占的(獨立數據空間),線程是數據共享的(這也是線程之間通訊容易的原因,不需要傳遞數據)。Java是語言級支持多線程的,體現在有現成的封裝類(java.lang.Thread)完成了必要的并發細節的工作(與操作系統打交道,分配PID等)。兩種方式來得到一個線程對象。
一個線程對象--〉代表著一個線程--〉一個順序執行流(run方法)這個程序有兩個線程,一個是main主線程,它調用了t1.start(),這是t1線程只是就緒狀態,還沒有真正啟動線程,main主線程結束了!!t1運行。兩個線程都退出了,進程完結。一個進程退出,要等待進程中所有線程都退出,再退出虛擬機。方式2,實現java.lang.Runable接口,這是這個類的對象是一個目標對象,而不能理解為是一個線程對象。
不要調用run()方法,它只是執行一下普通的方法,并不會啟動單獨的線程。上面只是線程狀態圖。在某一個時間內,處于運行狀態的線程,執行代碼,注意可能多個線程多次執行代碼。CPU會不斷從可運行狀態線程調入運行,不會讓CPU空閑。Thread.sleep(1000);當前線程睡眠1秒鐘,休眠后->進入阻塞->休眠結束->回到可運行狀態。在run(),有異常拋出,必須try{}catch(Exception e){},不能throws Exception,因為run()方法覆蓋不能拋例外。能進入運行狀態,只能由操作系統來調度。一旦sleep-〉阻塞->交出程序執行權。等待用戶輸入,輸入輸出設備占用CPU,處于阻塞的線程沒有機會運行,輸入完畢,重新進入可運行狀態。第三種進入阻塞狀態的可能。t1.join()調用后,運行狀態線程放出執行權,作為t1的后續線程,等待t1結束。也就是說至少得等t1線程run完畢,才可能進入運行狀態來執行,可不是說t1執行完,一定馬上就是調用t1.join()的線程馬上進入可運行行狀態。只有操作系統有權利決定誰進入運行狀態。join的實質就是讓兩個線程和二為一,串行。t1.join();執行這條語句現場是被保護起來的。t1結束,調用線程有機會運行時,會從上次的位置繼續運行。
線程優先級,setPriotity(1--100),數越大,優先級越高。開發中不提倡自省設置優先級,操作系統可能忽略優先級,不具有跨平臺性(兩方面,可運行,執行效果一致),因為這種方式很粗略。static void yield(),運行狀態的線程(當前線程),調用yield方法,馬上交出執行權。回到可運行狀態。
=============================
Thread對象有個run方法,當start()時,Thread進行系統級調用,系統分配一個線程空間,此時對象可以獲得CPU時間片,一個順序執行流程可以獨立運行,線程結束,對象還在,只是系統回收線程。=============================兩個線程同時的資源,稱為臨界資源,會有沖突。堆棧數據結構,有一個char[]和一個index(表示實際長度,也表示下一個要插入元素的位置)。一個push操作,有兩個核心操作(加元素,修改index)。都執行和都沒執行,沒有問題。但假設一個線程做了一個步,就交出執行權,別的線程,執行同樣的代碼,會造成數據不一致。數據完整性也是一個要在開發中注意的地方。------------------------------------------所以為了保證數據安全,要給數據加鎖。一個Java對象,不僅有屬性和方法,還有別的東西。任何一個對象,都有一個monitor,互斥鎖標記,可以交給一個線程。只有拿到這個對象互斥鎖標記的線程,才能訪問這個對象。synchronized可以修飾方法和代碼塊。synchronized(obj){ obj.setValue(123);}不是每個線程都能進入這段代碼塊,只有拿到鎖標記的線程才能進入執行完,釋放鎖標記,給下一個線程。記住,鎖標記是對對象來說的,鎖的是對象。當synchronized標識方法時,那么就是鎖當前對象。
注意此代碼中,Stack這個臨界資源類中的push方法中,有一個Thread.sleep,它讓當前進程阻塞,也就是讓擁有Stack對象s鎖標記的線程阻塞,但這時它并不釋放鎖標記。所以Synchronized使用是有代價的,犧牲效率換數據安全,要控制synchronized代碼塊,主要是數據寫,修改做同步限制,讀就不用了。還有一點要注意:synchronized不能繼承, 父類的方法是synchronized,那么其子類重載方法中就不會繼承“同步”。一個線程可以擁有很多對象鎖標記,但一個對象的鎖標記只能給一個線程。等待鎖標記的線程,進入該對象的鎖池。每個對象都有一個空間,鎖池,里面都是等待拿到該對象的鎖標記的線程。當然還是操作系統來決定誰來獲得鎖標記,在上一個鎖標記釋放掉后。死鎖,線程A拿到resourceA標記,去請求resourceB;線程B拿到resourceB標記,去請求resourceA;線程間通訊機制->協調機制一個對象不僅有鎖和鎖池,另外還有一個空間[等待隊列]。synchronized(路南){ 想要獲得路北資源的線程,調用路南.wait();將自己的所有鎖標記都釋放。以便其他線程滿足條件運行程序后,自己也就可以正常通過了。}調用obj.wait(),表示某一個線程釋放所有鎖標記并進入obj這個對象的等待隊列。等待隊列也是阻塞狀態。一個線程調用obj對象的notify(),會通知等待隊列中的一個線程可以出來,notifyAll()是通知所有線程。
上面為經典的生產者消費者問題,生產者使用SyncStack的push方法,消費者使用pop方法。push方法: while (index==data.length) { try{ this.wait();//<-----------釋放所有鎖標記,阻塞現場保留 } catch (InterruptedException e){} }如果貨架滿了,生產者即使擁有鎖標記,也不能再生產商品了,必須wait()。等待消費者消費物品,否則永遠不會從SyncStack對象的等待隊列中出來。等待通知,何時通知呢?public synchronized char pop() { while (index==0) { try{ this.wait(); } catch (InterruptedException e){} } index--; char c=data[index]; data[index]=' '; System.out.println("Char "+c+" Poped from Stack"); for(int k=0;k<data.length;k++) System.out.print(data[k]); System.out.println(); this.notifyAll(); //<-------------所有等待隊列中的生產者都出了隊列,因為沒有鎖標記,只能進入鎖池。
return c; }
為什么判斷是一個while循環,而不是一個if呢?注意:我們假設一種情形:1) 有十個生產者線程,貨架已經滿了,10生產者依次獲得鎖標記,依次都調用this.wait(),都進入同一個SyncStack對象的等待隊列,10個進程阻塞住,2) 有一個消費者線程獲得該SyncStack對象鎖標記,一個消費者消費一個,執行完消費,調用this.notifyAll()釋放鎖標記。3) 剛才消費者調用SyncStack對象的notifyAll()后,10個線程都出來了,準備生產商品,全部進入鎖池。這十個線程的代碼現場,還在wait()這個函數調用后面,也就是一旦或者鎖標記,要繼續從這里執行。this.wait();//從這一句的后面繼續執行。4) 但如果有一個生產者push的話,貨架已經就滿了,但這時還有9個在鎖池中,依次獲得鎖標記,但由于是while需要再次判斷是否貨架滿不滿,才能繼續前行進行生產。如果是if,就會直接push,數組越界。===================================釋放鎖標記只有兩種途徑,代碼執行完,wait()讓線程結束,就是想辦法讓run方法結束。注意下面的bStop,標志位,可以在線程進入wait狀態時,對某一線程調用interrupt(),線程拋出InterruptedException,然后根據標志位,方法返回。
Exc:AB1CD2.... 數字與字母依次打印。用線程完成。