天天看點

Java基礎(七)——多線程

  java vm 啟動的時候會有一個程序java.exe,該程序中至少有一個線程負責java程式的執行。而且這個線程運作的代碼存在于main方法中,該線程稱之為主線程。其實從細節上來說,jvm不止啟動了一個線程,其實至少有三個線程。除了main() 主線程,還有 gc() 負責垃圾回收機制的線程,異常處理線程。當然如果發生異常,會影響主線程。

  局部的變量在每一個線程區域中都有獨立的一份。

  程式(program)是為完成特定任務、用某種語言編寫的一組指令的集合 。即一段靜态的代碼 ,靜态對象。

  程序(process)是程式的一次執行過程,或是正在運作的一個程式。是一個動态過程,有它自身的産生、存在和消亡的過程——生命周期。如運作中的qq、運作中的 mp3播放器。

  線程(thread)是一個程式内部的一條執行路徑。由程序可進一步細化為線程。若一個程序同一時間并行執行多個線程,就是支援多線程的。

Java基礎(七)——多線程

  了解:

  程式是靜态的,程序是動态的。

  程序作為資源配置設定的基本機關,系統在運作時會為每個程序配置設定不同的記憶體區域。每一個程序執行都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元。一個程序中至少有一個線程。一個程序當中有可能會存在多條執行路徑。

  線程作為排程和執行的基本機關,每個線程擁有獨立的運作棧和程式計數器(pc),線程切換的開銷小。是程序中一個獨立的控制單元,線程在控制着程序的執行。是程序中的内容。

  一個程序中的多個線程共享相同的記憶體單元/記憶體位址空間,它們從同一堆中配置設定對象,可以通路相同的變量和對象。這就使得線程間通信更簡便、高效。但多個線程操作共享的系統資源可能就會帶來安全隐患。

  形象比喻:一個寝室就是一個程序,能住四個人,就有四個線程,四個人共享陽台和廁所,就是通路相同的變量和對象。而各自的書桌與床是各自私有的。

  為什麼有安全隐患?java記憶體模型:

Java基礎(七)——多線程

  class loader:類加載器。

  execution engine:執行引擎負責解釋指令,送出作業系統執行。

  native interface:本地接口。

  runtime data area:運作時資料區。

  程序可以細化為多個線程。每個線程,擁有自己獨立的棧和程式計數器;多個線程共享同一個程序中的方法區和堆。也就是說:

  虛拟機棧、程式計數器:一個線程一份,線程私有。

  方法區、堆:一個程序一份,也就是多個線程共享一份。

  結論:多個線程可以共享(一個程序中的)堆(有變量和對象)和方法區,是以實作多個線程間的通信是比較友善的,但是也導緻多個線程操作共享資源可能會帶來安全隐患。線程同步即用來解決上面的安全隐患。

  單核cpu:其實是一種假的多線程,因為在一個時間單元内,隻能執行一個線程的任務。例如:雖然有多車道,但是收費站隻有一個從業人員在收費,隻有收了費才能通過,那麼cpu 就好比收費人員。如果有某個人不想交錢,那麼收費人員可以把他"挂起"(晾着他,等他想通了,準備好了錢,再去收費),但是因為cpu時間單元特别短,是以感覺不出來。

  多核cpu:如果是多核的話,才能更好的發揮多線程的效率。現在的伺服器都是多核的。

  并行:多個cpu同時執行多個任務。比如:多個人同時做不同的事。

  并發:一個cpu同時執行多個任務(采用時間片原理)。比如:秒殺、多個人做同一件事。

  提高應用程式的響應。對圖形化界面更有意義,可增強使用者體驗;提高計算機系統cpu的使用率;改善程式結構,将既長又複雜的程序分為多個線程,獨立運作,利于了解和修改。

  thread 類的特性:

  每個線程都是通過某個特定 thread 對象的 run() 方法來完成操作的,經常把 run() 方法的主體稱為線程體。

  通過該 thread 對象的 start() 方法來啟動這個線程,而非直接調用 run()。

