溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

什么是線程安全與ThreadGroup

發(fā)布時(shí)間:2021-10-11 17:23:37 來(lái)源:億速云 閱讀:138 作者:iii 欄目:編程語(yǔ)言

這篇文章主要介紹“什么是線程安全與ThreadGroup”,在日常操作中,相信很多人在什么是線程安全與ThreadGroup問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”什么是線程安全與ThreadGroup”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

簡(jiǎn)介

synchronized可以防止線程干擾和內(nèi)存一致性錯(cuò)誤,具體表現(xiàn)如下:

  • synchronized提供了一種鎖機(jī)制,能夠確保共享變量的互斥訪問(wèn),從而防止數(shù)據(jù)不一致的問(wèn)題

  • synchronized包括monitor entermonitor exit兩個(gè)JVM指令,能保證在任何時(shí)候任何線程執(zhí)行到monitor enter成功之前都必須從主存獲取數(shù)據(jù),而不是從緩存中,在monitor exit運(yùn)行成功之后,共享變量被更新后的值必須刷入主內(nèi)存而不是僅僅在緩存中

  • synchronized指令嚴(yán)格遵循Happens-Beofre規(guī)則,一個(gè)monitor exit指令之前必定要有一個(gè)monitor enter

基本用法

synchronized的基本用法可以用于對(duì)代碼塊或方法進(jìn)行修飾,比如:

private final Object MUTEX = new Object();
    
public void sync1(){
    synchronized (MUTEX){
    }
}

public synchronized void sync2(){
}

字節(jié)碼簡(jiǎn)單分析

一個(gè)簡(jiǎn)單的例子如下:

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();
            }
        }
    }
}

編譯后查看字節(jié)碼:

javap -v -c -s -l Main.class

access()字節(jié)碼截取如下:

stack=3, locals=4, args_size=1
 0: getstatic     #9                  // Field MUTEX:Ljava/lang/Object;  獲取MUTEX
 3: dup
 4: astore_1
 5: monitorenter					  // 執(zhí)行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				  // 正常退出,跳轉(zhuǎn)到字節(jié)碼偏移量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

關(guān)于monitorentermonitorexit說(shuō)明如下:

  • monitorenter:每一個(gè)對(duì)象與一個(gè)monitor相對(duì)應(yīng),一個(gè)線程嘗試獲取與對(duì)象關(guān)聯(lián)的monitor的時(shí)候,如果monitor的計(jì)數(shù)器為0,會(huì)獲得之后立即對(duì)計(jì)數(shù)器加1,如果一個(gè)已經(jīng)擁有monitor所有權(quán)的線程重入,將導(dǎo)致計(jì)數(shù)器再次累加,而如果其他線程嘗試獲取時(shí),會(huì)一直阻塞直到monitor的計(jì)數(shù)器變?yōu)?,才能再次嘗試獲取對(duì)monitor的所有權(quán)

  • monitorexit:釋放對(duì)monitor的所有權(quán),將monitor的計(jì)數(shù)器減1,如果計(jì)數(shù)器為0,意味著該線程不再擁有對(duì)monitor的所有權(quán)

注意事項(xiàng)

非空對(duì)象

monitor關(guān)聯(lián)的對(duì)象不能為空:

private Object MUTEX = null;
private void sync(){
    synchronized (MUTEX){

    }
}

會(huì)直接拋出空指針異常。

作用域不當(dāng)

由于synchronized關(guān)鍵字存在排它性,作用域越大,往往意味著效率越低,甚至喪失并發(fā)優(yōu)勢(shì),比如:

private synchronized void sync(){
    method1();
    syncMethod();
    method2();
}

其中只有第二個(gè)方法是并發(fā)操作,那么可以修改為

private Object MUTEX = new Object();
private void sync(){
    method1();
    synchronized (MUTEX){
        syncMethod();
    }
    method2();
}

使用不同的對(duì)象

因?yàn)橐粋€(gè)對(duì)象與一個(gè)monitor相關(guān)聯(lián),如果使用不同的對(duì)象,這樣就失去了同步的意義,例子如下:

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();
        }
    }
}

