面試-執行緒池的成長之路

猿天地發表於2019-03-01

背景

相信大家在面試過程中遇到面試官問執行緒的很多,執行緒過後就是執行緒池了。從易到難,都是這麼個過程,還有就是確實很多人在工作中接觸執行緒池比較少,最多的也就是建立一個然後往裡面提交執行緒,對於一些經驗很豐富的面試官來說,一下就可以問出很多執行緒池相關的問題,與其被問的暈頭轉向,還不如好好學習。此時不努力更待何時。

什麼是執行緒池?

執行緒池是一種多執行緒處理形式,處理過程中將任務提交到執行緒池,任務的執行交由執行緒池來管理。

如果每個請求都建立一個執行緒去處理,那麼伺服器的資源很快就會被耗盡,使用執行緒池可以減少建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。

如果用生活中的列子來說明,我們可以把執行緒池當做一個客服團隊,如果同時有1000個人打電話進行諮詢,按照正常的邏輯那就是需要1000個客服接聽電話,服務客戶。現實往往需要考慮到很多層面的東西,比如:資源夠不夠,招這麼多人需要費用比較多。正常的做法就是招100個人成立一個客服中心,當有電話進來後分配沒有接聽的客服進行服務,如果超出了100個人同時諮詢的話,提示客戶等待,稍後處理,等有客服空出來就可以繼續服務下一個客戶,這樣才能達到一個資源的合理利用,實現效益的最大化。

Java中的執行緒池種類

1. newSingleThreadExecutor

建立方式:

ExecutorService pool = Executors.newSingleThreadExecutor();
複製程式碼

一個單執行緒的執行緒池。這個執行緒池只有一個執行緒在工作,也就是相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼會有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

使用方式:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
   public static void main(String[] args) {
   	ExecutorService pool = Executors.newSingleThreadExecutor();
   	for (int i = 0; i < 10; i++) {
   		pool.execute(() -> {
   			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	開始發車啦....
複製程式碼

從輸出的結果我們可以看出,一直只有一個執行緒在執行。

2.newFixedThreadPool

建立方式:

ExecutorService pool = Executors.newFixedThreadPool(10);
複製程式碼

建立固定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒達到執行緒池的最大大小。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

使用方式:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(10);
		for (int i = 0; i < 10; i++) {
			pool.execute(() -> {
				System.out.println(Thread.currentThread().getName() + "	開始發車啦....");
			});
		}
	}
}
複製程式碼

輸出結果如下:

pool-1-thread-1	開始發車啦....
pool-1-thread-4	開始發車啦....
pool-1-thread-3	開始發車啦....
pool-1-thread-2	開始發車啦....
pool-1-thread-6	開始發車啦....
pool-1-thread-7	開始發車啦....
pool-1-thread-5	開始發車啦....
pool-1-thread-8	開始發車啦....
pool-1-thread-9	開始發車啦....
pool-1-thread-10 開始發車啦....
複製程式碼

3. newCachedThreadPool

建立方式:

ExecutorService pool = Executors.newCachedThreadPool();
複製程式碼

建立一個可快取的執行緒池。如果執行緒池的大小超過了處理任務所需要的執行緒,那麼就會回收部分空閒的執行緒,當任務數增加時,此執行緒池又新增新執行緒來處理任務。

使用方式如上2所示。

4.newScheduledThreadPool

建立方式:

ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
複製程式碼

此執行緒池支援定時以及週期性執行任務的需求。

使用方式:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ThreadPool {
	public static void main(String[] args) {
		ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
		for (int i = 0; i < 10; i++) {
			pool.schedule(() -> {
				System.out.println(Thread.currentThread().getName() + "	開始發車啦....");
			}, 10, TimeUnit.SECONDS);
		}
	}
}

複製程式碼

上面演示的是延遲10秒執行任務,如果想要執行週期性的任務可以用下面的方式,每秒執行一次

//pool.scheduleWithFixedDelay也可以
pool.scheduleAtFixedRate(() -> {
				System.out.println(Thread.currentThread().getName() + "	開始發車啦....");
}, 1, 1, TimeUnit.SECONDS);
複製程式碼

5.newWorkStealingPool
newWorkStealingPool是jdk1.8才有的,會根據所需的並行層次來動態建立和關閉執行緒,通過使用多個佇列減少競爭,底層用的ForkJoinPool來實現的。ForkJoinPool的優勢在於,可以充分利用多cpu,多核cpu的優勢,把一個任務拆分成多個“小任務”,把多個“小任務”放到多個處理器核心上並行執行;當多個“小任務”執行完成之後,再將這些執行結果合併起來即可。

說說執行緒池的拒絕策略

當請求任務不斷的過來,而系統此時又處理不過來的時候,我們需要採取的策略是拒絕服務。RejectedExecutionHandler介面提供了拒絕任務處理的自定義方法的機會。在ThreadPoolExecutor中已經包含四種處理策略。

  • AbortPolicy策略:該策略會直接丟擲異常,阻止系統正常工作。
public static class AbortPolicy implements RejectedExecutionHandler {
      
    public AbortPolicy() { }
   
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
    }
}
複製程式碼
  • CallerRunsPolicy 策略:只要執行緒池未關閉,該策略直接在呼叫者執行緒中,執行當前的被丟棄的任務。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
     
    public CallerRunsPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
                r.run();
        }
    }
}
複製程式碼
  • DiscardOleddestPolicy策略: 該策略將丟棄最老的一個請求,也就是即將被執行的任務,並嘗試再次提交當前任務。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
     
    public DiscardOldestPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}
