Noseparte說:網絡遊戲中的網關線程池是如何創建的

Noseparte說:網絡遊戲中的網關線程池是如何創建的

線程池 ThreadPool

1. 線程池的定義:

(摘自職Q)在面向對象編程中,創建和銷燬對象是很費時間的,因爲創建一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷燬後進行垃圾回收。所以提高服務程序效率的一個手段就是儘可能減少創建和銷燬對象的次數,特別是一些很耗資源的對象創建和銷燬,這就是"池化資源"技術產生的原因。線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。

2. 如何創建線程池:

  • 使用 ThreadPoolExecutor:
    ThreadPoolExecutor是一個靈活的、穩定的線程池,允許進行定製。
  • 使用 Executors:
    Executors中的靜態工廠方法之一來創建線程池:
    newSingleThreadExecutor: 是一個單線程的Executor,它創建單個工作者線程來執行任務,如果這個線程異常結束,會創建另一個線程來替代。newSingleThreadExecutor能確保依照任務在隊列中的順序來串行執行(例如 FIFO、LIFO、優先級)。
    newFixedThreadPool: 將創建一個固定長度的線程池,每當提交一個任務時就創建一個線程,直到達到線程池的最大數量,這時線程池的規模將不再發生變化(如果某個線程由於發生了未預期的Exception而結束,那麼線程池會補充一個新的線程)。
    newCachedThreadPool: 將創建一個可緩存的線程池,如果線程池的當前規模超過了處理需求時,那麼將回收空閒的線程,而當需求增加時,則可以添加新的線程,線程池規模不存在任何限制。
    newScheduledThreadExecutor: 創建了一個固定長度的線程池,而且以延遲或定時的方式來執行任務,類似於Timer。

配置 ThreadPoolExecutor

public class ThreadPoolExecutor {
    
    // 線程池維護的最小線程數
    private volatile int corePoolSize;
    // 線程池可容納線程數的最大值
    private volatile int maximumPoolSize;
    // 線程池達到閾值後,新的線程需要等待的時間
    private volatile long keepAliveTime;
    // 以工廠模式創建新的線程
    private volatile ThreadFactory threadFactory;
    // 上下文
    private final AccessControlContext acc;
    // 阻塞隊列
    private final BlockingQueue<Runnable> workQueue;
    // 拒絕策略
    private volatile RejectedExecutionHandler handler;
    
    /**
     * ThreadPoolExecutor的核心構造器
     */
    public ThreadPoolExecutor(int corePoolSize,
                                       int maximumPoolSize,
                                       long keepAliveTime,
                                       TimeUnit unit,
                                       BlockingQueue<Runnable> workQueue,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler handler) {
           if (corePoolSize < 0 || 
              maximumPoolSize <= 0 ||
              maximumPoolSize < corePoolSize || 
              keepAliveTime < 0) throw new IllegalArgumentException();
           if (workQueue == null || threadFactory == null || handler == null) 
              throw new NullPointerException();
           this.acc = System.getSecurityManager() == null ?
                    null :
                    AccessController.getContext();
           this.corePoolSize = corePoolSize;
           this.maximumPoolSize = maximumPoolSize;
           this.workQueue = workQueue;
           this.keepAliveTime = unit.toNanos(keepAliveTime);
           this.threadFactory = threadFactory;
           this.handler = handler;
    }
    
}

管理任務隊列 BlockingQueue

