多線程核心知識


線程生命周期(線程狀態)

Java中的線程的生命周期大體可分為5種狀態。
  新建:創建完線程、還沒調用start方法。
  就緒:已經調用start方法,等待CPU分配時間片。
  運行:run方法正在運行中。
  阻塞:wait、sleep、yield、join 使線程阻塞住。
  死亡:run方法運行完畢。

多線程通信

jion

我們現在有兩個線程的話,在第二個線程里面調用第一個線程的join方法,程序會立即切換到第一個線程去執行。點進去thread.join()源碼發現,join主要是下面幾行代碼實現的。
    public final void join() throws InterruptedException {
        // 最多等待millis毫秒,此線程才能死;millis=0意味着永遠等待。
        join(0);
    }
    
    // 可以看到調用join(long millis)方法,實際還是通過synchronized實現的。
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {//如果線程還沒執行完
                wait(0);//釋放對象鎖,程序停止執行。
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
可以看到join的底層還是對方法加鎖,然后通過wait()使自己阻塞,從而切換到其他線程執行;join()方法的java文檔中寫到:a thread terminates the this.notifyAll method is invoked.也就是說,其他線程run()方法運行完了以后,會調用this.notifyAll,釋放鎖。這樣線程獲得鎖以后就可以繼續往下執行了。

yield

yield()讓出線程時間片,增大線程切換的幾率。由於它是native修飾的,所以是通過其他語言直接操作機器實現的。

public static native void yield();

sleep

sleep(long millis)會讓線程阻塞住millis秒,在這個時間段內不再參與到CPU競爭。sleep()是通過millis參數來設置一個定時器實現的,時間一結束,若沒有其他線程正在執行,則會同其他線程一起搶占cpu資源。

public static native void sleep(long millis) throws InterruptedException;

wait

wait()會讓線程休眠,釋放鎖。
(1)為什么 wait/notify/notifyAll一定要放在同步代碼塊里,我們可以看下面這個案例。

boolean flag = false;

// A線程代碼
while(!flag){
    wait();
}

// B線程代碼
if(!flag){
    condition = true;
    // nofity隨機喚醒一個等待的線程;notifyAll喚醒等待該鎖的所有線程
    notify();
}

1. 如果線程A剛執行完while(!flag),准備執行wait(),此時線程A的時間片已經耗盡
2. B線程執行完 condition = true; notify();的操作,由於A並沒有wait,所以B的notify不會起任何效果
3. 此時A又獲得了時間片,繼續執行wait(),此時由於沒有notify()喚醒她,那么她會一直沉睡下去。
所以相當於:鎖對象里面維護了一個隊列,線程A執行lock的wait方法,把線程A保存到list中,線程B中執行lock的notify方法,從等待隊列中取出線程A繼續執行。

(2)為什么 wait要放在while中判斷而不是if中
如果采用if判斷,當線程從wait中喚醒時,判斷條件已經不滿足處理業務邏輯的條件了,從而出現錯誤的結果。所以我們業務需要像下面一樣在判斷一次。而循環則是對上述寫法的簡化
synchronized (monitor) { // 判斷條件謂詞是否得到滿足 if(!locked) { // 等待喚醒 monitor.wait(); if(locked) { // 處理其他的業務邏輯 } else { // 跳轉到monitor.wait(); } } }

為什么要使用線程池

諸如 Web 服務器、數據庫服務器、文件服務器或郵件服務器之類的許多服務器應用程序都面向處理來自某些遠程來源的大量短小的任務。線程池為線程生命周期開銷問題和資源不足問題提供了解決方案。通過對多個任務重用線程,線程創建的開銷被分攤到了多個任務上。其好處是,因為在請求到達時線程已經存在,所以無意中也消除了線程創建所帶來的延遲。這樣,就可以立即為請求服務,使應用程序響應更快。而且,通過適當地調整線程池中的線程數目,也就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,直到獲得一個線程來處理為止,從而可以防止資源不足。風險與機遇:用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有並發風險,諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。
總結起來就三點:
第一:降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用。

線程池原理剖析

提交一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池里的核心線程是否都在執行任務,如果不是(核心線程空閑或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
3、判斷線程池里的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。

下面4種是比比較常用的線程池創建方式,第一個用的比較多。

public class ExecutorDemo {
    public static void main(String[] args) {
        // 創建一個定長線程池,可控制線程最大並發數,超出的線程會在隊列中等待。
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 15; i++) {
            final int temp = i;
            newFixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            /**
             * 控制台輸出
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-1
             * pool-1-thread-2
             * pool-1-thread-3
             * pool-1-thread-4
             * pool-1-thread-5
             * 
             * 總結:
             *     因為線程池大小為5,超出的請求需要排隊等待線程的分配。定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()
             */
        }
    }
}
newFixedThreadPool
public class ExecutorDemo {
    public static void main(String[] args) {
        // 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
        ExecutorService executorService3 = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            final int index = i;
            executorService3.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
}
newSingleThreadExecutor
public class ExecutorDemo {
    public static void main(String[] args) {
        // 創建一個定長線程池,能延遲執行、定時執行任務,支持周期性任務執行。
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        System.out.println(LocalTime.now());
        for (int i = 0; i < 10; i++) {
            scheduledExecutorService.schedule(new Runnable() {
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" 現在時間:"+ LocalTime.now());
                }
            }, 3, TimeUnit.SECONDS);
        }
        /**
         * 控制台輸出
         * 19:06:29.623
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         * pool-1-thread-1 現在時間:19:06:32.626
         */
    }
}
newScheduledThreadPool
public class ExecutorDemo {
    public static void main(String[] args) {
        // 創建一個可緩存線程池,如果線程池無可用線程,則回收空閑線程、否則新建線程(線程太多可能會引起OOM)。
        ExecutorService executorService = Executors.newCachedThreadPool();
//        executorService.submit(() -> {
//            System.out.println(Thread.currentThread().getName());
//        });
        for (int i = 0; i < 15; i++) {
            final int temp = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        /**
         * 控制台輸出
         * pool-1-thread-1
         * pool-1-thread-2
         * pool-1-thread-2
         * pool-1-thread-3
         * pool-1-thread-1
         * pool-1-thread-7
         * pool-1-thread-6
         * pool-1-thread-5
         * pool-1-thread-4
         * pool-1-thread-8
         * pool-1-thread-9
         * pool-1-thread-10
         * pool-1-thread-11
         * pool-1-thread-12
         * pool-1-thread-13
         *
         * 總結:
         *     線程池為無限大,當執行第二個任務時第一個任務已經完成,會復用執行第一個任務的線程,而不用每次新建線程。
         *
         */
    }
}
newCachedThreadPool

threadPoolExecutor實現原理

/**
 * 自定義線程池
 */
public class ExecutorDemo {
    public static void main(String[] args) {
        /**
         * 上面4種線程池的創建其實都是對於ThreadPoolExecutor的封裝,點進去看發現就是:
         * new ThreadPoolExecutor(int corePoolSize,
         *                           int maximumPoolSize,
         *                        long keepAliveTime,
         *                        TimeUnit unit,
         *                        new LinkedBlockingQueue<Runnable>())
         */
        /**
         * @param corePoolSize 核心線程數(空閑也不會被回收)
         * @param maximumPoolSize 最大線程數
         * @param keepAliveTime 線程空閑時的超時時間
         * @param TimeUnit 超時時間單位
         * @param BlockingQueue<Runnable> 阻塞隊列,線程排隊時就裝在這個隊列里面
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,2,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(3));
        /**
         * 上面已經創建好線程池了,核心線程為1,也就是初始線程數為1;最大線程2;隊列最多容納3個線程等待。下面我們有6個任務要執行
         * (1) 第一個任務開始,直接就交給了核心線程去執行
         * (2) 后面的線程進來,由於沒有線程了就會放到隊列里面去等待。如果之前的線程空閑了就會繼續復用;否則放入隊列等待新的線程創建
         * (3) 假使線程都在使用中,因為最大線程數為2,那么最多同時運行2個線程,由於隊列也只能排隊3個線程,那么這個線程池最多就只能容納5個線程
         * (4) 如果線程池已經被占滿,此時還有第6個任務要進來,那么線程池就會溢出報錯。
         */
        for (int i = 1; i <= 6; i++) {
            final int temp = i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "任務" + temp);
                }
            });
        }
        /**
         * 控制台輸出
         * pool-1-thread-1任務1
         * pool-1-thread-1任務2
         * pool-1-thread-1任務3
         * pool-1-thread-1任務4
         * pool-1-thread-2任務5
         * java.util.concurrent.RejectedExecutionException: Task com.Thread.A_newThread.pool.ExecutorDemo$1@6d6f6e28 rejected from java.util.concurrent.ThreadPoolExecutor@135fbaa4[Running, pool size = 2, active threads = 2, queued tasks = 3, completed tasks = 0]
         *     at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
         *     at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
         *     at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
         *     at com.Thread.A_newThread.pool.ExecutorDemo.main(ExecutorDemo.java:35)
         */
    }
}

