Worker Thread mode: How to avoid repeated thread creation

Topic: Worker Thread Mode: How to avoid repeated thread creation?

In the last article, we introduced one of the simplest division of labor mode - Thread-Per-Message mode , corresponding to the real world, in fact, commissioned . If this division of labor mode is implemented with Java Thread, frequent creation and destruction of threads will greatly affect performance. At the same time, unlimited creation of threads may also lead to OOM, so the usage scenarios in the Java field are limited.

To effectively avoid the frequent creation, destruction, and OOM of threads, we have to mention what we are going to talk about in detail today, which is also the most used Worker Thread mode in the Java field.

Worker Thread mode and its implementation

The Worker Thread mode can be compared to the working mode of the workshop in the real world: the workers in the workshop have some work, everyone works together, and chatting and waiting when there is no work. You can refer to the following diagram to understand that Worker Thread in the Worker Thread mode corresponds to the real world, and actually refers to the workers in the workshop. However, it should be noted here that the number of workers in the workshop is often determined.
Insert picture description here
How to simulate this working mode of workshop in the field of programming? Or how to implement Worker Thread mode? From the above diagram, you can easily think of using the blocking queue as the task pool, and then create a fixed number of threads to consume the tasks in the blocking queue. In fact, if you think about it carefully, this solution is the thread pool provided by the Java language.

The thread pool has many advantages, such as being able to avoid repeated creation and destruction of threads, while being able to limit the upper limit for creating threads, and so on. After studying the previous article, you already know that it is difficult to implement the Thread-Per-Message mode with Java Thread to deal with high concurrency scenarios, because the cost of frequently creating and destroying Java threads is a bit high, and the unlimited creation of threads is also possible. Lead to the application of OOM. Thread pool can solve these problems.

Let's take the echo program as an example to see how to implement it with a thread pool.

The following sample code is an echo server implemented with a thread pool. Compared with the implementation of the Thread-Per-Message mode, there are very few changes, just create a thread pool es with a maximum number of threads of 500, and then pass es.execute The () method submits the task requested for processing to the thread pool for processing.

ExecutorService es = Executors.newFixedThreadPool(500);

final ServerSocketChannel ssc = 
  ServerSocketChannel.open().bind(new InetSocketAddress(8080));
  
//处理请求    
try {
  while (true) {
    // 接收请求
    SocketChannel sc = ssc.accept();
    // 将请求处理任务提交给线程池
    es.execute(()->{
      try {
        // 读Socket
        ByteBuffer rb = ByteBuffer
          .allocateDirect(1024);
        sc.read(rb);
        //模拟处理请求
        Thread.sleep(2000);
        // 写Socket
        ByteBuffer wb = 
          (ByteBuffer)rb.flip();
        sc.write(wb);
        // 关闭Socket
        sc.close();
      }catch(Exception e){
        throw new UncheckedIOException(e);
      }
    });
  }
} finally {
  ssc.close();
  es.shutdown();
}

Create thread pool correctly

Java's thread pool can not only avoid creating OOM due to unlimited threads , but also avoid OOM due to unlimited receiving tasks . It's just that the latter is often easily overlooked by us. For example, in the above implementation, it is ignored by us. So it is strongly recommended that you use the creation of bounded queues to receive tasks.

When the number of requests is greater than the capacity of the bounded queue, it is necessary to reasonably reject the request. How to reasonably refuse? This requires you to make a combination of specific business scenarios. Even if the default rejection strategy of the thread pool can meet your needs, it is also recommended that you clearly indicate the rejection strategy when creating the thread pool.

At the same time, in order to facilitate debugging and diagnosis, I also strongly recommend that you give the thread a business-related name in your actual work .

Based on the above three suggestions, the following sample code can be used to create a thread in the echo program.