Java基礎(七)——多線程

  說明:

  run() 方法由 jvm 調用,什麼時候調用,執行的過程控制都由作業系統的 cpu 排程決定 。

  想要啟動多線程,必須調用 start() 方法 。

  一個線程對象隻能調用一次 start() 方法啟動,如果重複調用了,則将抛出異常"illegalthreadstateexception"。

  jdk 1.5 之前建立線程有兩種方法:繼承 thread 類的方式、實作 runnable 接口的方式。

  jdk 5.0 之後,新增兩種方法:實作 callable 接口的方式、線程池。

  代碼示例:方式一、繼承 thread 類

Java基礎(七)——多線程
Java基礎(七)——多線程

繼承thread類

  代碼示例:方式二、實作 runnable 接口

  調用 thread 類的 start 方法開啟線程,會調用目前線程的 run() 方法。

Java基礎(七)——多線程
Java基礎(七)——多線程

實作runnable接口

  代碼示例:方式三、實作 callable 接口

Java基礎(七)——多線程
Java基礎(七)——多線程

實作callable接口

  值得注意:

  将futuretask作為runnable實作類傳遞,本質為方式二。而調用 thread 類的 start 方法開啟線程,會調用目前線程的 run() 方法。

  目前線程的 run()方法 --> futuretask.run() --> callable.call()。

  執行個體都是通過構造器初始化的。

  傳回值即為 call() 方式的傳回值。

  方式四:線程池

  見标簽:聊聊并發

  ①相比繼承,實作runnable接口方式

  好處:避免了單繼承的局限性。通過多個線程可以共享同一個接口實作類的對象,天然就能展現多個線程處理共享資料的情況(有共享變量)。參考賣票案例。

  應用:建立了多個線程,多個線程共享資料,則使用實作接口的方式,資料天然的就是共享的。

在定義線程時,建議使用實作接口的方式。

  ②相比runnable,callable功能更強大些:

  比run()方法,call()有傳回值;call()可以抛出異常。被外面捕獲,擷取異常資訊;支援泛型的傳回值;需要借助futuretask類,比如擷取傳回結果。

  void start():啟動線程,并執行對象的 run() 方法。   run():線程在被排程時執行的方法體。   string getname():傳回線程的名稱。   void setname(string name):設定該線程的名稱。   static thread currentthread():傳回目前線程對象 。在 thread 子類中就是 this ,通常用于主線程和 runnable 實作類。   static void yield():線程讓步,釋放目前cpu的執行權。下一刻可能又立馬得到。暫停目前正在執行的線程,把執行機會讓給優先級相同或更高的線程。若隊列中沒有同優先級的線程,忽略此方法。   join():線上程a中調用線程b的join(),此時線程a就進入阻塞狀态,直到線程b完全執行完以後,線程a才結束阻塞狀态(相當于插隊)。低優先級的線程也可以獲得執行。   static void sleep(long millis): 讓目前線程睡眠指定毫秒數,在睡眠期間,目前線程是阻塞狀态。不會釋放鎖。令目前活動線程在指定時間段内放棄對 cpu 控制,使其他線程有機會被執行,時間到後重排隊。抛出 interruptedexception 異常。   stop():強制線程生命期結束,不推薦使用。(已過時)。   boolean isalive():傳回 boolean,判斷線程是否還活着。

  代碼示例:join() 使用

  排程政策:時間片輪訓;搶占式,高優先級的線程搶占cpu。