如何合理分配線程池

  一個線程池究竟設置多大才合適這個並沒有一個固定值,而是根據不同任務而定的。與任務的CPU密集度和任務的IO密集度有關。
CPU密集型
  指的就是任務需要大量的運算(即run方法中代碼業務很多),而沒有阻塞,cpu一直全速運行。cpu密集的任務只有在多核cpu上通過多線程加速,在單核cpu是不會有任何速度提升的。
IO密集型
  是指任務需要大量的io(即大量的阻塞),也是只有在多核cpu上通過多線程加速,在單核cpu是不會有任何速度提升。
所以想合理的配置線程池的大小,首先得分析任務的特性,可以從以下幾個角度分析:
  1. 任務的性質:CPU密集型任務、IO密集型任務、混合型任務。
  2. 任務的優先級:高、中、低。
  3. 任務的執行時間:長、中、短。
  4. 任務的依賴性:是否依賴其他系統資源,如數據庫連接等。

CPU密集型時:盡量使用較小的線程池,一般為CPU核心數+1。 因為CPU密集型任務使得CPU使用率很高,若開過多的線程數,只能增加上下文切換的次數,因此會帶來額外的開銷。
IO密集型時:可以使用稍大的線程池,一般為2*CPU核心數。IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候去處理別的任務,充分利用CPU時間。
一般經過監控程序,逐步調整線程池線程數,使其達到合理的數量。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM