本文是兩章的筆記整理。java
本文主要講述了synchronized
以及ThreadGroup
的基本用法。編程
synchronized
synchronized
能夠防止線程干擾和內存一致性錯誤,具體表現以下:數組
synchronized
提供了一種鎖機制,可以確保共享變量的互斥訪問,從而防止數據不一致的問題synchronized
包括monitor enter
和monitor exit
兩個JVM
指令,能保證在任什麼時候候任何線程執行到monitor enter
成功以前都必須從主存獲取數據,而不是從緩存中,在monitor exit
運行成功以後,共享變量被更新後的值必須刷入主內存而不是僅僅在緩存中synchronized
指令嚴格遵循Happens-Beofre
規則,一個monitor exit
指令以前一定要有一個monitor enter
synchronized
的基本用法能夠用於對代碼塊或方法進行修飾,好比:緩存
private final Object MUTEX = new Object(); public void sync1(){ synchronized (MUTEX){ } } public synchronized void sync2(){ }
一個簡單的例子以下:bash
public class Main { private static final Object MUTEX = new Object(); public static void main(String[] args) throws InterruptedException { final Main m = new Main(); for (int i = 0; i < 5; i++) { new Thread(m::access).start(); } } public void access(){ synchronized (MUTEX){ try{ TimeUnit.SECONDS.sleep(20); }catch (InterruptedException e){ e.printStackTrace(); } } } }
編譯後查看字節碼:服務器
javap -v -c -s -l Main.class
access()
字節碼截取以下:多線程
stack=3, locals=4, args_size=1 0: getstatic #9 // Field MUTEX:Ljava/lang/Object; 獲取MUTEX 3: dup 4: astore_1 5: monitorenter // 執行monitor enter指令 6: getstatic #10 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit; 9: ldc2_w #11 // long 20l 12: invokevirtual #13 // Method java/util/concurrent/TimeUnit.sleep:(J)V 15: goto 23 // 正常退出,跳轉到字節碼偏移量23的地方 18: astore_2 19: aload_2 20: invokevirtual #15 // Method java/lang/InterruptedException.printStackTrace:()V 23: aload_1 24: monitorexit // monitor exit指令 25: goto 33 28: astore_3 29: aload_1 30: monitorexit 31: aload_3 32: athrow 33: return
關於monitorenter
與monitorexit
說明以下:架構
monitorenter
:每個對象與一個monitor
相對應,一個線程嘗試獲取與對象關聯的monitor
的時候,若是monitor
的計數器爲0,會得到以後當即對計數器加1,若是一個已經擁有monitor
全部權的線程重入,將致使計數器再次累加,而若是其餘線程嘗試獲取時,會一直阻塞直到monitor
的計數器變爲0,才能再次嘗試獲取對monitor
的全部權monitorexit
:釋放對monitor
的全部權,將monitor
的計數器減1,若是計數器爲0,意味着該線程再也不擁有對monitor
的全部權與monitor
關聯的對象不能爲空:併發
private Object MUTEX = null; private void sync(){ synchronized (MUTEX){ } }
會直接拋出空指針異常。app
因爲synchronized
關鍵字存在排它性,做用域越大,每每意味着效率越低,甚至喪失併發優點,好比:
private synchronized void sync(){ method1(); syncMethod(); method2(); }
其中只有第二個方法是併發操做,那麼能夠修改成
private Object MUTEX = new Object(); private void sync(){ method1(); synchronized (MUTEX){ syncMethod(); } method2(); }
由於一個對象與一個monitor
相關聯,若是使用不一樣的對象,這樣就失去了同步的意義,例子以下:
public class Main { public static class Task implements Runnable{ private final Object MUTEX = new Object(); @Override public void run(){ synchronized (MUTEX){ } } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 20; i++) { new Thread(new Task()).start(); } } }
每個線程爭奪的monitor
都是互相獨立的,這樣就失去了同步的意義,起不到互斥的做用。
另外,使用synchronized
還須要注意的是有可能形成死鎖的問題,先來看一下形成死鎖可能的緣由。
jstack
等工具看不到死鎖,可是程序不工做,CPU
佔有率高,這種死鎖也叫系統假死,難以排查和重現public class Main { private final Object MUTEX_READ = new Object(); private final Object MUTEX_WRITE = new Object(); public void read(){ synchronized (MUTEX_READ){ synchronized (MUTEX_WRITE){ } } } public void write(){ synchronized (MUTEX_WRITE){ synchronized (MUTEX_READ){ } } } public static void main(String[] args) throws InterruptedException { Main m = new Main(); new Thread(()->{ while (true){ m.read(); } }).start(); new Thread(()->{ while (true){ m.write(); } }).start(); } }
兩個線程分別佔有MUTEX_READ
/MUTEX_WRITE
,同時等待另外一個線程釋放MUTEX_WRITE
/MUTEX_READ
,這就是交叉鎖形成的死鎖。
使用jps
找到進程後,經過jstack
查看:
能夠看到明確的提示找到了1個死鎖,Thread-0
等待被Thread-1
佔有的monitor
,而Thread-1
等待被Thread-0
佔有的monitor
。
monitor
這裏介紹兩個特殊的monitor
:
this monitor
class monitor
this monitor
先上一段代碼:
public class Main { public synchronized void method1(){ System.out.println(Thread.currentThread().getName()+" method1"); try{ TimeUnit.MINUTES.sleep(5); }catch (InterruptedException e){ e.printStackTrace(); } } public synchronized void method2(){ System.out.println(Thread.currentThread().getName()+" method2"); try{ TimeUnit.MINUTES.sleep(5); }catch (InterruptedException e){ e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Main m = new Main(); new Thread(m::method1).start(); new Thread(m::method2).start(); } }
運行以後能夠發現,只有一行輸出,也就是說,只是運行了其中一個方法,另外一個方法根本沒有執行,使用jstack
能夠發現:
一個線程處於休眠中,而另外一個線程處於阻塞中。而若是將method2()
修改以下:
public void method2(){ synchronized (this) { System.out.println(Thread.currentThread().getName() + " method2"); try { TimeUnit.MINUTES.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }
效果是同樣的。也就是說,在方法上使用synchronized
,等價於synchronized(this)
。
class monitor
把上面的代碼中的方法修改成靜態方法:
public class Main { public static synchronized void method1() { System.out.println(Thread.currentThread().getName() + " method1"); try { TimeUnit.MINUTES.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } public static synchronized void method2() { System.out.println(Thread.currentThread().getName() + " method2"); try { TimeUnit.MINUTES.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { new Thread(Main::method1).start(); new Thread(Main::method2).start(); } }
運行以後能夠發現輸出仍是隻有一行,也就是說只運行了其中一個方法,jstack
分析也相似:
而若是將method2()
修改以下:
public static void method2() { synchronized (Main.class) { System.out.println(Thread.currentThread().getName() + " method2"); try { TimeUnit.MINUTES.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } }
能夠發現輸出仍是一致,也就是說,在靜態方法上的synchronized
,等價於synchronized(XXX.class)
。
this monitor
:在成員方法上的synchronized
,就是this monitor
,等價於在方法中使用synchronized(this)
class monitor
:在靜態方法上的synchronized
,就是class monitor
,等價於在靜態方法中使用synchronized(XXX.class)
ThreadGroup
不管什麼狀況下,一個新建立的線程都會加入某個ThreadGroup
中:
ThreadGroup
,默認就是main
線程所在的ThreadGroup
ThreadGroup
,那麼就加入該ThreadGroup
中ThreadGroup
中存在父子關係,一個ThreadGroup
能夠存在子ThreadGroup
。
建立ThreadGroup
能夠直接經過構造方法建立,構造方法有兩個,一個是直接指定名字(ThreadGroup
爲main
線程的ThreadGroup
),一個是帶有父ThreadGroup
與名字的構造方法:
ThreadGroup group1 = new ThreadGroup("name"); ThreadGroup group2 = new ThreadGroup(group1,"name2");
完整例子:
public static void main(String[] args) throws InterruptedException { ThreadGroup group1 = new ThreadGroup("name"); ThreadGroup group2 = new ThreadGroup(group1,"name2"); System.out.println(group2.getParent() == group1); System.out.println(group1.getParent().getName()); }
輸出結果:
true main
enumerate()
enumerate()
可用於Thread
和ThreadGroup
的複製,由於一個ThreadGroup
能夠加入若干個Thread
以及若干個子ThreadGroup
,使用該方法能夠方便地進行復制。方法描述以下:
public int enumerate(Thread [] list)
public int enumerate(Thread [] list, boolean recurse)
public int enumerate(ThreadGroup [] list)
public int enumerate(ThreadGroup [] list, boolean recurse)
上述方法會將ThreadGroup
中的活躍線程/ThreadGroup
複製到Thread
/ThreadGroup
數組中,布爾參數表示是否開啓遞歸複製。
例子以下:
public static void main(String[] args) throws InterruptedException { ThreadGroup myGroup = new ThreadGroup("MyGroup"); Thread thread = new Thread(myGroup,()->{ while (true){ try{ TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } } },"MyThread"); thread.start(); TimeUnit.MILLISECONDS.sleep(1); ThreadGroup mainGroup = currentThread().getThreadGroup(); Thread[] list = new Thread[mainGroup.activeCount()]; int recurseSize = mainGroup.enumerate(list); System.out.println(recurseSize); recurseSize = mainGroup.enumerate(list,false); System.out.println(recurseSize); }
後一個輸出比前一個少1,由於不包含myGroup
中的線程(遞歸設置爲false
)。須要注意的是,enumerate()
獲取的線程僅僅是一個預估值,並不能百分百地保證當前group
的活躍線程,好比調用複製以後,某個線程結束了生命週期或者新的線程加入進來,都會致使數據不許確。另外,返回的int
值相較起Thread[]
的長度更爲真實,由於enumerate
僅僅將當前活躍的線程分別放進數組中,而返回值int
表明的是真實的數量而不是數組的長度。
API
activeCount()
:獲取group
中活躍的線程,估計值activeGroupCount()
:獲取group
中活躍的子group
,也是一個近似值,會遞歸獲取全部的子group
getMaxPriority()
:用於獲取group
的優先級,默認狀況下,group
的優先級爲10,且全部線程的優先級不得大於線程所在group
的優先級getName()
:獲取group
名字getParent()
:獲取父group
,若是不存在返回null
list()
:一個輸出方法,遞歸輸出全部活躍線程信息到控制檯parentOf(ThreadGroup g)
:判斷當前group
是否是給定group
的父group
,若是給定的group
是本身自己,也會返回true
setMaxPriority(int pri)
:指定group
的最大優先級,設定後也會改變全部子group
的最大優先級,另外,修改優先級後會出現線程優先級大於group
優先級的狀況,好比線程優先級爲10,設置group
優先級爲5後,線程優先級就大於group
優先級,可是新加入的線程優先級必須不能大於group
優先級interrupt()
:致使全部的活躍線程被中斷,遞歸調用線程的interrupt()
destroy()
:若是沒有任何活躍線程,調用後在父group
中將本身移除setDaemon(boolean daemon)
:設置爲守護ThreadGroup
後,若是該ThreadGroup
沒有任何活躍線程,自動被銷燬