Homemade simple thread pool with java (independent of concurrent package)

Keywords: Java less Linux jvm

Long ago, people did not want to use processes in order to continue to enjoy the benefits of parallelism, so they created lighter threads than processes. Taking linux as an example, creating a process needs to apply for new memory space and copy some data from the parent process, so the overhead is relatively large. Threads (or lightweight processes) can share memory space with the parent process, so that the overhead of creating threads is much less than that of creating processes. Therefore, multithreading flourishes today.
But even though the cost of creating threads is small, frequent creation and deletion is a waste of performance. So people think of thread pools. Threads in thread pools will only be created once, and will not be destroyed when they are used up, but wait for reuse in thread pools. This is especially suitable for many small tasks. For example, at the extreme point, if the execution of a small task consumes less than the creation and destruction of a thread, then more than half of the performance consumption without a thread pool will be thread creation and destruction. When I first learned java, I didn't understand thread pools, especially how threads are reused, and how thread pools relate to the Thread/Runnable objects I created. Today we're going to write a suggested thread pool to understand all this. (Not dependent on the java concurrent package)
First, correct a misunderstanding of many people. When we create a Thread/Runnable object, we do not create a thread, but create a task that needs to be executed by the thread. When we call the Thread.start() method, jvm will help us create a thread. Thread pools just help you perform these tasks. When you submit, you just put the task in a storage and wait for the idle threads in the thread pool to execute, rather than creating threads. Knowing this, we first have to have something to store tasks, but also support multi-threaded access, and preferably support blocking to avoid idle threads without tasks.
In addition to storage, we also need threads to consume these tasks. As you can see, this is actually a producer-consumer model. Java has many producer-consumer implementations. You can refer to my previous blog. Several Implementations of Java Producer and Consumer . If we implement thread pooling, we can choose to use BlockingQueue. Although many BlockingQueues have been implemented in the java concurrent package, in order to let you understand what BlockingQueue has done, I simply encapsulate a simple BlockingQueue with LinkedListQueue, the code is as follows.

package me.xindoo.concurrent;

import java.util.LinkedList;
import java.util.Queue;

public class LinkedBlockingQueue<E> {
    private Object lock;
    private Queue<E> queue;
    public LinkedBlockingQueue() {
        queue = new LinkedList<>();
        lock = new Object();
    }

    public boolean add(E e) {
        synchronized (lock) {
            queue.add(e);
            lock.notify();
            return true;
        }
    }

    public  E take() {
        synchronized (lock) {
            while (queue.isEmpty()) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return queue.poll();
        }
    }

    public synchronized int size() {
        return queue.size();
    }
}

I simply added synchronized LinkedListQueue to ensure its normal operation in a multi-threaded environment. Secondly, I add thread blocking through wait() method when queue is empty to prevent thread idling when queue is empty. Since blocking is added, wake-up is also required. Every time a task is added to the queue, notify() is called to wake up a waiting thread.
Storage is done, the next thing we need to achieve is consumers. The consumer is the thread in the thread pool, because the tasks in the task queue all implement the Runnable interface, so we can call its run() method directly when consuming tasks. When a task is completed and retrieved from the queue, the entire thread pool is shutdown.

package me.xindoo.concurrent;

public class ThreadPool {
    private int coreSize;
    private boolean stop = false;
    private LinkedBlockingQueue<Runnable> tasks = new LinkedBlockingQueue<>();
    private Thread[] threads;

    public ThreadPool(int coreSize)  {
        this.coreSize = coreSize;
        threads = new Thread[coreSize];
        for (int i = 0; i < coreSize; i++) {
            threads[i] = new Thread(new Worker("thread"+i));
            threads[i].start();
        }
    }

    public boolean submit(Runnable command) {
        return tasks.add(command);
    }

    public void shutdown() {
        stop = true;
    }

    private class Worker implements Runnable {
        public String name;

        public Worker(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            while (!stop) {
                Runnable command = tasks.take();
                System.out.println(name + " start run, starttime " + System.currentTimeMillis()/1000);  //Easy to observe thread execution
                command.run();
                System.out.println(name + " finished, endtime " + System.currentTimeMillis()/1000);
            }
        }
    }
}

Above is the implementation of a thread pool, is it simple to initialize a fixed number of threads in the constructor, each thread just takes tasks from the queue and executes them... It's always circulating.
Yes, a simple thread pool is implemented with dozens of lines of code above. It is ready to be used, even in the production environment. Of course, this is not a well-functioning thread pool similar to that in concurrent package, where parameters can be customized, but it does realize the basic function of a thread pool, thread reuse. Next, write a suggested test code. If the thread pool producer is the consumer in the consumer model, then the test code is the producer. The code is as follows.

package me.xindoo.concurrent;

public class Test {
    private static class Task implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
    public static void main(String[] args) {
        ThreadPool pool = new ThreadPool(5);

        for (int i = 0; i < 30; i++) {
            pool.submit(new Task());
        }

        System.out.println("add task finished");

        try {
            Thread.sleep(10000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        pool.shutdown();
    }
}

The results are as follows.

thread0 start run, starttime 1566724202
thread2 start run, starttime 1566724202
thread1 start run, starttime 1566724202
thread3 start run, starttime 1566724202
thread4 start run, starttime 1566724202
add task finished
thread2 finished, endtime 1566724207
thread2 start run, starttime 1566724207
thread1 finished, endtime 1566724207
thread4 finished, endtime 1566724207
thread3 finished, endtime 1566724207
thread0 finished, endtime 1566724207
thread3 start run, starttime 1566724207
thread4 start run, starttime 1566724207
thread1 start run, starttime 1566724207
thread0 start run, starttime 1566724207
thread3 finished, endtime 1566724212
thread0 finished, endtime 1566724212
thread1 finished, endtime 1566724212
thread4 finished, endtime 1566724212
thread2 finished, endtime 1566724212

The test code is also very simple, creating five threads, and then submitting 30 tasks. You can see from the output that five threads have completed 30 tasks in batches. Note: Although the tasks in my test code are very simple, complex tasks are also possible.

summary

In real-time, as mentioned several times above, the java.util.concurrent package has helped you to implement a robust and powerful thread pool. You don't need to build wheels anymore, you can use different BlockingQueue to achieve different functions of the thread pool. Take a chestnut, such as Delayed WorkQueue, to implement thread pools that can be executed regularly. Even Executors encapsulated a simpler thread pool creation interface for everyone, but the Alibaba Java Development Manual forcibly forbids the use of Executors to create thread pools. Instead, it uses ThreadPool Executor, which allows students to write more clearly about the rules of thread pool operation and avoid resource exhaustion. Risks.

  1. Fixed ThreadPool and Single ThreadPool: The allowable request queue length is Integer.MAX_VALUE, which may accumulate a large number of requests, leading to OOM.
  2. CachedThreadPool and Scheduled ThreadPool: The number of creation threads allowed is Integer.MAX_VALUE, which may create a large number of threads, leading to OOM.

Last but not least, we triggered an unfixed bug in jdk when a service started https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7092821 As a result, all tasks in the thread pool are blocked, but other worker threads have been submitting tasks to the pool, because we used Executors. Fixed ThreadPool directly, so the memory burst finally.... Our final solution was to use ThreadPool Executor directly, which limited the size of BlockingQueue.

Copyright Statement: This article is the original article of the blogger. Please indicate the source for reprinting. Blog address: https://xindoo.blog.csdn.net/

Posted by Nakor on Sun, 25 Aug 2019 04:50:22 -0700