Multi-threading case (3) - timer, thread pool

1. Timer

Timer function: Agree on a time interval, and when the time is reached, execute a certain piece of code logic. In fact, it is an "alarm clock".

1.1 Using timers in the standard library

  • The standard library provides a Timer class. The core method of the Timer class is schedule.
  • The Timer class contains a scanning thread to observe whether any tasks have reached the execution time
  • schedule contains two parameters. The first parameter specifies the task code to be executed, and the second parameter specifies how long it will take to execute after (unit: milliseconds)
  • The TimerTask class inherits the Runnable interface, so it can override the run() method 
public class Test {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        },1000);
    }
}

This is because the thread inside the Timer prevents the end of the process. Multiple tasks can be arranged in the Timer. We should pay attention to this when we implement it below.

1.2 Implementation of timer

1. A scanning thread is needed in Timer to scan whether the task has expired and whether it can be executed.

2. Timer can schedule the execution of multiple tasks, and the execution time of each task is different, so we need to use a priority queue to store tasks and arrange these tasks in chronological order.

3. You also need to create a class to describe a task through the class (including the content and time of the task)

class MyTimer{
    private PriorityQueue<MyTimerTask> priorityQueue = new PriorityQueue<>();

    public MyTimer(){
        //扫描线程
        Thread t = new Thread(() -> {
            while(true){
                synchronized (this){//涉及到修改操作,加锁
                    try{
                        while(priorityQueue.isEmpty()){
                            this.wait();
                        }

                        MyTimerTask myTimerTask = priorityQueue.peek();
                        long curTime = System.currentTimeMillis();//得到当前时间

                        if(curTime >= myTimerTask.getTime()){//到达执行时间
                            myTimerTask.getRunnable().run();
                            priorityQueue.poll();
                        }else {//未到达执行时间
                            this.wait(myTimerTask.getTime() - curTime);//线程等待
                            //如果没有这句代码,就会出现忙等,类似于,一直在看表
                        }
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }

    public void schedule(Runnable runnable, long delay){
        synchronized (this){
            MyTimerTask myTimerTask = new MyTimerTask(runnable,delay);
            priorityQueue.offer(myTimerTask);//将任务放入队列
            this.notify();//如果当前队列为空,唤醒线程
        }
    }
}

class MyTimerTask implements Comparable<MyTimerTask>{
    private Runnable runnable;//任务内容
    private long time;//任务执行的具体时间

    public MyTimerTask(Runnable runnable, long delay){
        this.time = System.currentTimeMillis() + delay;
        this.runnable = runnable;
    }

    //得到任务执行的时间
    public long getTime(){
        return time;
    }

    //得到任务内容
    public Runnable getRunnable() {
        return runnable;
    }

    //重写比较方法,按照时间顺序从小到大排列
    @Override
    public int compareTo(MyTimerTask o) {
        return (int) (this.time - o.time);
    }
}

There are a few details to note about the above code:

1. 

Because wait() may also be interrupted by InterruptedException, if you use if, the queue is still null at this time, and no error can occur.

2.

Because if you use sleep, there is a scenario that cannot be established, that is, when we insert a task with an earlier execution time, the thread is still in sleep state. At this time, the newly inserted task will be delayed in execution, which does not conform to our logic.

If you use wait, the thread will find the first task to be executed again.

2. Thread pool

The thread pool can reduce the overhead of thread creation and destruction, which means it is suitable for scenarios where threads are frequently created and destroyed.

2.1 Using the thread pool in the standard library

Several ways for Executors to create thread pools

  • newFixedThreadPool: Create a thread pool with a fixed number of threads
  • newCachedThreadPool: Create a thread pool with a dynamically growing number of threads. After the thread is executed, it will not be destroyed immediately, but will be cached for a period of time.
  • newSingleThreadExecutor: Create a thread pool containing only a single thread.
  • newScheduledThreadPool: Execute the command after setting the delay time, or execute the command regularly. It is an advanced version of Timer.
public class Demo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        ExecutorService service1 = Executors.newFixedThreadPool(3);
        ExecutorService service2 = Executors.newSingleThreadExecutor();
        ExecutorService service3 = Executors.newScheduledThreadPool(2);

        service.submit(new Runnable() {//通过 ExecutorService.submit 可以注册一个任务到线程池
            @Override
            public void run() {
                System.out.println("111");
            }
        });
    }
}

Why not use the new method to create a thread pool here, but use the "factory mode" to implement it?

First, let’s understand what the factory pattern is. The factory pattern refers to using ordinary methods instead of constructors to complete initialization work. Because ordinary methods can be distinguished by method names, they are not subject to overloading rules. For example: our coordinates can use either the Cartesian coordinate system or the polar coordinate system. The parameters of these two construction methods are exactly the same. At this time, we need to use "factory mode" to initialize.

Executors are essentially an encapsulation of the ThreadPoolExecutor class. ThreadPoolExecutor provides more optional parameters, which can further refine the settings of thread pool behavior. Let's introduce the parameters of the construction method (very important!!!).

  • corePoolSize: The minimum number of threads in the thread pool
  • maximumPoolSize: The maximum number of threads in the thread pool
  • keepAliverTime: How long is the "fishing" time of the thread. If a thread has not worked for keepAliverTime times, the thread will be destroyed.
  • unit: unit of keepAliverTime
  • workQueue: blocking queue. If priority is required, set PriorityBlockingQueue. If there is a quantity limit, set ArrayBlockingQueue. If the number changes greatly, set LinkedBlockingQueue.
  • threadFactory: Factory mode, use factory mode to create threads and set some thread properties
  • handler: The rejection policy of the thread pool. There is an upper limit on the number of tasks a thread pool can accommodate. When the upper limit is reached, the processing method of adding threads continues. The four processing methods are as follows:

 Here is another classic interview question: If you need to set the number of threads to use a thread pool, what is the appropriate setting?

At this time, as long as you answer the specific number, it will be wrong, because there are two types of code executed by a thread:

1) CPU-intensive: the code mainly performs arithmetic operations/logical operations

2) IO intensive: the code mainly performs IO operations

Assume that all the code of a thread is CPU-intensive. At this time, the number of threads in the thread pool should not exceed N (number of cpu logical cores). If it is larger than N, efficiency cannot be improved.

Assuming that all the code of a thread is IO intensive and does not consume the CPU at this time, it can exceed N at this time.

Depending on the code, the number of threads in a thread pool is set differently, because we cannot know how much of a piece of code is CPU-intensive and how much is IO-intensive. The correct answer is: use experimental methods to perform performance tests on the program, and adjust the number of threads in the thread pool from time to time during the test to see which situation better meets the requirements.

 2.2 Simple implementation of thread pool

import java.util.concurrent.*;

class MyThreadPool{
    BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(4);

    public void submit(Runnable runnable){
        try {
            blockingQueue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    public MyThreadPool(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(()->{
                try {
                    Runnable a = blockingQueue.take();
                    a.run();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
    }
}

Guess you like

Origin blog.csdn.net/m0_74859835/article/details/132918361