1 多線程的優(yōu)缺點(diǎn)

之前寫(xiě)的都亂糟糟的,現(xiàn)在也需要重新記憶一遍。所以重新整理一下JUC包。

多線程及其優(yōu)缺點(diǎn)

什么是線程

是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。(wiki百科)

創(chuàng)建線程的三種方式

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1、繼承Thread方式
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                System.out.println("thread1 start");
            }
        };
        thread1.start();

        //2、實(shí)現(xiàn)Runnable接口
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2 start");
            }
        });
        thread2.start();

        //3、實(shí)現(xiàn)Callable接口
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "future start";
            }
        });
        try {
            String result = future.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

在jdk8之后用lambda表達(dá)式轉(zhuǎn)換一下

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //1、繼承Thread方式
        Thread thread1 = new Thread(() -> System.out.println("thread1 start"));
        thread1.start();

        //2、實(shí)現(xiàn)Runnable接口
        Thread thread2 = new Thread(() -> System.out.println("thread2 start"));
        thread2.start();

        //3、實(shí)現(xiàn)Callable接口
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(() -> "future start");
        try {
            String result = future.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

簡(jiǎn)化了一點(diǎn),但是更多是有點(diǎn)懵,lambda為什么會(huì)簡(jiǎn)化方法,->是怎么找到對(duì)應(yīng)的方法,下次在研究。

為什么要用多線程

早期的CPU是單核的,為了提升計(jì)算能力,將多個(gè)計(jì)算單元整合到一起。形成了多核CPU。多線程就是為了將多核CPU發(fā)揮到極致,一邊提高性能。

多線程缺點(diǎn)呢

上面說(shuō)了多線程的有點(diǎn)是:為了提高計(jì)算性能。那么一定會(huì)提高?
答案是不一定的。有時(shí)候多線程不一定比單線程計(jì)算快。引入《java并發(fā)編程的藝術(shù)》上第一個(gè)例子

public class ConcurrencyTest {

    /** 執(zhí)行次數(shù) */
    private static final long count = 10000l;

    public static void main(String[] args) throws InterruptedException {
        //并發(fā)計(jì)算
        concurrency();
        //單線程計(jì)算
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a += 5;
                }
                System.out.println(a);
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        thread.join();
        long time = System.currentTimeMillis() - start;
        System.out.println("concurrency :" + time + "ms,b=" + b);
    }

    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
    }

}

結(jié)果為

50000
concurrency :22ms,b=-10000
serial:0ms,b=-10000,a=50000

而且多線程會(huì)帶來(lái)額外的開(kāi)銷

  • 上下文切換
  • 線程安全

上下文切換

時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間非常短,所以CPU不斷通過(guò)切換線程,讓我們覺(jué)得多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒。而每次切換時(shí),需要保存當(dāng)前的狀態(tài)起來(lái),以便能夠進(jìn)行恢復(fù)先前狀態(tài),而這個(gè)切換時(shí)非常損耗性能,過(guò)于頻繁反而無(wú)法發(fā)揮出多線程編程的優(yōu)勢(shì)。
減少上下文切換可以采用無(wú)鎖并發(fā)編程,CAS算法,使用最少的線程和使用協(xié)程。

  • 無(wú)鎖并發(fā)編程:可以參照concurrentHashMap鎖分段的思想,不同的線程處理不同段的數(shù)據(jù),這樣在多線程競(jìng)爭(zhēng)的條件下,可以減少上下文切換的時(shí)間。
  • CAS算法,利用Atomic下使用CAS算法來(lái)更新數(shù)據(jù),使用了樂(lè)觀鎖,可以有效的減少一部分不必要的鎖競(jìng)爭(zhēng)帶來(lái)的上下文切換
  • 使用最少線程:避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多的線程,這樣會(huì)造成大量的線程都處于等待狀態(tài)
  • 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換

線程安全的問(wèn)題

多線程編程中最難以把握的就是臨界區(qū)線程安全問(wèn)題,稍微不注意就會(huì)出現(xiàn)死鎖的情況
同樣引入《java并發(fā)編程的藝術(shù)》的一個(gè)例子

public class DeadLockDemo {
    private static String resource_a = "A";
    private static String resource_b = "B";

    public static void main(String[] args) {
        deadLock();
    }

    public static void deadLock() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (resource_b) {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("get resource b");
                    synchronized (resource_a) {
                        System.out.println("get resource a");
                    }
                }
            }
        });
        threadA.start();
        threadB.start();

    }
}

然后通過(guò)jps查看,找個(gè)這個(gè)類的id
然后通過(guò)jstack id來(lái)查看

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000000016074808 (object 0x00000000e0b89280, a java.lang.String),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000000016075ca8 (object 0x00000000e0b892b0, a java.lang.String),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================


"Thread-1" #11 prio=5 os_prio=0 tid=0x00000000175ba800 nid=0x232c waiting for monitor entry [0x000000001889f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockDemo$2.run(DeadLockDemo.java:37)
        - waiting to lock <0x00000000e0b89280> (a java.lang.String)
        - locked <0x00000000e0b892b0> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #10 prio=5 os_prio=0 tid=0x00000000175b7800 nid=0x234c waiting for monitor entry [0x000000001861f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockDemo$1.run(DeadLockDemo.java:18)
        - waiting to lock <0x00000000e0b892b0> (a java.lang.String)
        - locked <0x00000000e0b89280> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

兩個(gè)線程相互等待,仔細(xì)看上面的waiting to lock 和locked兩個(gè)對(duì)象。是相互的。造成死鎖。
造成死鎖的原因和解決方案

死鎖:指兩個(gè)或兩個(gè)以上的進(jìn)程在執(zhí)行過(guò)程中,由于競(jìng)爭(zhēng)資源或者由于彼此通信而造成的一種阻塞的現(xiàn)象,若無(wú)外力作用,它們都將無(wú)法推進(jìn)下去。
造成死鎖的原因是:

  1. 因?yàn)橄到y(tǒng)資源不足。
  2. 進(jìn)程運(yùn)行推進(jìn)的順序不合適。
  3. 資源分配不當(dāng)?shù)取?/li>

如果系統(tǒng)資源充足,進(jìn)程的資源請(qǐng)求都能夠得到滿足,死鎖出現(xiàn)的可能性就很低,否則
就會(huì)因爭(zhēng)奪有限的資源而陷入死鎖。其次,進(jìn)程運(yùn)行推進(jìn)順序與速度不同,也可能產(chǎn)生死鎖。

那么死鎖的必要條件是:

  1. 互斥條件:一個(gè)資源每次只能被一個(gè)進(jìn)程使用。
  2. 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
  3. 不剝奪條件:進(jìn)程已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪。
  4. 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

這四個(gè)條件是 死鎖的必要條件 ,只要系統(tǒng)發(fā)生死鎖,這些條件必然成立,而只要上述條件之
一不滿足,就不會(huì)發(fā)生死鎖。

線程的狀態(tài)

線程有6種狀態(tài)

  1. NEW:新建,線程被構(gòu)建,但是還沒(méi)有start()
  2. RUNNABLE:運(yùn)行,java中將就緒和運(yùn)行統(tǒng)稱為運(yùn)行中
  3. BLOCKED:阻塞,線程阻塞于鎖
  4. WAITING:等待,表示線程進(jìn)入等待狀態(tài),需要其他線程的特定動(dòng)作(通知或中斷)
  5. TIMED_WAITING:帶超時(shí)的等待,可以在指定的時(shí)間內(nèi)自動(dòng)返還
  6. TERMINATED:終止,表示線程已經(jīng)執(zhí)行完畢

狀態(tài)轉(zhuǎn)換