複製程式碼
  • DiscardPolicy策略:該策略默默的丟棄無法處理的任務,不予任何處理。
public static class DiscardPolicy implements RejectedExecutionHandler {
      
    public DiscardPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}
複製程式碼

除了JDK預設為什麼提供的四種拒絕策略,我們可以根據自己的業務需求去自定義拒絕策略,自定義的方式很簡單,直接實現RejectedExecutionHandler介面即可

比如Spring integration中就有一個自定義的拒絕策略CallerBlocksPolicy,將任務插入到佇列中,直到佇列中有空閒並插入成功的時候,否則將根據最大等待時間一直阻塞,直到超時

package org.springframework.integration.util;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class CallerBlocksPolicy implements RejectedExecutionHandler {

	private static final Log logger = LogFactory.getLog(CallerBlocksPolicy.class);

	private final long maxWait;

	/**
	 * @param maxWait The maximum time to wait for a queue slot to be
	 * available, in milliseconds.
	 */
	public CallerBlocksPolicy(long maxWait) {
		this.maxWait = maxWait;
	}

	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
		if (!executor.isShutdown()) {
			try {
				BlockingQueue<Runnable> queue = executor.getQueue();
				if (logger.isDebugEnabled()) {
					logger.debug("Attempting to queue task execution for " + this.maxWait + " milliseconds");
				}
				if (!queue.offer(r, this.maxWait, TimeUnit.MILLISECONDS)) {
					throw new RejectedExecutionException("Max wait time expired to queue task");
				}
				if (logger.isDebugEnabled()) {
					logger.debug("Task execution queued");
				}
			}
			catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				throw new RejectedExecutionException("Interrupted", e);
			}
		}
		else {
			throw new RejectedExecutionException("Executor has been shut down");
		}
	}

}
複製程式碼

定義好之後如何使用呢?光定義沒用的呀,一定要用到執行緒池中呀,可以通過下面的方式自定義執行緒池,指定拒絕策略。

BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 100, 10, TimeUnit.SECONDS, workQueue, new CallerBlocksPolicy());
複製程式碼

execute和submit的區別?

在前面的講解中,我們執行任務是用的execute方法,除了execute方法,還有一個submit方法也可以執行我們提交的任務。

這兩個方法有什麼區別呢?分別適用於在什麼場景下呢?我們來做一個簡單的分析。

execute適用於不需要關注返回值的場景,只需要將執行緒丟到執行緒池中去執行就可以了

public class ThreadPool {
	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(10);
		pool.execute(() -> {
			System.out.println(Thread.currentThread().getName() + "	開始發車啦....");
		});
	}
}
複製程式碼

submit方法適用於需要關注返回值的場景,submit方法的定義如下:

public interface ExecutorService extends Executor {
&emsp;&emsp;...
&emsp;&emsp;<T> Future<T> submit(Callable<T> task);
&emsp;&emsp;<T> Future<T> submit(Runnable task, T result);
&emsp;&emsp;Future<?> submit(Runnable task);
&emsp;&emsp;...
}
複製程式碼

