本文是兩章的筆記整理。
本文主要講述了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(){
}
一個簡單的例子如下:
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){
}
}
會直接丟擲空指標異常。
由於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
沒有任何活躍執行緒,自動被銷燬