ThreadPoolExecutor允許提供一個BlockingQueue來保存等待執行的任務。基本的任務排隊方法有3種:無界隊列、有界隊列和同步移交(Synchronous Handoff)。

  • 無界隊列: 隊列大小無限制,常用的爲無界的LinkedBlockingQueue,使用該隊列做爲阻塞隊列時要尤其當心,當任務耗時較長時可能會導致大量新任務在隊列中堆積最終導致OOM。
    閱讀代碼發現,Executors.newFixedThreadPool 採用就是 LinkedBlockingQueue,而樓主踩到的就是這個坑,當QPS很高,發送數據很大,大量的任務被添加到這個無界LinkedBlockingQueue 中,導致cpu和內存飆升服務器掛掉。
  • 有界隊列: 常用的有兩類,
    一類是遵循FIFO原則的隊列如ArrayBlockingQueue與有界的LinkedBlockingQueue,
    另一類是優先級隊列如PriorityBlockingQueue。PriorityBlockingQueue中的優先級由任務的Comparator決定。
    使用有界隊列時隊列大小需和線程池大小互相配合,線程池較小有界隊列較大時可減少內存消耗,降低cpu使用率和上下文切換,但是可能會限制系統吞吐量。在我們的修復方案中,選擇的就是這個類型的隊列,雖然會有部分任務被丟失,但是我們線上是排序日誌蒐集任務,所以對部分對丟失是可以容忍的。
  • 同步移交隊列: 如果不希望任務在隊列中等待而是希望將任務直接移交給工作線程,可使用SynchronousQueue作爲等待隊列。SynchronousQueue不是一個真正的隊列,而是一種線程之間移交的機制。要將一個元素放入SynchronousQueue中,必須有另一個線程正在等待接收這個元素。只有在使用無界線程池或者有飽和策略時才建議使用該隊列。

飽和策略 RejectedExecutionHandler

ThreadPoolExecutor提供如下4種飽和策略:

  • CallerRunsPolicy 由調用線程(提交任務的線程)處理該任務
  • AbortPolicy 丟棄任務並直接拋出RejectedExecutionException異常(默認的線程池拒絕策略)
  • DiscardPolicy 僅丟棄任務並不拋出異常
  • DiscardOldestPolicy 丟棄隊列最前面的任務,然後重新提交被拒絕的任務

自定義飽和策略,只需實現RejectedExecutionHandler接口並重寫void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法

public class ThreadPoolExecutor{

    /** 
     *  默認的線程池拒絕策略 AbortPolicy
     */
    private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

    /* ThreadPoolExecutor提供如下4中拒絕策略: */
    /**
     * 由調用線程(提交任務的線程)處理該任務
     */
   public static class CallerRunsPolicy implements RejectedExecutionHandler {}
 
    /** 
     *  丟棄任務並直接拋出RejectedExecutionException異常
     */
   public static class AbortPolicy implements RejectedExecutionHandler {}
 
   /** 
    * 僅丟棄任務並不拋出異常
    */
   public static class DiscardPolicy implements RejectedExecutionHandler {}
   
   /** 
    * 丟棄隊列最前面的任務,然後重新提交被拒絕的任務
    */
   public static class DiscardOldestPolicy implements RejectedExecutionHandler {}

}

Executors(不推薦)

在阿里巴巴Java開發手冊中提到,使用Executors創建線程池可能會導致OOM(OutOfMemory ,內存溢出)

BlockingQueue致使OOM示意圖

ExecutorService

public interface ExecutorService extends Executor {
     void shutdown();
     List<Runnable> shutdownNow();
     boolean isShutdown(); 
     boolean isTerminated();
     boolean awaitTermination(long timeout, TimeUnit unit)throws InterruptedException;
     // .......其他用於任務提交的方法   
}    

爲了解決執行服務的生命週期問題,
ExecutorService拓展了Executor接口,添加了一些用於生命週期管理的方法。
ExecutorService的生命週期有3種狀態:運行、關閉和已終止。
ExecutorService在初始創建時處於運行狀態。
shutdown方法將執行平緩的關閉過程:不再接受新的任務,同時等待已經提交的任務執行完成——包括那些還未開始執行的任務。
shutdownNow方法將執行粗暴的關閉過程:它將嘗試取消所有運行中的任務,並且不再啓動隊列中尚未開始執行的任務。

ThreadFactory

DefaultThreadFactory


/** * The default thread factory */
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 = "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;
        }
}

PrivilegedThreadFactory

/**
 * 權限訪問與類加載
 */
static class PrivilegedThreadFactory extends DefaultThreadFactory {