線程創(chuàng)建之后調(diào)用start()方法開(kāi)始運(yùn)行。
當(dāng)調(diào)用wait(),join(),LockSupport.lock()方法線程會(huì)進(jìn)入到WAITING狀態(tài),而同樣的wait(long timeout),sleep(long), join(long), LockSupport.parkNanos(), LockSupport.parkUtil()增加了超時(shí)等待的功能,也就是調(diào)用這些方法后線程會(huì)進(jìn)入TIMED_WAITING狀態(tài),當(dāng)超時(shí)等待時(shí)間到達(dá)后,線程會(huì)切換到Runable的狀態(tài),另外當(dāng)WAITINGTIMED _WAITING狀態(tài)時(shí)可以通過(guò)Object.notify(),Object.notifyAll()方法使線程轉(zhuǎn)換到Runable狀態(tài)。當(dāng)線程出現(xiàn)資源競(jìng)爭(zhēng)時(shí),即等待獲取鎖的時(shí)候,線程會(huì)進(jìn)入到BLOCKED阻塞狀態(tài),當(dāng)線程獲取鎖時(shí),線程進(jìn)入到Runable狀態(tài)。線程運(yùn)行結(jié)束后,線程進(jìn)入到TERMINATED狀態(tài),狀態(tài)轉(zhuǎn)換可以說(shuō)是線程的生命周期。
注意
當(dāng)線程進(jìn)入到synchronized方法或者synchronized代碼塊時(shí),線程切換到的是BLOCKED狀態(tài).
而使用java.util.concurrent.lockslock進(jìn)行加鎖的時(shí)候線程切換的是WAITING或者TIMED_WAITING狀態(tài),因?yàn)閘ock會(huì)調(diào)用LockSupport的方法。

線程狀態(tài)的操作

interrupted()

中斷可以理解為線程的一個(gè)標(biāo)志位,它表示了一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作。中斷好比其他線程對(duì)該線程打了一個(gè)招呼。
其他線程可以調(diào)用該線程的interrupt()方法對(duì)其進(jìn)行中斷操作,同時(shí)該線程可以調(diào)用 isInterrupted()來(lái)感知其他線程對(duì)其自身的中斷操作,從而做出響應(yīng)。
另外,同樣可以調(diào)用Thread的靜態(tài)方法 interrupted()對(duì)當(dāng)前線程進(jìn)行中斷操作,該方法會(huì)清除中斷標(biāo)志位。

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        //sleepThread睡眠1000ms
        final Thread sleepThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                super.run();
            }
        };
        //busyThread一直執(zhí)行死循環(huán)
        Thread busyThread = new Thread() {
            @Override
            public void run() {
                while (true) ;
            }
        };
        sleepThread.start();
        busyThread.start();
        sleepThread.interrupt();
        busyThread.interrupt();
        while (sleepThread.isInterrupted()) ;
        System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
        System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
    }
}

運(yùn)行結(jié)果是:


對(duì)著兩個(gè)線程進(jìn)行中斷操作,可以看出sleepThread拋出InterruptedException后清除標(biāo)志位,而busyThread就不會(huì)清除標(biāo)志位。

join()

join方法可以看做是線程間協(xié)作的一種方式。
如果一個(gè)線程實(shí)例A執(zhí)行了threadB.join(),其含義是:當(dāng)前線程A會(huì)等待threadB線程終止后threadA才會(huì)繼續(xù)執(zhí)行。

public class JoinDemo {
    public static void main(String[] args) {
        Thread previousThread = Thread.currentThread();
        for (int i = 1; i <= 5; i++) {
            Thread curThread = new JoinThread(previousThread);
            curThread.start();
            previousThread = curThread;
        }
    }

    static class JoinThread extends Thread {
        private Thread thread;