其子類AbstractExecutorService實現了submit方法,可以看到無論引數是Callable還是Runnable,最終都會被封裝成RunnableFuture,然後再呼叫execute執行。

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
複製程式碼

下面我們來看看這三個方法分別如何去使用:

submit(Callable task);

public class ThreadPool {
	public static void main(String[] args) throws Exception {
		ExecutorService pool = Executors.newFixedThreadPool(10);
		Future<String> future = pool.submit(new Callable<String>() {
			@Override
			public String call() throws Exception {
				return "Hello";
			}
		});
		String result = future.get();
		System.out.println(result);
	}
}
複製程式碼

submit(Runnable task, T result);

public class ThreadPool {
	public static void main(String[] args) throws Exception {
		ExecutorService pool = Executors.newFixedThreadPool(10);
		Data data = new Data();
		Future<Data> future = pool.submit(new MyRunnable(data), data);
		String result = future.get().getName();
		System.out.println(result);
	}
}

class Data {
    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class MyRunnable implements Runnable {
	private Data data;

	public MyRunnable(Data data) {
		this.data = data;
	}
	
	@Override
	public void run() {
		data.setName("yinjihuan");
	}
}
複製程式碼

Future<?> submit(Runnable task);
直接submit一個Runnable是拿不到返回值的,返回值就是null.

五種執行緒池的使用場景

  • newSingleThreadExecutor:一個單執行緒的執行緒池,可以用於需要保證順序執行的場景,並且只有一個執行緒在執行。

  • newFixedThreadPool:一個固定大小的執行緒池,可以用於已知併發壓力的情況下,對執行緒數做限制。

  • newCachedThreadPool:一個可以無限擴大的執行緒池,比較適合處理執行時間比較小的任務。

  • newScheduledThreadPool:可以延時啟動,定時啟動的執行緒池,適用於需要多個後臺執行緒執行週期任務的場景。