Java基礎(七)——多線程

  java 的排程方法:同優先級線程組成先進先出隊列(先到先服務),使用時間片政策;對高優先級,使用優先排程的搶占式政策。

  等級:

  max_priority:10   min priority:1   norm_priority:5

  涉及的方法:

  getpriority():傳回線程優先級   setpriority(int newpriority):設定線程的優先級

  說明:線程建立時繼承父線程的優先級;高優先級的線程要搶占低優先級線程cpu的執行權。隻是從機率上講,高優先級的線程高機率被執行,低優先級隻是獲得排程的機率低,并非一定是在高優先級線程執行完之後才被調用。

  java中的線程分為兩類:一種是守護線程,一種是使用者線程 。

  它們在幾乎每個方面都是相同的,唯一的差別是判斷 jvm 何時離開。

  守護線程是用來服務使用者線程的,通過在 start() 方法前調用thread.setdaemon(true)可以把一個使用者線程變成一個守護線程。

  java 垃圾回收就是一個典型的守護線程。main() 主線程就是一個使用者線程。

  若 jvm 中都是守護線程,目前 jvm 将退出 。當使用者線程執行完畢,守護線程也結束。形象了解:兔死狗烹,鳥盡弓藏。

  代碼示例:方式一、三個視窗同時售100張票

  由于 new 了三次,結果三個視窗各自出售了100張(共300張)。要想三個視窗共同賣 100 張票,對共享變量的通路。修改如下:

  結果:100号(也可能是其他号)票依然有重複,這裡就存線上程安全問題。

  代碼示例:方式二、三個視窗同時售100張票

  上面兩種方式,隻是實作不同,但都存線上程安全問題。後面會解決。

  jdk 中用 thread.state 枚舉定義了線程的幾種狀态。要想實作多線程必須在主線程中建立新的線程對象。java 語言使用 thread 類及其子類的對象來表示線程,在它的一個完整的生命周期中通常要經曆如下的五種狀态:

  建立:當一個 thread 類或其子類的對象被聲明并建立時,新生的線程對象處于建立狀态。

  就緒:處于建立狀态的線程被 start() 後,将進入線程隊列等待 cpu 時間片,此時它已具備了運作的條件,隻是沒配置設定到 cpu 資源。

  運作:當就緒的線程被排程并獲得 cpu 資源時便進入運作狀态,開始執行 run(),run() 方法定義了線程的操作和功能。

  阻塞:在某種特殊情況下,被人為挂起或執行輸入輸出操作時,讓出 cpu 并臨時中止自己的執行,進入阻塞狀态。

  死亡:線程完成了它的全部工作或線程被提前強制性的中止或出現異常導緻結束。

2、狀态轉換圖

Java基礎(七)——多線程
  線程類 thread 的方法:thread.yield()、thread.sleep()   對象類 object 的方法:wait()、notify()、notifyall()   線程對象的方法:其餘都是。   suspend():挂起。為什麼過時?因為可能會導緻死鎖。已過時   resume():結束挂起的狀态。容易導緻死鎖。已過時   stop():線程終止。已過時

  suspend()、resume():容易導緻死鎖,這兩個操作就好比播放器的暫停和恢複。但這兩個 api 是過期的,也就是不建議使用的。

  不推薦使用 suspend() 去挂起線程的原因,是因為 suspend() 導緻線程暫停的同時,并不會去釋放任何鎖資源。其他線程都無法通路被它占用的鎖。直到對應的線程執行 resume() 方法後,被挂起的線程才能繼續,進而其它被阻塞在這個鎖的線程才可以繼續執行。

  但是,如果 resume() 操作出現在 suspend() 之前執行,那麼線程将一直處于挂起狀态,同時一直占用鎖,這就産生了死鎖。而且,對于被挂起的線程,它的線程狀态居然還是 runnable。

  上述售票案例中,不管是方式一還是方式二,都存在重票的情況,這裡讓線程睡眠0.1s,暴露出錯票的情況。這就是線程安全問題。

  代碼示例:有線程安全問題的售票

  問題:很明顯,上述售票有重票(9),還是錯票(-1),那麼如何解決這種線程安全問題呢?

  注意:這裡并不是加了sleep之後,才出現重票錯票的情況。sleep隻是将這種情況出現的機率提高了。

  解決:解決線程安全問題有兩種方式

  ①synchronize(隐式鎖):同步代碼塊、同步方法

  ②lock(顯式鎖)

  ①什麼是需要被同步的代碼?

  有沒有共享資料:多個線程共同操作的變量。案例中的 ticket。

  有操作共享資料的代碼,即為需要被同步的代碼。

  同步螢幕,俗稱:鎖。任何一個類的對象,都可以充當鎖。要求:多個線程必須共用同一把鎖。

  ②優缺點?

  優點:同步的方式,解決了線程安全的問題。

  缺點:對同步代碼的操作,隻能有一個線程參與,其他線程必須等待。相當于是一個單線程的過程,效率低。

  代碼示例:處理"實作runnable的線程安全問題"

  注意:以下的 object 可以換成 this。

  代碼示例:處理"繼承thread類的線程安全問題"

  仿造前一個的方案:傳入一個object,是行不通的。原因:不是同一把鎖。有三個object。寫 this 也是不對的,正确寫法:

  以下的 object 可以換成 window.class。類:也是對象。window.class 隻會加載一次。

  深刻了解:synchronized包含的代碼塊一定不能包含了while。這樣會導緻,第一個拿到鎖的線程把票賣光之後,才釋放鎖,這不符合題意。

  如果操作共享資料的代碼完整的聲明在一個方法中,那麼不妨将此方法聲明為同步的。

  總結:同步方法仍然涉及到鎖,隻是不需要我們顯式聲明;非靜态的同步方法,鎖是this,靜态的同步方法,鎖是類對象。

  死鎖:不同的線程分别占用對方需要的同步資源不放棄,都在等待對方釋放自己需要的同步資源,就形成了線程的死鎖。

  出現死鎖後,不會出現異常,不會出現提示,隻是所有的線程都處于阻塞狀态,無法繼續。我們同步時,應盡量避免出現死鎖。

  解決方法:專門的算法、原則。盡量減少同步資源的定義。盡量避免嵌套同步。

  代碼示例:死鎖

  讓線程一在擷取到buffer1的時候,睡眠0.1s。線程二擷取到buffer2的時候,睡眠0.1s,可以讓死鎖的問題暴露出來。

  從jdk 5.0 開始,java提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實作同步。同步鎖使用lock對象充當。

  java.util.concurrent.locks.lock接口是控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對lock對象加鎖,線程開始通路共享資源之前應先獲得lock對象。

  reentrantlock類(可重入鎖)實作了lock,它擁有與synchronize相同的并發性和記憶體語義,在實作線程安全的控制中,比較常用的是reentrantlock,可以顯示加鎖、釋放鎖。

  代碼示例:用 lock 解決賣票的同步問題。

  說明:即使遇到break,finally裡的代碼塊也會被執行。

  reentrantlock(boolean fair):含參構造器,fair:true,公平鎖、線程先進先出。保證當線程一、線程二、線程三來了之後,線程一執行完之後,線程二拿到鎖,而不是線程一又拿到鎖。false:非公平鎖、多個線程搶占式。

  reentrantlock():無參構造器,fair為false。則不難了解為什麼是上述結果了。

  值得注意的是:try{}并非為了捕獲異常,此次代碼也沒有catch塊,是為了執行finally塊将鎖釋放出來。否則會導緻死循環(賣票情況正常,且沒有同步問題)。

  不寫try,finally的結果:死循環

Java基礎(七)——多線程

  說明:視窗一賣出最後一張票,且釋放了鎖。視窗二擷取到鎖以後,此時ticket = 0,則執行break,視窗二跳出while循環,但此時并沒有釋放鎖。而另外兩個線程一直在等待擷取鎖,導緻了死循環。

  相同:都用于解決線程安全問題

  不同:synchronize機制在執行完相應的同步代碼之後,會自動的釋放鎖。而lock需要手動擷取鎖,同時需要在finally中手動釋放鎖。

  lock是一個接口,而synchronized是關鍵字。

  lock是顯示鎖(必須手動開啟與釋放),synchronize是隐式鎖,出了作用域自動釋放。

  lock可以讓等待鎖的線程響應中斷,而synchronize不會,線程會一直等下去。

  lock可以知道線程有沒有拿到鎖,而synchronize不能。

  lock可以實作多個線程同時讀操作,而synchronize不能,線程必須等待。

  lock隻有代碼塊鎖,synchronize有代碼塊鎖和方法鎖。

  使用lock鎖,jvm将花費較少的時間來排程線程,性能更好,并且具有更好的擴充性(提供更多的子類)。

  lock有比synchronize更精确的線程語義和更好的性能,lock還有更強大的功能,例如,它的trylock方法可以非阻塞的方式去拿鎖。

  優先使用順序:

  lock-->同步代碼塊(進入了方法體,配置設定了相應資源)-->同步方法(在方法體外)

  代碼示例:銀行有一個賬戶,兩個儲戶分别向同一個賬戶存3000元,每次存1000,存3次。每次存完列印賬戶餘額。

  問:wait 等待中的線程被 notify 喚醒了會立馬執行嗎?

  答:不會。被喚醒的線程需要重新競争鎖對象,獲得鎖的線程可以從wait處繼續往下執行。

  代碼示例:使用兩個線程交替列印1—100。

Java基礎(七)——多線程
Java基礎(七)——多線程

錯誤示例

  這裡需要用到線程的通信,正确方式如下:

Java基礎(七)——多線程
Java基礎(七)——多線程

交替列印

  注意:上面兩個線程公用同一把鎖 num,this 指num。

  若此時将同步螢幕換成

  會報異常"illegalmonitorstateexception"。同步螢幕非法

Java基礎(七)——多線程

  原因:預設情況下,方法前有一個this,而鎖卻是object。

  如果要用object當鎖,需要修改為:

  了解:其實不難了解,解決線程同步問題,是要求要同一把鎖。鎖是object,而喚醒卻是this,就不知道要喚醒誰了呀?應該喚醒跟我(目前線程)共用同一把鎖的線程,喚醒别人(别的鎖)有什麼意義呢?而且本身還是錯的。

  形象了解:一個寝室四個人(四個線程)有一個廁所(共享資源),共用廁所(多個線程對共享資源進行通路)有安全隐患,如何解決?加鎖。當甲進入廁所時,将廁所門前挂牌(鎖)拿走(獲得鎖),然後使用廁所(此時其他人都進不來,必須在門外等待獲得挂牌,即時此時甲在廁所睡着了sleep(),其他人依然要等待,因為甲依然拿着挂牌,線程沒有釋放鎖),使用完畢後,将挂牌挂于門前(線程釋放鎖),其他三人方可使用,使用前先競争鎖。

若要求兩人交替使用廁所,那麼當甲使用完畢,通知(notify)乙使用,甲去等待(wait)下一次使用,自然而然,甲需要釋放鎖。就是甲使用時,乙等待,甲用完了,通知乙(我用完了,你去吧),乙使用時,甲等待,乙用完了,通知甲(我用完了,你去吧)。那麼很自然的問題是,甲用完後,通知誰?是通知和我競争同一個廁所的人,不會去通知隔壁寝室的人(即我用完了,釋放出鎖,通知競争這把鎖的線程)。

  總結:

  wait():一旦執行此方法,目前線程進入阻塞狀态,并釋放鎖。

  notify():一旦執行此方法,就會喚醒一個被wait()的線程。如果有多個,就喚醒優先級高的,如果優先級一樣,則随機喚醒一個。

  notifyall():一旦執行此方法,會喚醒所有wait()的線程。

  以上三個方法必須使用在同步代碼塊或同步方法中,這裡才有鎖。如果是lock,有别的方式(暫未介紹,可自行百度)。

  以上三個方法的調用者必須是同步代碼塊或同步方法中的同步螢幕。否則會出現異常。

  而任何一個類的對象,都可以充當鎖。則當鎖是object時,根據上一條,以上三個方法調用者就是object,是以定義在java.lang.object類中。

  相同:都可以使目前線程進入阻塞狀态。

  不同:聲明位置不同,thread類中聲明sleep(),object類中聲明wait();sleep()随時都可以調用,wait()必須在同步代碼塊或同步方法中;sleep()不會釋放鎖,wait()會釋放鎖;sleep(),逾時或者調用interrupt()方法就可以喚醒,wait(),等待其他線程調用對象的notify()或者notifyall()方法才可以喚醒。

作者:craftsman-l

本部落格所有文章僅用于學習、研究和交流目的,版權歸作者所有,歡迎非商業性質轉載。

如果本篇部落格給您帶來幫助,請作者喝杯咖啡吧!點選下面打賞,您的支援是我最大的動力!