Detailed explanation of ThreadPoolExecutor

Posted by y4m4 on Fri, 10 Sep 2021 21:31:28 +0200

Thread pool

Thread pool type:

ThreadPoolExecutor

ForkJoinPool - breakdown summary task for CPU intensive

ThreadPoolExecuter

Class relation

ThreadPoolExecutor His parent is from AbstractExecutorService,and AbstractExecutorService The parent class of is ExecutorService,again ExecutorService The parent class of is Executor,therefore ThreadPoolExecutor It is equivalent to the executor of the thread pool. You can throw tasks into the pool and let the thread pool run.

1.Executor(Interface, only execute method, separating thread definition and execution) - extensions - >
2. ExecutorService(Interface, which defines the thread life cycle method) - implements - >
3.AbstractExecutorService(abstract, class, which implements some thread life cycle methods (because it is an abstract class, it can not be fully implemented), as the parent class of thread pool) - extends - >
4. ThreadPoolExecutor - construction method application - >
5. singleThreadPool, CachedThreadPool, custom thread pool, etc

structure

HashSet (storage worker) + BlockingQueue (storage task)

Custom thread pool parameters:

  • The first parameter * * number of corePoolSoze core threads;
  • The second one is * * maximumPoolSize. The maximum number of threads is not enough. It can be expanded to the maximum number of threads, including core threads;
  • The third * * keepAliveTime thread has a lifetime. The thread has not worked for a long time. Return it to the operating system;
  • The fourth TimeUnit.SECONDS defines whether the unit of lifetime is milliseconds, nanoseconds or seconds;
  • The fifth is the * * task queue. All kinds of blockingqueues can be thrown into it. Different queues have different queue receiving modes;
  • The sixth is the thread factory defaultThreadFactory, which returns a new DefaultThreadFactory. To implement the ThreadFactory interface, there is only one method called newThread, so threads are generated. Custom threads can be generated in this way. The default ThreadFactory is generated, The defaultThreadFactory has several characteristics when generating threads: when new comes out, the group is specified, the thread name is formulated, and then the specified thread is definitely not a guard thread. Set the priority of your thread. You can define what kind of thread is generated and what the name of the specified thread is (why to specify the thread name and what the meaning is, it can facilitate error backtracking);
  • The seventh is called reject policy, which means that when the thread pool is busy and the task queue is full, we have to implement various reject policies. jdk provides four kinds of reject policies by default, which can also be customized.
    1: Abort: throw exception
    2: Discard: throw it away without throwing exceptions
    3: Discard oldest: throw away the longest queue
    4: CallerRuns: caller processing service
    Generally, we will customize the four policies to implement the interface of the rejection policy. The processing method is that our messages need to be saved. If the order is placed, it needs to be saved to kafka, redis or the database. You can make a log.

Size of thread pool (maximumPoolSize):

N_threads=N_cpu * UseCap_cpu * (1 + Wait/Calculation)

UseCap_cpu: expected CPU utilization

Wait and Calculation: thread waiting and Calculation time (distribution intensive, IO intensive)

Pressure measurement must be carried out during actual use

Thread pool provided by JDK

JDK provides four default thread pools

Executors: method class of thread pool

singleThreadPool

There is only one thread pool, which can ensure the sequential execution of tasks

ExecutorService service = Executors.newSingleThreadExecutor();
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));   
}

Since there is only one thread, why use a thread pool?

1. The thread pool can maintain the waiting queue by itself, which shows that the thread creation needs to be maintained by itself

2. Thread pool can manage the life cycle of threads without developers calling methods themselves

CachedThreadPool

Suitable for fluctuating type

ExecutorService service = Executors.newCachedThreadPool(); 
public static ExecutorService newCachedThreadPool() {       
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());    
}

fixedThreadPool

Suitable for smooth type

ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue<Runnable>());    
}

ScheduledPool

Timed task thread pool

Use less, simple timers and complex timing frames (such as quartz and cron)

ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
public ScheduledThreadPoolExecutor(int corePoolSize) { 
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());    
}

If there is an alarm clock service and one billion people subscribe, how to design it?

From a large perspective, the traffic is transferred from the main server to multiple edge servers. A single server uses thread pool and queue

Thread pool usage example:

public class HelloThreadPool {
    static class Task implements Runnable {
        private int i;
        public Task(int i) {
            this.i = i;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " Task " + i);
        }
        @Override
        public String toString() {
            return "Task{" +
                    "i=" + i +
                    '}';
        }
    }
    public static void main(String[] args) {
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4,
                60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(4),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        for (int i = 0; i < 7; i++) {
            tpe.execute(new Task(i));
        }

        System.out.println(tpe.getQueue());
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }
        tpe.shutdown();
    }
}

