您好,登錄后才能下訂單哦!
這篇文章主要介紹“什么是線程安全與ThreadGroup”,在日常操作中,相信很多人在什么是線程安全與ThreadGroup問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”什么是線程安全與ThreadGroup”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
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
還需要注意的是有可能造成死鎖的問題,先來看一下造成死鎖可能的原因。
交叉鎖導致程序死鎖:比如線程A持有R1的鎖等待R2的鎖,線程B持有R2的鎖等待R1的鎖
內存不足:比如兩個線程T1和T2,T1已獲取10MB內存,T2獲取了15MB內存,T1和T2都需要獲取30MB內存才能工作,但是剩余可用的內存為10MB,這樣兩個線程都在等待彼此釋放內存資源
一問一答式的數據交換:服務器開啟某個端口,等待客戶端訪問,客戶端發送請求后,服務器因某些原因錯過了客戶端請求,導致客戶端等待服務器回應,而服務器等待客戶端發送請求
死循環引起的死鎖:比較常見,使用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
沒有任何活躍線程,自動被銷毀
到此,關于“什么是線程安全與ThreadGroup”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。