這兩天為了定位JBOSS老是掛死的問題,學習了一下JAVA多線程方面的知識,在此總結一下
1、在Java程序中,JVM負責線程的調度。線程調度是指按照特定的機制為多個線程分配CPU的使用權。
調度的模式有兩種:分時調度和搶占式調度。分時調度是所有線程輪流獲得CPU使用權,並平均分配每個線程占用CPU的時間;搶占式調度是根據線程的優先級別來獲取CPU的使用權。JVM的線程調度模式采用了搶占式模式。
2、Thread類實際上也是實現了Runnable接口的類。
在啟動的多線程的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出對象,然后調用Thread對象的start()方法來運行多線程代碼。
實際上所有的多線程代碼都是通過運行Thread的start()方法來運行的。因此,不管是擴展Thread類還是實現Runnable接口來實現多線程,最終還是通過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。
3、JAVA多線程涉及到2個問題,一個是線程的調度,另一個是線程的同步
4、線程的狀態有:new、runnable、running、waiting、timed_waiting、blocked、dead
當執行new Thread(Runnable r)后,新創建出來的線程處於new狀態,這種線程不可能執行
當執行thread.start()后,線程處於runnable狀態,這種情況下只要得到CPU,就可以開始執行了。runnable狀態的線程,會接受JVM的調度,進入running狀態,但是具體何時會進入這個狀態,是隨機不可知的
running狀態中的線程最為復雜,可能會進入runnable、waiting、timed_waiting、blocked、dead狀態:
如果CPU調度給了別的線程,或者執行了Thread.yield()方法,則進入runnable狀態,但是也有可能立刻又進入running狀態
如果執行了Thread.sleep(long),或者thread.join(long),或者在鎖對象上調用object.wait(long)方法,則會進入timed_waiting狀態
如果執行了thread.join(),或者在鎖對象上調用了object.wait()方法,則會進入waiting狀態
如果進入了同步方法或者同步代碼塊,沒有獲取鎖對象的話,則會進入blocked狀態
處於waiting狀態中的線程,如果是因為thread.join()方法進入等待的話,在目標thread執行完畢之后,會回到runnable狀態;如果是因為object.wait()方法進入等待的話,在鎖對象執行object.notify()或者object.notifyAll()之后會回到runnable狀態
處於timed_waiting狀態中的線程,和waiting狀態中的差不多,只不過是設定時間到了,就會回到runnable狀態
處於blocked狀態中的線程,只有獲取了鎖之后,才會脫離阻塞狀態
當線程執行完畢,或者拋出了未捕獲的異常之后,會進入dead狀態,該線程結束
5、當線程池中線程都具有相同的優先級,調度程序的JVM實現自由選擇它喜歡的線程。這時候調度程序的操作有兩種可能:一是選擇一個線程運行,直到它阻塞或者運行完成為止。二是時間分片,為池內的每個線程提供均等的運行機會。
6、設置線程的優先級:線程默認的優先級是創建它的執行線程的優先級。可以更改線程的優先級。
JVM從不會改變一個線程的優先級。然而,1-10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先級進行每兩個或多個合並,變成少於10個的優先級,則兩個或多個優先級的線程可能被映射為一個優先級。
7、Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程。
yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的線程還有可能被線程調度程序再次選中。
結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。
8、另一個問題是線程的同步,這個我感覺比調度更加復雜一些
Java中每個對象都有一個“內置鎖”,也有一個內置的“線程表”
當程序運行到非靜態的synchronized方法上時,會獲得與正在執行代碼類的當前實例(this實例)有關的鎖;當運行到同步代碼塊時,獲得與聲明的對象有關的鎖
釋放鎖是指持鎖線程退出了synchronized方法或代碼塊。
當程序運行到synchronized同步方法或代碼塊時對象鎖才起作用。
一個對象只有一個鎖。所以,如果一個線程獲得該鎖,就沒有其他線程可以獲得鎖,直到第一個線程釋放(或返回)鎖。這也意味着任何其他線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。
9、當提到同步(鎖定)時,應該清楚是在哪個對象上同步(鎖定)?
10、
obj.wait()
obj.notify()
obj.notifyAll()
關於這3個方法,有一個關鍵問題是:
必須從同步環境內調用wait()、notify()、notifyAll()方法。只有擁有該對象的鎖的線程,才能調用該對象上的wait()、notify()、notifyAll()方法
與每個對象具有鎖一樣,每個對象也可以有一個線程列表,他們等待來自該對象的通知。線程通過執行對象上的wait()方法獲得這個等待列表。從那時候起,它不再執行任何其他指令,直到調用對象的notify()方法為止。如果多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續執行。如果沒有線程等待,則不采取任何特殊操作。
11、下面貼幾個代碼實例,配合jstack命令說明一下
- public class ThreadA {
- public static void main(String[] args) {
- ThreadB b = new ThreadB();// ThreadB status: new
- b.start();// ThreadB status: runnable
- synchronized (b) {
- try {
- System.out.println("等待對象b完成計算。。。");
- Thread.sleep(60000);
- b.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("b對象計算的總和是:" + b.total);
- }
- }
- }
- public class ThreadB extends Thread {
- int total;
- public void run() {
- synchronized (this) {
- for (int i = 0; i < 101; i++) {
- total += i;
- }
- notifyAll();
- }
- }
- }
jstack輸出的結果是:
"main" prio=6 tid=0x00846800 nid=0x1638 waiting on condition [0x0092f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at net.kyfxbl.lock.ThreadA.main(ThreadA.java:20)
- locked <0x22a18a90> (a net.kyfxbl.lock.ThreadB)
"Thread-0" prio=6 tid=0x02bbb800 nid=0x1410 waiting for monitor entry [0x02f0f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at net.kyfxbl.lock.ThreadB.run(ThreadB.java:11)
- waiting to lock <0x22a18a90> (a net.kyfxbl.lock.ThreadB)
可以看到,主線程和新線程在同一個對象上鎖定,主線程的方法里執行了Thread.sleep(60000),因此進入了TIMED_WAITING狀態,而新線程則進入BLOCKED狀態
- public class ThreadA {
- public static void main(String[] args) {
- ThreadB b = new ThreadB();// ThreadB status: new
- b.start();// ThreadB status: runnable
- synchronized (b) {
- try {
- System.out.println("等待對象b完成計算。。。");
- b.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("b對象計算的總和是:" + b.total);
- }
- }
- }
- public class ThreadB extends Thread {
- int total;
- public void run() {
- synchronized (this) {
- try {
- Thread.sleep(60000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- for (int i = 0; i < 101; i++) {
- total += i;
- }
- notifyAll();
- }
- }
- }
jstack輸出的結果是:
"main" prio=6 tid=0x00846800 nid=0x1684 in Object.wait() [0x0092f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x22a18b08> (a net.kyfxbl.lock.ThreadB)
at java.lang.Object.wait(Object.java:485)
at net.kyfxbl.lock.ThreadA.main(ThreadA.java:22)
- locked <0x22a18b08> (a net.kyfxbl.lock.ThreadB)
"Thread-0" prio=6 tid=0x02bcc800 nid=0x19c waiting on condition [0x02f0f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at net.kyfxbl.lock.ThreadB.run(ThreadB.java:12)
- locked <0x22a18b08> (a net.kyfxbl.lock.ThreadB)
2個線程還是在同一個對象上同步,但這次主線程立刻執行了b.wait()方法,因此釋放了對象b上的鎖,自己進入了WAITING狀態。接下來新線程得到了對象b上的鎖,所以沒有進入阻塞狀態,緊接着執行Thread.sleep(60000)方法,進入了TIMED_WAITING狀態
- public class ThreadA {
- public static void main(String[] args) {
- ThreadB b = new ThreadB();// ThreadB status: new
- b.start();// ThreadB status: runnable
- synchronized (b) {
- try {
- System.out.println("等待對象b完成計算。。。");
- b.wait();// ThreadB status: running
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println("b對象計算的總和是:" + b.total);
- }
- }
- }
- public class ThreadB extends Thread {
- int total;
- public void run() {
- synchronized (this) {
- for (int i = 0; i < 101; i++) {
- total += i;
- }
- notifyAll();
- try {
- System.out.println("我要睡了");
- Thread.sleep(60000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
jstack輸出的結果是:
"main" prio=6 tid=0x00846800 nid=0x3ec in Object.wait() [0x0092f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x22a18ba0> (a net.kyfxbl.lock.ThreadB)
at java.lang.Object.wait(Object.java:485)
at net.kyfxbl.lock.ThreadA.main(ThreadA.java:20)
- locked <0x22a18ba0> (a net.kyfxbl.lock.ThreadB)
"Thread-0" prio=6 tid=0x02bbb800 nid=0x14b4 waiting on condition [0x02f0f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at net.kyfxbl.lock.ThreadB.run(ThreadB.java:19)
- locked <0x22a18ba0> (a net.kyfxbl.lock.ThreadB)
當主線程執行b.wait()之后,就進入了WAITING狀態,但是新線程執行notifyAll()之后,有一個瞬間主線程回到了RUNNABLE狀態,但是好景不長,由於這個時候新線程還沒有釋放鎖,所以主線程立刻進入了BLOCKED狀態
12、當在對象上調用wait()方法時,執行該代碼的線程立即放棄它在對象上的鎖。然而調用notify()時,並不意味着這時線程會放棄其鎖。如果線程仍然在完成同步代碼,則線程在移出之前不會放棄鎖。因此,只要調用notify()並不意味着這時該鎖被釋放
13、與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級低的並非沒機會執行。
14、在一個線程中開啟另外一個新線程,則新開線程稱為該線程的子線程,子線程初始優先級與父線程相同。
15、JRE判斷程序是否執行結束的標准是所有的前台執線程行完畢了,而不管后台線程的狀態,因此,在使用后台線程時候一定要注意這個問題。
16、下面說說我們這次JBOSS掛死問題的處理方法
現象:系統運行一段時間之后,發現有幾個子系統無法訪問了,但是另外幾個可以。CPU占用達到100%
觀察了一下,發現無法訪問的應用都部署在同一個JBOSS里,於是把該JBOSS的堆棧用jstack命令輸出
發現里面有大量的線程處於BLOCKED狀態,均是在執行到c3p0的一個方法里的某一行時,BLOCKED住了
於是下載c3p0的源碼,跟進去看了一下,這是一個同步方法,內部會去獲取數據庫連接,如果獲取到連接,就進行下一步操作,如果獲取不到,就執行sleep(long timeout)方法。
反推一下,我猜測可能是這樣的:
由於某段代碼沒有釋放數據庫連接-->連接池中的連接耗盡-->部分線程無限TIMED_WAITING-->其余線程都BLOCKED-->開啟新線程-->頻繁引發GC-->占用大量CPU-->應用掛起
后來對所有涉及到數據庫連接的代碼進行排查,發現確實有幾個地方做完數據庫操作以后,沒有釋放連接。把這部分代碼改掉,重新啟動JBOSS,沒有再出現JBOSS掛起的現象