ExecutorService es = new ThreadPoolExecutor(
  50, 500,
  60L, TimeUnit.SECONDS,
  //注意要创建有界队列
  new LinkedBlockingQueue<Runnable>(2000),
  //建议根据业务需求实现ThreadFactory
  r->{
    return new Thread(r, "echo-"+ r.hashCode());
  },
  //建议根据业务需求实现RejectedExecutionHandler
  new ThreadPoolExecutor.CallerRunsPolicy());

Avoid thread deadlock

In the process of using the thread pool, we should also pay attention to a thread deadlock scenario. If tasks submitted to the same thread pool are not independent of each other, but have dependencies, it may lead to thread deadlock. In actual work, I have experienced this kind of thread deadlock scenario. The specific phenomenon is that the application will occasionally be in an unresponsive state every time it runs for a while. The monitoring data seems to be normal, but in fact it is no longer working properly.

After the related logic is simplified in this problematic application, as shown in the following figure, the application divides a large-scale computing task into two phases. The task in the first phase will wait for the subtasks in the second phase to complete. In this application, each stage uses a thread pool, and the same thread pool is used in both stages.
Insert picture description here
We can use the following sample code to simulate the application, if you execute the following code, you will find that it will never execute the last line. There are no exceptions during execution, but the application has stopped responding.

//L1、L2阶段共用的线程池
ExecutorService es = Executors.newFixedThreadPool(2);

//L1阶段的闭锁    
CountDownLatch l1=new CountDownLatch(2);
for (int i=0; i<2; i++){
  System.out.println("L1");
  //执行L1阶段任务
  es.execute(()->{
    //L2阶段的闭锁 
    CountDownLatch l2=new CountDownLatch(2);
    //执行L2阶段子任务
    for (int j=0; j<2; j++){
      es.execute(()->{
        System.out.println("L2");
        l2.countDown();
      });
    }
    
    //等待L2阶段任务执行完
    l2.await();
    l1.countDown();
  });
}
//等着L1阶段任务执行完
l1.await();
System.out.println("end");

When a similar problem occurs in the application, the preferred diagnostic method is to check the thread stack. The following figure is the thread stack after the above sample code stops responding. You will find that both threads in the thread pool are blocked in l2.await (); This line of code is on, that is, all threads in the thread pool Waiting for the completion of the task in the L2 phase, when can the subtasks in the L2 phase be completed? It will never be that day, why? Because the threads in the thread pool are blocked, there are no idle threads to perform the tasks in the L2 phase.
Insert picture description here
After finding the reason, how to solve it is simple. The simplest and crudest way is to increase the maximum number of threads in the thread pool. This method is also feasible if the number of tasks is not very large, otherwise this method will not work. . In fact, the common solution to this problem is to create different thread pools for different tasks. ** For the above application, if the L1 phase task and the L2 phase task each have their own thread pool, this problem will not occur.

Finally, emphasize again: tasks submitted to the same thread pool must be independent of each other, otherwise you must be cautious.

to sum up

We once said that the best way to solve the problem of division of labor in concurrent programming is to compare it with the real world. Comparing with the real world to build a model in the field of programming can make the model easier to understand. The Thread-Per-Message mode we introduced in the previous article is similar to the commissioning of others in the real world, and the Worker Thread mode introduced today is similar to the work mode of workers in the workshop. If you find that after modeling the business model in the design stage, the model is very similar to the working mode of the workshop, then you can basically determine that the Worker Thread mode can be used in the implementation stage.

What is the difference between Worker Thread mode and Thread-Per-Message mode? From a real-world perspective, when you entrust an agent to do things, you often communicate directly with the agent; corresponding to the field of programming, its implementation is also that the main thread directly creates a sub-thread, and the main and sub-threads can communicate directly. The work methods of workshop workers are completely around tasks. It is impossible to know in advance which worker performs a specific task; corresponding to the programming field, the main thread submits the task to the thread pool, but the main thread does not care By which thread the task is executed.

Worker Thread mode can avoid the problem of frequent thread creation and destruction, and can limit the maximum number of threads. The Java language can directly use the thread pool to implement the Worker Thread mode. The thread pool is a very basic and excellent tool class. Even the coding specifications of some major manufacturers do not allow new Thread () to create threads. You must use the thread pool .

However, you need to be extra cautious when using the thread pool. In addition to how to correctly create the thread pool and how to avoid the thread deadlock problem, we also need to pay attention to the ThreadLocal memory leak problem we mentioned earlier. At the same time, for the tasks submitted to the thread pool, we must also handle exceptions to avoid the abnormal tasks slipping away. From a business perspective, sometimes the consequences of tasks that do not find abnormalities are often very serious.

Demo

1
public class Channel {//流水线上的货物,流水线工人从流水线上拿货物工作

    private final static int MAX_REQUEST = 100;

    private final Request[] requestQueue;

    private int head;//队头

    private int tail;//队尾

    private int count;//流水线当前的数量

    private final WorkerThread[] workerPool;//流水线的工人

    public Channel(int workers) {
        this.requestQueue = new Request[MAX_REQUEST];
        this.head = 0;
        this.tail = 0;
        this.count = 0;
        this.workerPool = new WorkerThread[workers];//线程
        this.init();
    }

    private void init() {
        for (int i = 0; i < workerPool.length; i++) {
            workerPool[i] = new WorkerThread("Worker-" + i, this);
        }
    }

    /**
     * push switch to start all of worker to work.  一推开关,流水线工人开始工作
     */
    public void startWorker() {//给外面的人调用
        Arrays.asList(workerPool).forEach(WorkerThread::start);
    }

    public synchronized void put(Request request) {//放到流水线上面去,
        while (count >= requestQueue.length) {
            try {
                this.wait();
            } catch (Exception e) {
            }
        }

        this.requestQueue[tail] = request;
        this.tail = (tail + 1) % requestQueue.length;//简单算法,每次移动到前一个位置
        this.count++;
        this.notifyAll();
    }

    public synchronized Request take() {
        while (count <= 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        Request request = this.requestQueue[head];
        this.head = (this.head + 1) % this.requestQueue.length;//
        this.count--;
        this.notifyAll();
        return request;
    }
}
2
public class Request {

    private final String name;

    private final int number;

    public Request(final String name, final int number) {
        this.name = name;
        this.number = number;
    }

    public void execute() {
        System.out.println(Thread.currentThread().getName() +
         " executed " + this);//哪位工人放的,拿到该货再执行这个方法
    }

    @Override
    public String toString() {
        return "Request=> No. " + number + " Name. " + name;
    }
}
3
public class WorkerThread extends Thread {

    private final Channel channel;

    private static final Random random = new Random(System.currentTimeMillis());

    public WorkerThread(String name, Channel channel) {
        super(name);
        this.channel = channel;
    }

    @Override
    public void run() {
        while (true) {
            channel.take().execute();
            try {
                Thread.sleep(random.nextInt(1_000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
4
public class WorkerClient {
    public static void main(String[] args) {
        final Channel channel = new Channel(5);
        channel.startWorker();//拉咂开工

        //搬运工开始搬货物到流水线上面
        new TransportThread("Alex", channel).start();
        new TransportThread("Jack", channel).start();
        new TransportThread("William", channel).start();
    }
}
5
public class TransportThread extends Thread {
    private final Channel channel;//往流水线放

    private static final Random random = new Random(System.currentTimeMillis());
 
    //装配工人,搬运工,放到流水线上面
    public TransportThread(String name, Channel channel) {
        super(name);
        this.channel = channel;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; true; i++) {
                Request request = new Request(getName(), i);
                this.channel.put(request);
                Thread.sleep(random.nextInt(1_000));
            }
        } catch (Exception e) {
        }
    }
}
Published 138 original articles · Like 3 · Visitor 7203

Guess you like

Origin blog.csdn.net/weixin_43719015/article/details/105695230
Recommended