每一個(gè)線程爭(zhēng)奪的monitor都是互相獨(dú)立的,這樣就失去了同步的意義,起不到互斥的作用。

死鎖

另外,使用synchronized還需要注意的是有可能造成死鎖的問(wèn)題,先來(lái)看一下造成死鎖可能的原因。

死鎖成因

  • 交叉鎖導(dǎo)致程序死鎖:比如線程A持有R1的鎖等待R2的鎖,線程B持有R2的鎖等待R1的鎖

  • 內(nèi)存不足:比如兩個(gè)線程T1和T2,T1已獲取10MB內(nèi)存,T2獲取了15MB內(nèi)存,T1和T2都需要獲取30MB內(nèi)存才能工作,但是剩余可用的內(nèi)存為10MB,這樣兩個(gè)線程都在等待彼此釋放內(nèi)存資源

  • 一問(wèn)一答式的數(shù)據(jù)交換:服務(wù)器開(kāi)啟某個(gè)端口,等待客戶端訪問(wèn),客戶端發(fā)送請(qǐng)求后,服務(wù)器因某些原因錯(cuò)過(guò)了客戶端請(qǐng)求,導(dǎo)致客戶端等待服務(wù)器回應(yīng),而服務(wù)器等待客戶端發(fā)送請(qǐng)求

  • 死循環(huán)引起的死鎖:比較常見(jiàn),使用jstack等工具看不到死鎖,但是程序不工作,CPU占有率高,這種死鎖也叫系統(tǒng)假死,難以排查和重現(xiàn)

例子

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();
    }
}

兩個(gè)線程分別占有MUTEX_READ/MUTEX_WRITE,同時(shí)等待另一個(gè)線程釋放MUTEX_WRITE/MUTEX_READ,這就是交叉鎖造成的死鎖。

排查

使用jps找到進(jìn)程后,通過(guò)jstack查看:

什么是線程安全與ThreadGroup

可以看到明確的提示找到了1個(gè)死鎖,Thread-0等待被Thread-1占有的monitor,而Thread-1等待被Thread-0占有的monitor

兩個(gè)特殊的monitor

這里介紹兩個(gè)特殊的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();
    }
}

運(yùn)行之后可以發(fā)現(xiàn),只有一行輸出,也就是說(shuō),只是運(yùn)行了其中一個(gè)方法,另一個(gè)方法根本沒(méi)有執(zhí)行,使用jstack可以發(fā)現(xiàn):

什么是線程安全與ThreadGroup

一個(gè)線程處于休眠中,而另一個(gè)線程處于阻塞中。而如果將method2()修改如下:

public void method2(){
    synchronized (this) {
        System.out.println(Thread.currentThread().getName() + " method2");
        try {
            TimeUnit.MINUTES.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

效果是一樣的。也就是說(shuō),在方法上使用synchronized,等價(jià)于synchronized(this)。

class monitor

把上面的代碼中的方法修改為靜態(tài)方法:

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();
    }
}

運(yùn)行之后可以發(fā)現(xiàn)輸出還是只有一行,也就是說(shuō)只運(yùn)行了其中一個(gè)方法,jstack分析也類似:

什么是線程安全與ThreadGroup

而如果將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();
        }
    }
}

可以發(fā)現(xiàn)輸出還是一致,也就是說(shuō),在靜態(tài)方法上的synchronized,等價(jià)于synchronized(XXX.class)。

總結(jié)

  • this monitor:在成員方法上的synchronized,就是this monitor,等價(jià)于在方法中使用synchronized(this)

  • class monitor:在靜態(tài)方法上的synchronized,就是class monitor,等價(jià)于在靜態(tài)方法中使用synchronized(XXX.class)

ThreadGroup

簡(jiǎn)介

無(wú)論什么情況下,一個(gè)新創(chuàng)建的線程都會(huì)加入某個(gè)ThreadGroup中:

  • 如果新建線程沒(méi)有指定ThreadGroup,默認(rèn)就是main線程所在的ThreadGroup

  • 如果指定了ThreadGroup,那么就加入該ThreadGroup

