天天看點

Java多線程感悟一

寫在前面

有時候在項目開發中,時不時的會遇到多線程方面的問題。可是要想駕馭它,就必須了解Java多線程方面的知識,認識它的内在。結合我自己的開發經驗以及對Java多線程的一些學習過程,将寫幾篇關于Java多線程的部落格,分享給大家,如有錯誤,請大家指正。

目錄

線程和程序

Thread和Runnable

線程的狀态

生産者和消費者模型:wait/notify

背景線程

interrupt

線程合并join

UncaughtExceptionHandler

線程和程序的概念相信大家都知道,我來說下我的了解。作業系統可以将系統的一些資源,如CPU,記憶體等交給一段運作起來的程式,即程序。程序之間一般是互相獨立的,比如Nginx的工作模型中有一個master程序,多個worker程序,如果有worker程序“挂”了,并不會影響其他worker程序處理請求。在程序内部可以有多個并發執行流,即線程。線程的存在必須依賴程序,同時一個程序内部的多個線程,它們共享程序的資源,互相之間可以影響。線程是輕量級的程序,它的建立、銷毀、切換都比程序要小。

說起Java的多線程,我們腦海裡面可能有如下的想法:

要麼extends Thread,然後重寫run(),調用start()

或者implements Runnable , 實作run(),将Runnable執行個體傳遞給Thread構造方法,調用start()

呵呵,其實,以前,我也是這麼想的。

那麼,在Java線程中,Thread和Runnable到底是什麼,各自扮演着什麼角色?

看一下Thread的類聲明吧:

1

<code>public</code> <code>class</code> <code>Thread </code><code>implements</code> <code>Runnable</code>

從上面的來看,好像它們是“一家人”。

我們可以掃一掃Thread的源碼,發現:

2

3

4

5

<code>/* What will be run. */</code>

<code>private</code> <code>Runnable target;</code>

<code>public</code> <code>Thread(Runnable target) {</code>

<code>init(</code><code>null</code><code>, target, </code><code>"Thread-"</code> <code>+ nextThreadNum(), </code><code>0</code><code>);</code>

<code>}</code>

其實,Runnable接口扮演的是一個任務的角色,通過提供run()來明确這個任務要做什麼。對于Thread而言,預設情況下,它調用的就是你傳遞給它的Runnable的run()方法。Thread類中的target就是用來接收你傳遞進來的任務的,這樣任務是任務,線程是線程,線程可以執行任務,就這麼簡單!

線程的狀态很重要!

就我自己的感覺而言,我們寫多線程方面的代碼,就是在進行線程的狀态轉換進而完成業務上的要求,如果我們不清楚線程的狀态,那麼我們将無從下手,更加看不懂别人的代碼!

根據我的了解,畫了張圖,一起來看下吧。

<a href="http://s3.51cto.com/wyfs02/M00/58/FA/wKioL1TDQY-hNnPdAACMYASm-2g174.jpg" target="_blank"></a>

線程有5個基本狀态:建立、就緒、運作、阻塞、死亡。

當我們建立了一個Thread類的對象時,在JVM中隻是存在了一個Thread對象而已,此時OS中并沒有真正存在一個線程,隻有一個操作線程的外殼,此時為建立狀态。

如果我們調用了start()方法,那麼将從建立狀态轉為就緒狀态。start()方法的調用結束使得OS完成了核心調用,線程已經存在。需要注意的是,start()調用,run()方法一般并不會同步調用,因為run()的調用是CPU的選擇,什麼時候調用,我們無法準确幹預。

當start()調用結束,系統相關資源準備完畢,那麼就會進入運作狀态,調用run()。如果在run過程中,失去了CPU時間片,将回到就緒狀态。比如yield()方法,這個方法會讓出CPU的使用權,希望給其他人用一用,當然這隻是它的想法,也許yield()調用進入就緒狀态後,馬上CPU又排程他執行。