        public JoinThread(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
               //join
               thread.join();
                System.out.println(thread.getName() + " terminated.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
運(yùn)行結(jié)果

如果注釋了上面的thread.join();

運(yùn)行結(jié)果

每個(gè)線程都會(huì)等待前一個(gè)線程結(jié)束才會(huì)繼續(xù)運(yùn)行。

sleep() VS wait()

兩者主要的區(qū)別:

  1. sleep()方法是Thread的靜態(tài)方法,而wait是Object實(shí)例方法
  2. wait()方法必須要在同步方法或者同步塊中調(diào)用,也就是必須已經(jīng)獲得對(duì)象鎖。而sleep()方法沒(méi)有這個(gè)限制可以在任何地方種使用。另外,wait()方法會(huì)釋放占有的對(duì)象鎖,使得該線程進(jìn)入等待池中,等待下一次獲取資源。而sleep()方法只是會(huì)讓出CPU并不會(huì)釋放掉對(duì)象鎖;
  3. sleep()方法在休眠時(shí)間達(dá)到后如果再次獲得CPU時(shí)間片就會(huì)繼續(xù)執(zhí)行,而wait()方法必須等待Object.notift/Object.notifyAll通知后,才會(huì)離開(kāi)等待池,并且再次獲得CPU時(shí)間片才會(huì)繼續(xù)執(zhí)行。

守護(hù)線程Daemon

守護(hù)線程是一種特殊的線程,就和它的名字一樣,它是系統(tǒng)的守護(hù)者,在后臺(tái)默默地守護(hù)一些系統(tǒng)服務(wù),比如垃圾回收線程,JIT線程就可以理解守護(hù)線程。與之對(duì)應(yīng)的就是用戶線程,用戶線程就可以認(rèn)為是系統(tǒng)的工作線程,它會(huì)完成整個(gè)系統(tǒng)的業(yè)務(wù)操作。用戶線程完全結(jié)束后就意味著整個(gè)系統(tǒng)的業(yè)務(wù)任務(wù)全部結(jié)束了,因此系統(tǒng)就沒(méi)有對(duì)象需要守護(hù)的了,守護(hù)線程自然而然就會(huì)退。當(dāng)一個(gè)Java應(yīng)用,只有守護(hù)線程的時(shí)候,虛擬機(jī)就會(huì)自然退出。下面以一個(gè)簡(jiǎn)單的例子來(lái)表述Daemon線程的使用。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        //設(shè)置為守護(hù)線程
        daemonThread.setDaemon(true);
        daemonThread.start();
        //確保main線程結(jié)束前能給daemonThread能夠分到時(shí)間片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運(yùn)行結(jié)果:

結(jié)果

守護(hù)線程應(yīng)該先于start()方法之前。如果在之后,但是該線程還是會(huì)執(zhí)行,只不過(guò)會(huì)當(dāng)做正常的用戶線程執(zhí)行。

其他的一些概念

同步和異步

同步和異步通常用來(lái)形容一次方法調(diào)用。
同步方法調(diào)用一開(kāi)始,調(diào)用者必須等待被調(diào)用的方法結(jié)束后,調(diào)用者后面的代碼才能執(zhí)行。
而異步調(diào)用,指的是,調(diào)用者不用管被調(diào)用方法是否完成,都會(huì)繼續(xù)執(zhí)行后面的代碼,當(dāng)被調(diào)用的方法完成后會(huì)通知調(diào)用者。

并發(fā)與并行

并發(fā)指的是多個(gè)任務(wù)交替進(jìn)行,而并行則是指真正意義上的“同時(shí)進(jìn)行”。
實(shí)際上,如果系統(tǒng)內(nèi)只有一個(gè)CPU,而使用多線程時(shí),那么真實(shí)系統(tǒng)環(huán)境下不能并行,只能通過(guò)切換時(shí)間片的方式交替進(jìn)行,而成為并發(fā)執(zhí)行任務(wù)。真正的并行也只能出現(xiàn)在擁有多個(gè)CPU的系統(tǒng)中。

阻塞和非阻塞

阻塞和非阻塞通常用來(lái)形容多線程間的相互影響。
比如一個(gè)線程占有了臨界區(qū)資源,那么其他線程需要這個(gè)資源就必須進(jìn)行等待該資源的釋放,會(huì)導(dǎo)致等待的線程掛起,這種情況就是阻塞。
而非阻塞就恰好相反,它強(qiáng)調(diào)沒(méi)有一個(gè)線程可以阻塞其他線程,所有的線程都會(huì)嘗試地往前運(yùn)行。

臨界區(qū)

臨界區(qū)用來(lái)表示一種公共資源或者說(shuō)是共享數(shù)據(jù),可以被多個(gè)線程使用。但是每個(gè)線程使用時(shí),一旦臨界區(qū)資源被一個(gè)線程占有,那么其他線程必須等待。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容