    private final AccessControlContext acc;
    private final ClassLoader ccl;
    PrivilegedThreadFactory() {
        super();
        SecurityManager sm = System.getSecurityManager(); 
        if (sm != null) {
            // Calls to getContextClassLoader from this class
            // never trigger a security check, but we check 
            // whether our callers have this permission anyways. 
            sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION); 
            // Fail fast 
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        this.acc = AccessController.getContext(); 
        this.ccl = Thread.currentThread().getContextClassLoader();
    } 
    
    public Thread newThread(final Runnable r) {
        return super.newThread(new Runnable() { 
            public void run() {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        Thread.currentThread().setContextClassLoader(ccl); 
                        r.run(); 
                         return null;
                    } 
                }, acc); 
            }
        }); 
    }
}

使用guava的 ThreadFactoryBuilder


public class ThreadFactoryBuilder{

    private static ThreadFactory doBuild(ThreadFactoryBuilder builder) {
        final String nameFormat = builder.nameFormat;
        final Boolean daemon = builder.daemon;
        final Integer priority = builder.priority;
        final UncaughtExceptionHandler uncaughtExceptionHandler = builder.uncaughtExceptionHandler;
        final ThreadFactory backingThreadFactory = 
             (builder.backingThreadFactory != null) 
                ? builder.backingThreadFactory
                : Executors.defaultThreadFactory();
        final AtomicLong count = (nameFormat != null) ? new AtomicLong(0) : null;
        return new ThreadFactory() { 
            @Override 
            public Thread newThread(Runnable runnable) { 
                Thread thread = backingThreadFactory.newThread(runnable); 
                if (nameFormat != null) {
                    thread.setName(format(nameFormat, count.getAndIncrement())); 
                }
                if (daemon != null) {// 守護線程
                    thread.setDaemon(daemon);
                }
                if (priority != null) {// 優先級
                    thread.setPriority(priority); 
                } 
                if (uncaughtExceptionHandler != null) {
                    thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
                } 
                return thread;
            }
        };
    }
}

創建線程池的正確姿勢

/** 
 * @Auther: Noseparte * @Date: 2019/11/27 10:35 
 * @Description: 
 * 
 * <p>定製協議網關線程池</p> 
 */
public class ThreadPool { 

    protected final static Logger _LOG = LogManager.getLogger(ThreadPool.class); 
    private List<ExecutorService> workers = new ArrayList<>(); 
    private int threadCount; 
    private ThreadFactory threadFactory;
    
    public ThreadPool(int threadCount) {
        this(threadCount, new UserThreadFactory("網關遊戲邏輯協議線程池")); 
    } 
    
    public ThreadPool(int threadCount, ThreadFactory threadFactory) {
        this.threadCount = threadCount;
        this.threadFactory = threadFactory;
        if (threadCount <= 0 || null == threadFactory) 
            throw new IllegalArgumentException();
            for (int i = 0; i < threadCount; i++) { 
                workers.add(new ThreadPoolExecutor(threadCount, 200,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(1024),
                    threadFactory, 
                    new ThreadPoolExecutor.AbortPolicy()));
            } 
    } 
    
    public Future execute(Runnable task, int mold) {
        int index = Math.abs(mold) % threadCount;
        ExecutorService executor = workers.get(index);
        if (null == executor) {
            _LOG.error("sid=" + mold + ", tid=" + index); 
            return null;
        }
        return executor.submit(task); 
    } 
    
    public void shutdown() {
        int count = 0;
        for (ExecutorService worker : workers) { 
            _LOG.error("close thread{}.", ++count); 
            worker.shutdown();
        }
    } 
    
    static class UserThreadFactory 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;
        
        UserThreadFactory(String poolName) { 
            SecurityManager s = System.getSecurityManager(); 
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup(); 
            namePrefix = poolName + "-" +
                    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;
        }
        
    }
}

總結

創建線程池的注意事項:

  1. 根據業務場景定製ThreadFactory、飽和策略、任務隊列、ThreadPoolExecutor
  2. 注意BlockingQueue中任務阻塞數量越來越多會導致內存耗盡(OOM), 要設置隊列的上限值

源碼地址:
Almost-Famous: 遊戲中的網關線程池是如何創建的

相關博文:友情鏈接
一次Java線程池誤用引發的血案和總結
Java中線程池,你真的會用嗎?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章