運作狀态中,如果發生了sleep/等待鎖/IO阻塞/wait等,将進入阻塞狀态。如果在阻塞狀态中,睡好了/鎖來了/IO完成了/notify來了,那麼就完成了阻塞狀态的解除,進而進入到就緒狀态,重新等待CPU的排程。

正常情況下,當run()結束或者發生了未捕獲的異常,就會進入死亡狀态。

前面說了那麼多理論,下面我們就來點代碼練練手吧!

業務場景:

有一個籃子,容量是10個,生産者生産饅頭放入其中,消費者從籃子裡面拿饅頭吃。如果生産者發現籃子裡面的饅頭已經滿了,就不再生産,消費者可以消費。如果消費者發現籃子裡面的饅頭空了,就停止消費,生産者可以生産。

饅頭Model:

<code>class</code> <code>ManTou{</code>

籃子容器:

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

<code>class</code> <code>LanZhi{</code>

<code>private</code> <code>int</code> <code>size;</code>

<code>private</code> <code>int</code> <code>index = </code><code>0</code><code>;</code>

<code>private</code> <code>ManTou[] foods;</code>

<code>public</code> <code>LanZhi(</code><code>int</code> <code>size){</code>

<code>this</code><code>.size = size;</code>

<code>this</code><code>.foods = </code><code>new</code> <code>ManTou[size];</code>

<code>public</code> <code>synchronized</code> <code>void</code> <code>produce(ManTou m){</code>

<code>if</code><code>(index == size){</code>

<code>//不應該在生産饅頭</code>

<code>try</code> <code>{</code>

<code>this</code><code>.wait();</code>

<code>} </code><code>catch</code> <code>(InterruptedException e) {</code>

<code>e.printStackTrace();</code>

<code>//饅頭放入籃子</code>

<code>foods[index] = m;</code>

<code>++index;</code>

<code>System.out.println(</code><code>"生産一個饅頭,現在饅頭總共有:"</code> <code>+ index);</code>

<code>//發出通知:可以吃饅頭了</code>

<code>this</code><code>.notify();</code>

<code>public</code> <code>synchronized</code> <code>ManTou eat(){</code>

<code>ManTou m = </code><code>null</code><code>;</code>

<code>if</code><code>(index == </code><code>0</code><code>){</code>

<code>//沒得吃了</code>

<code>m = foods[--index];</code>

<code>System.out.println(</code><code>"吃掉一個饅頭,現在饅頭總共有:"</code> <code>+ index);</code>

<code>//發出通知:可以放入饅頭了</code>

<code>return</code> <code>m;</code>

生産任務:

<code>class</code> <code>ProduceRunnable </code><code>implements</code> <code>Runnable{</code>

<code>private</code> <code>LanZhi l;</code>

<code>public</code> <code>ProduceRunnable(LanZhi l){</code>

<code>this</code><code>.l = l;</code>

<code>@Override</code>

<code>public</code> <code>void</code> <code>run() {</code>

<code>while</code><code>(</code><code>true</code><code>){</code>

<code>l.produce(</code><code>new</code> <code>ManTou());</code>

<code>//假設生産饅頭耗時0.3S</code>

<code>Thread.sleep(</code><code>300</code><code>);</code>

消費任務

<code>class</code> <code>ConsumeRunnable </code><code>implements</code> <code>Runnable{</code>

<code>public</code> <code>ConsumeRunnable(LanZhi l){</code>

<code>l.eat();</code>

<code>//假設吃饅頭耗時1S</code>

<code>Thread.sleep(</code><code>1000</code><code>);</code>

主線程調用:

<code>public</code> <code>static</code> <code>void</code> <code>main(String[] args) {</code>

<code>LanZhi l = </code><code>new</code> <code>LanZhi(</code><code>10</code><code>);</code>

<code>new</code> <code>Thread(</code><code>new</code> <code>ProduceRunnable(l)).start();</code>

<code>new</code> <code>Thread(</code><code>new</code> <code>ConsumeRunnable(l)).start();</code>

說明:

wait/notify這兩個方法都是在object中定義的方法,都應該在臨界區域内發生調用(也就是synchronized區域),否則會發生異常。一個線程好不容易拿到了“門票”,為什麼要調用wait呢?因為有些時候,我們拿到了鎖,準備進行一些操作的時候,發現并不滿足業務上的一些要求,就需要wati了。就好比,我們進入了衛生間,關上了門,蹲在了馬桶上,此時發現,沒有帶紙,沒有辦法,隻好退出衛生間!

線程分為前台線程和背景線程。

首先需要注意的是,JVM認為一旦所有的前台線程運作完畢,隻剩下背景線程的話,那麼就意味着程式要結束了。

舉個例子:

<code>class</code> <code>Daemon </code><code>extends</code> <code>Thread{</code>

<code>for</code><code>(</code><code>int</code> <code>i = </code><code>0</code> <code>; i &lt; </code><code>10</code> <code>; i++){</code>

<code>System.out.println(</code><code>"i : "</code> <code>+ i);</code>

<code>Daemon d = </code><code>new</code> <code>Daemon();</code>

<code>d.setDaemon(</code><code>true</code><code>);</code>

<code>d.start();</code>

<code>System.out.println(</code><code>"hello"</code><code>);</code>

<code>運作結果:</code>

<code>hello</code>

<code>i : </code><code>0</code>

<code>i : </code><code>1</code>

<code>i : </code><code>2</code>

<code>i : </code><code>3</code>

<code>i : </code><code>4</code>

<code>i : </code><code>5</code>

<code>i : </code><code>6</code>

<code>i : </code><code>7</code>

hello的輸出,意味着前台線程已經運作完畢了,那麼JVM将會結束整個程序,并且不保證執行完背景線程。那麼背景線程可以用來幹嘛?比如Java的垃圾回收線程就是一個背景線程,而且它的排程優先級比較低,它不會随便去和其他線程争奪使用CPU。

interrupt,即中斷的意思,那中斷意味着什麼?

我們先看看下面的:

<code>public</code> <code>final</code> <code>native</code> <code>void</code> <code>wait(</code><code>long</code> <code>timeout) </code><code>throws</code> <code>InterruptedException;</code>

<code>public</code> <code>static</code> <code>native</code> <code>void</code> <code>sleep(</code><code>long</code> <code>millis) </code><code>throws</code> <code>InterruptedException;</code>

<code>public</code> <code>final</code> <code>synchronized</code> <code>void</code> <code>join(</code><code>long</code> <code>millis)  </code><code>throws</code> <code>InterruptedException</code>

我們知道有些方法會抛出中斷異常,那什麼時候抛呢?

再去掃一眼Thread源碼,發現:

<code> </code><code>public</code> <code>static</code> <code>boolean</code> <code>interrupted() {</code>

<code>return</code> <code>currentThread().isInterrupted(</code><code>true</code><code>);</code>

<code> </code><code>}</code>

<code> </code><code>public</code> <code>boolean</code> <code>isInterrupted() {</code>

<code>return</code> <code>isInterrupted(</code><code>false</code><code>);</code>

<code> </code><code>private</code> <code>native</code> <code>boolean</code> <code>isInterrupted(</code><code>boolean</code> <code>ClearInterrupted);</code>

在預設情況下,顯然,線程的中斷标志是false。當我們調用了interrupted()後,實際上是将線程中斷标志設定為true。

看一個小例子:

<code>class</code> <code>Thread1 </code><code>extends</code> <code>Thread{</code>

<code>Thread.sleep(</code><code>10000</code><code>);</code>

<code>//其他一些業務操作</code>

<code>System.out.println(</code><code>"sleep over..."</code><code>);</code>

<code>System.out.println(</code><code>"出現中斷異常"</code><code>);</code>

<code>Thread1 t1 = </code><code>new</code> <code>Thread1();</code>

<code>t1.start();</code>

<code>t1.interrupt();</code>

運作結果:

出現中斷異常

說明interrupt()的調用,隻是做了一個标記并産生異常抛出而已,并沒有真正的喚醒線程。

線程合并?為什麼多個線程要合并?什麼時候會出現需要合并呢?怎麼合并?

假設有這麼一個業務排序場景:

我們有N個這樣的塊,每個塊中存放數字,而且塊間有序、塊内是無序的,如下:

第一塊數字範圍:[0,1000]

第二塊數字範圍:[1001,2000]

第三塊數字範圍:[2001,3000]

...

分析:

不應該對所有塊的所有數字來一次全排序,很顯然的是,我們想利用塊間有序這個條件,能不能對每一塊進行排序。要知道,第一塊排序和第二塊排序,他們是互相獨立的,沒有鎖的限制,完全可以并行進行。那我們的初步想法是,啟動N個線程對N個塊進行排序,當N個線程都結束,就完成了排序了。但是要知道,我們無法保證每個排序線程的完成順序,但是業務上,我們确實需要當排序完畢後進行XXX操作。

當然,我們可以寫一個循環進行周遊,來擷取N個排序線程的狀态,進而決定排序完畢後的操作什麼時候進行。其實,Java已經提供了join(),來完成這方面的需要。

執行個體如下:

<code>class</code> <code>SortRunnable </code><code>implements</code> <code>Runnable{</code>

<code>int</code><code>[] array;</code>

<code>public</code> <code>SortRunnable(</code><code>int</code><code>[] array){</code>

<code>this</code><code>.array = array;</code>

<code>for</code><code>(</code><code>int</code> <code>i = </code><code>0</code> <code>; i &lt; array.length ; i++){</code>

<code>for</code><code>(</code><code>int</code> <code>j = i+</code><code>1</code> <code>; j &lt; array.length ; j++){</code>

<code>if</code><code>(array[i] &gt; array[j]){</code>

<code>int</code> <code>tmp = array[i] ^ array[j];</code>

<code>array[i] = tmp ^ array[i];</code>

<code>array[j] = tmp ^ array[j];</code>

<code>int</code><code>[] array1 = {</code><code>1</code><code>,</code><code>9</code><code>,</code><code>8</code><code>,</code><code>7</code><code>};</code>

<code>int</code><code>[] array2 = {</code><code>10</code><code>,</code><code>30</code><code>,</code><code>25</code><code>};</code>

<code>Thread t1 = </code><code>new</code> <code>Thread(</code><code>new</code> <code>SortRunnable(array1));</code>

<code>Thread t2 = </code><code>new</code> <code>Thread(</code><code>new</code> <code>SortRunnable(array2));</code>

<code>t2.start();</code>

<code>t1.join();</code>

<code>t2.join();</code>

<code>for</code><code>(</code><code>int</code> <code>tmp : array1){</code>

<code>System.out.print(tmp + </code><code>" , "</code><code>);</code>

<code>System.out.println();</code>

<code>for</code><code>(</code><code>int</code> <code>tmp : array2){</code>

直接看一個例子,大家就能明白!

<code>class</code> <code>MyException </code><code>implements</code> <code>UncaughtExceptionHandler{</code>

<code>public</code> <code>void</code> <code>uncaughtException(Thread t, Throwable e) {</code>

<code>System.out.println(</code><code>"捕捉到線程中未處理的異常"</code><code>);</code>

<code>Thread t = </code><code>new</code> <code>Thread(){</code>

<code>//業務操作</code>

<code>//...</code>

<code>System.out.println(</code><code>"start..."</code><code>);</code>

<code>Integer.parseInt(</code><code>"a"</code><code>);</code>

<code>System.out.println(</code><code>"end..."</code><code>);</code>

<code>};</code>

<code>t.setUncaughtExceptionHandler(</code><code>new</code> <code>MyException());</code>

<code>t.start();</code>

start...

捕捉到線程中未處理的異常

如果在run()中出現了我們沒有處理的異常,那麼我們還有一次“後悔藥”可以吃~

本文轉自zfz_linux_boy 51CTO部落格,原文連結:http://blog.51cto.com/zhangfengzhe/1607712,如需轉載請自行聯系原作者