Interpretation of ThreadPoolExecutor source code

  1. Main structure

    ThreadPoolExecutor:

    //ctl:AtomicInteger type, 32 bits in total. The first 3 bits represent the status of the thread pool and the last 29 bits represent the number of threads. The reason for this is to reduce the complexity of cas
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //Workers: stores the instance object of workers, which can be considered as the stored thread object
    private final HashSet<Worker> workers = new HashSet<Worker>();
    //workQueue: stores the tasks waiting to be executed, i.e. Runnable
    private final BlockingQueue<Runnable> workQueue;
    

    Worker:

    //thread 
    final Thread thread;
    //Tasks carried by the current worker
    Runnable firstTask;
    //Number of tasks completed
    volatile long completedTasks;
    
    1. Process Mapping

tips:

  • worker.thread can be understood as a consumer when a while loop executes a task, and as a producer when a main thread puts in a task

  • There is no difference between core threads and non core threads, but a boundary drawn by coresize (number of core threads)

  • The thread pool needs to maintain a certain number of threads. These threads will not end but block when they finish executing tasks and the task queue is empty. These threads are core threads waiting to obtain tasks in the queue. Other threads will be terminated when they finish executing tasks and the task queue is empty, When the thread is to be terminated, it will also check whether the workQueue is not empty and whether the core thread has free space. If so, another thread will be created, which may be the core thread

  • When the workQueue is empty, the getTask() method controls the return of core threads and non core threads through the different processing of poll() and take() by BlockingQueue

    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    

ForkJoinPool

Break down and summarize tasks for CPU intensive

Use example:

public class T12_ForkJoinPool {
   static int[] nums = new int[1000000];
   static final int MAX_NUM = 50000;
   static Random r = new Random();
   static {
      for(int i=0; i<nums.length; i++) {
         nums[i] = r.nextInt(100);
      }
      
      System.out.println("---" + Arrays.stream(nums).sum()); //stream api
   }
   //No return value for subtask
   static class AddTask extends RecursiveAction {
      int start, end;
      AddTask(int s, int e) {
         start = s;
         end = e;
      }
      @Override
      protected void compute() {
         if(end-start <= MAX_NUM) {
            long sum = 0L;
            for(int i=start; i<end; i++) sum += nums[i];
            System.out.println("from:" + start + " to:" + end + " = " + sum);
         } else {
            int middle = start + (end-start)/2;
            AddTask subTask1 = new AddTask(start, middle);
            AddTask subTask2 = new AddTask(middle, end);
            subTask1.fork();
            subTask2.fork();
         }
      }
   }
   //Subtasks have return values
   static class AddTaskRet extends RecursiveTask<Long> {
      private static final long serialVersionUID = 1L;
      int start, end;
      AddTaskRet(int s, int e) {
         start = s;
         end = e;
      }
      @Override
      protected Long compute() {
         if(end-start <= MAX_NUM) {
            long sum = 0L;
            for(int i=start; i<end; i++) sum += nums[i];
            return sum;
         } 
         int middle = start + (end-start)/2;
         AddTaskRet subTask1 = new AddTaskRet(start, middle);
         AddTaskRet subTask2 = new AddTaskRet(middle, end);
         subTask1.fork();
         subTask2.fork();
         return subTask1.join() + subTask2.join();
      }
   }
   public static void main(String[] args) throws IOException {
      ForkJoinPool fjp = new ForkJoinPool();
      AddTask task = new AddTask(0, nums.length);
      fjp.execute(task);

      AddTaskRet taskRet = new AddTaskRet(0, nums.length);
      fjp.execute(taskRet);
      long result = taskRet.join();
      System.out.println(result);
   }
}

WorkStealingPool:

A kind of ForkJoinPool

Applicable: when the task scale is large but can be disassembled into subtasks, subtasks with / without return values (similar to recursion) are supported

fjp.execute(task);

  AddTaskRet taskRet = new AddTaskRet(0, nums.length);
  fjp.execute(taskRet);
  long result = taskRet.join();
  System.out.println(result);

}
}

### WorkStealingPool: 

ForkJoinPool A kind of

Applicable: when the task scale is large, but it can be disassembled into subtasks, the subtask band is supported/No return value (similar to recursion)

Structure: each thread has a separate queue. After the tasks in its own queue are executed, it can go to other queues for task execution

Topics: Java Multithreading thread pool