  • newWorkStealingPool:一個擁有多個任務佇列的執行緒池,可以減少連線數,建立當前可用cpu數量的執行緒來並行執行。

執行緒池的關閉

關閉執行緒池可以呼叫shutdownNow和shutdown兩個方法來實現

shutdownNow:對正在執行的任務全部發出interrupt(),停止執行,對還未開始執行的任務全部取消,並且返回還沒開始的任務列表

public class ThreadPool {
	public static void main(String[] args) throws Exception {
		ExecutorService pool = Executors.newFixedThreadPool(1);
		for (int i = 0; i < 5; i++) {
			System.err.println(i);
			pool.execute(() -> {
				try {
					Thread.sleep(30000);
					System.out.println("--");
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		Thread.sleep(1000);
		List<Runnable> runs = pool.shutdownNow();
	}
}
複製程式碼

上面的程式碼模擬了立即取消的場景,往執行緒池裡新增5個執行緒任務,然後sleep一段時間,執行緒池只有一個執行緒,如果此時呼叫shutdownNow後應該需要中斷一個正在執行的任務和返回4個還未執行的任務,控制檯輸出下面的內容:

0
1
2
3
4
[fs.ThreadPool$$Lambda$1/990368553@682a0b20, 
fs.ThreadPool$$Lambda$1/990368553@682a0b20, 
fs.ThreadPool$$Lambda$1/990368553@682a0b20, 
fs.ThreadPool$$Lambda$1/990368553@682a0b20]
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at fs.ThreadPool.lambda$0(ThreadPool.java:15)
	at fs.ThreadPool$$Lambda$1/990368553.run(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)

複製程式碼

shutdown:當我們呼叫shutdown後,執行緒池將不再接受新的任務,但也不會去強制終止已經提交或者正在執行中的任務

public class ThreadPool {
	public static void main(String[] args) throws Exception {
		ExecutorService pool = Executors.newFixedThreadPool(1);
		for (int i = 0; i < 5; i++) {
			System.err.println(i);
			pool.execute(() -> {
				try {
					Thread.sleep(30000);
					System.out.println("--");
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		Thread.sleep(1000);
		pool.shutdown();
		pool.execute(() -> {
			try {
				Thread.sleep(30000);
				System.out.println("--");
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
	}
}
複製程式碼

上面的程式碼模擬了正在執行的狀態,然後呼叫shutdown,接著再往裡面新增任務,肯定是拒絕新增的,請看輸出結果:

0
1
2
3
4
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task fs.ThreadPool$$Lambda$2/1747585824@3d075dc0 rejected from java.util.concurrent.ThreadPoolExecutor@214c265e[Shutting down, pool size = 1, active threads = 1, queued tasks = 4, 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 fs.ThreadPool.main(ThreadPool.java:24)

複製程式碼

還有一些業務場景下需要知道執行緒池中的任務是否全部執行完成,當我們關閉執行緒池之後,可以用isTerminated來判斷所有的執行緒是否執行完成,千萬不要用isShutdown,isShutdown只是返回你是否呼叫過shutdown的結果。

public class ThreadPool {
	public static void main(String[] args) throws Exception {
		ExecutorService pool = Executors.newFixedThreadPool(1);
		for (int i = 0; i < 5; i++) {
			System.err.println(i);
			pool.execute(() -> {
				try {
					Thread.sleep(3000);
					System.out.println("--");
				} catch (Exception e) {
					e.printStackTrace();
				}
			});
		}
		Thread.sleep(1000);
		pool.shutdown();
		while(true){  
            if(pool.isTerminated()){  
                System.out.println("所有的子執行緒都結束了!");  
                break;  
            }  
            Thread.sleep(1000);    
        }  
	}
}
複製程式碼

自定義執行緒池

在實際的使用過程中,大部分我們都是用Executors去建立執行緒池直接使用,如果有一些其他的需求,比如指定執行緒池的拒絕策略,阻塞佇列的型別,執行緒名稱的字首等等,我們可以採用自定義執行緒池的方式來解決。

如果只是簡單的想要改變執行緒名稱的字首的話可以自定義ThreadFactory來實現,在Executors.new…中有一個ThreadFactory的引數,如果沒有指定則用的是DefaultThreadFactory。

自定義執行緒池核心在於建立一個ThreadPoolExecutor物件,指定引數

下面我們看下ThreadPoolExecutor建構函式的定義:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) ;
複製程式碼
  • corePoolSize
    執行緒池大小,決定著新提交的任務是新開執行緒去執行還是放到任務佇列中,也是執行緒池的最最核心的引數。一般執行緒池開始時是沒有執行緒的,只有當任務來了並且執行緒數量小於corePoolSize才會建立執行緒。
  • maximumPoolSize
    最大執行緒數,執行緒池能建立的最大執行緒數量。
  • keepAliveTime
    線上程數量超過corePoolSize後,多餘空閒執行緒的最大存活時間。
  • unit
    時間單位
  • workQueue
    存放來不及處理的任務的佇列,是一個BlockingQueue。
  • threadFactory
    生產執行緒的工廠類,可以定義執行緒名,優先順序等。
  • handler
    拒絕策略,當任務來不及處理的時候,如何處理, 前面有講解。

瞭解上面的引數資訊後我們就可以定義自己的執行緒池了,我這邊用ArrayBlockingQueue替換了LinkedBlockingQueue,指定了佇列的大小,當任務超出佇列大小之後使用CallerRunsPolicy拒絕策略處理。

這樣做的好處是嚴格控制了佇列的大小,不會出現一直往裡面新增任務的情況,有的時候任務處理的比較慢,任務數量過多會佔用大量記憶體,導致記憶體溢位。

當然你也可以在提交到執行緒池的入口進行控制,比如用CountDownLatch, Semaphore等。

/**
 * 自定義執行緒池<br>
 * 預設的newFixedThreadPool裡的LinkedBlockingQueue是一個無邊界佇列,如果不斷的往裡加任務,最終會導致記憶體的不可控<br>
 * 增加了有邊界的佇列,使用了CallerRunsPolicy拒絕策略
 * @author yinjihuan
 *
 */
public class FangjiaThreadPoolExecutor {
	
	private static ExecutorService executorService = newFixedThreadPool(50);
	
	private static ExecutorService newFixedThreadPool(int nThreads) {
		return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
				new ArrayBlockingQueue<Runnable>(10000), new DefaultThreadFactory(), new CallerRunsPolicy());
	}
	
	public static void execute(Runnable command) {
		executorService.execute(command);
	}
	
	public static void shutdown() {
		executorService.shutdown();
	}
	
	static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "FSH-pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
	
}

複製程式碼

更多技術分享請關注微信公眾號:猿天地

image.png

相關文章