ThreadGroup中存在父子關(guān)系,一個(gè)ThreadGroup可以存在子ThreadGroup。

創(chuàng)建

創(chuàng)建ThreadGroup可以直接通過(guò)構(gòu)造方法創(chuàng)建,構(gòu)造方法有兩個(gè),一個(gè)是直接指定名字(ThreadGroupmain線程的ThreadGroup),一個(gè)是帶有父ThreadGroup與名字的構(gòu)造方法:

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());
}

輸出結(jié)果:

true
main

enumerate()

enumerate()可用于ThreadThreadGroup的復(fù)制,因?yàn)橐粋€(gè)ThreadGroup可以加入若干個(gè)Thread以及若干個(gè)子ThreadGroup,使用該方法可以方便地進(jìn)行復(fù)制。方法描述如下:

  • public int enumerate(Thread [] list)

  • public int enumerate(Thread [] list, boolean recurse)

  • public int enumerate(ThreadGroup [] list)

  • public int enumerate(ThreadGroup [] list, boolean recurse)

上述方法會(huì)將ThreadGroup中的活躍線程/ThreadGroup復(fù)制到Thread/ThreadGroup數(shù)組中,布爾參數(shù)表示是否開(kāi)啟遞歸復(fù)制。

例子如下:

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);
}

后一個(gè)輸出比前一個(gè)少1,因?yàn)椴话?code>myGroup中的線程(遞歸設(shè)置為false)。需要注意的是,enumerate()獲取的線程僅僅是一個(gè)預(yù)估值,并不能百分百地保證當(dāng)前group的活躍線程,比如調(diào)用復(fù)制之后,某個(gè)線程結(jié)束了生命周期或者新的線程加入進(jìn)來(lái),都會(huì)導(dǎo)致數(shù)據(jù)不準(zhǔn)確。另外,返回的int值相較起Thread[]的長(zhǎng)度更為真實(shí),因?yàn)?code>enumerate僅僅將當(dāng)前活躍的線程分別放進(jìn)數(shù)組中,而返回值int代表的是真實(shí)的數(shù)量而不是數(shù)組的長(zhǎng)度。

其他API

  • activeCount():獲取group中活躍的線程,估計(jì)值

  • activeGroupCount():獲取group中活躍的子group,也是一個(gè)近似值,會(huì)遞歸獲取所有的子group

  • getMaxPriority():用于獲取group的優(yōu)先級(jí),默認(rèn)情況下,group的優(yōu)先級(jí)為10,且所有線程的優(yōu)先級(jí)不得大于線程所在group的優(yōu)先級(jí)

  • getName():獲取group名字

  • getParent():獲取父group,如果不存在返回null

  • list():一個(gè)輸出方法,遞歸輸出所有活躍線程信息到控制臺(tái)

  • parentOf(ThreadGroup g):判斷當(dāng)前group是不是給定group的父group,如果給定的group是自己本身,也會(huì)返回true

  • setMaxPriority(int pri):指定group的最大優(yōu)先級(jí),設(shè)定后也會(huì)改變所有子group的最大優(yōu)先級(jí),另外,修改優(yōu)先級(jí)后會(huì)出現(xiàn)線程優(yōu)先級(jí)大于group優(yōu)先級(jí)的情況,比如線程優(yōu)先級(jí)為10,設(shè)置group優(yōu)先級(jí)為5后,線程優(yōu)先級(jí)就大于group優(yōu)先級(jí),但是新加入的線程優(yōu)先級(jí)必須不能大于group優(yōu)先級(jí)

  • interrupt():導(dǎo)致所有的活躍線程被中斷,遞歸調(diào)用線程的interrupt()

  • destroy():如果沒(méi)有任何活躍線程,調(diào)用后在父group中將自己移除

  • setDaemon(boolean daemon):設(shè)置為守護(hù)ThreadGroup后,如果該ThreadGroup沒(méi)有任何活躍線程,自動(dòng)被銷毀

到此,關(guān)于“什么是線程安全與ThreadGroup”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI