Notes on "The Art of Java Concurrent Programming" (Part 2)

Thread status

A Java thread may be in six different states as shown in Table 4-1 during its running life cycle. At a given moment, a thread can only be in one of the states.

Insert image description here
Insert image description here

Daemon thread

The Daemon thread is a support thread because it is mainly used for background scheduling and support work in the program. This means that when there are no non-Daemon threads in a Java virtual machine, the Java virtual machine will exit. A thread can be set as a Daemon thread by callingThread.setDaemon(true).

Note: Daemon attributes need to be set before starting the thread, and cannot be set after the thread is started.

The Daemon thread is used to complete support work, but when the Java virtual machine exitsThe finally block in the Daemon thread will not necessarily be executed.

understanding interruptions

Interruption can be understood as an identification bit attribute of a thread, which indicates whether a running thread has been interrupted by other threads. Interruption is like another thread saying hello to the thread, and other threads interrupt it by calling the thread's intercept() method.

The thread responds by checking whether it has been interrupted. The thread uses the methodisInterrupted() to determine whether it has been interrupted. It can also call the static methodThread.interrupted() for The interrupt flag bit of the current thread is reset. If the thread is already in the terminal state, even if the thread has been interrupted, will still be returned when calling of the thread object. isInterrupted()false

As can be seen from the Java API, many methods that declare throwingInterruptedException (such as the Thread.sleep(long millis) method) are throwing. method will return . At this time, calling the InterruptedException, the Java virtual machine first clears the interrupt flag bit of the thread, and then throws InterruptedExceptionisInterrupted()false

Terminate thread safely

The thread terminates naturally:run() The method execution is completed or an unhandled exception is thrown, causing the thread to end prematurely.

The interrupt status is an identification bit of the thread, and the interrupt operation is a simple way of interaction between threads, and this interaction method is most suitable for canceling or stopping tasks. In addition to interrupts, you can also use a boolean variable to control whether you need to stop the task and terminate the thread.

Object monitor - monitor lock

Any thread that accesses Object must first obtain the monitor of Object. If the acquisition fails, the thread enters the synchronization queue and the thread status becomesBLOCKED. When the thread accessing Object (the thread that obtained the lock) releases the lock, the release operation wakes up the thread in the synchronization queue to retry acquiring the monitor. That is, at the same time, one thread can obtain the monitor of the object protected by synchronized.

Insert image description here

Wait/notify mechanism

The waiting/notification mechanism means that one thread A calls the wait() method of object O and enters the waiting state, while another thread B calls the method, thread A returns from the method of object O after receiving the notification, and then performs subsequent operations. The above two threads complete the interaction through the object O, and the relationship between and on the object is like a switch signal, which is used to complete the waiting party and the notifying party. interaction between them. notify()notifyAll()wait()wait()notify/notifyAll()

Insert image description here

Notes on use:

  • 1) Using the wait()、notify()、notifyAll() method requires locking the calling object first. (That is, the lock object should be the calling object)
  • 2) After calling the wait() method, the thread status changes from RUNNING (running) to . waiting queue (waiting), and puts the current thread into the object's WAITTING
  • 3) After the notify() or notifyAll() method is called, the waiting thread still does not return from wait() and needs to call < After the threads of /span> a> returns. , the waiting thread has a chance to exit from release the locknotify() and notifyAll()wait()
  • 4) The notify() method moves a waiting thread in the object's waiting queue from the waiting queue to the synchronization queue, while the notifyAll() method is Move all threads in the waiting queue to the synchronization queue, and the moved status changes from WAITING to BLOCKED< /span>.
  • 5) The prerequisite for returning from wait() is that the lock of the calling object is obtained.

Insert image description here

In Figure 4-3,WaitThreadfirstlyobtains the object's lock, and then calls the object's wait() method, thus giving up the lock and entering the object's waiting queue< /span> method to continue execution. And return from the acquires the lock again, After releasing the lock. blocked status becomes . At this time The status of to from method to move , And call the object's acquires the object's lock then releases the object's lock, . Since waiting stateWaitQueue, enter WaitThreadNotifyThreadnotify()WaitThreadWaitQueueSychronizedQueueWaitThreadNotifyThread WaitThreadwait()

Classic paradigm of wait/notify

The classic paradigm can be divided into two parts, respectively targeting: waiting party (consumer) and Notifying Party (Producer).

The waiting party follows the following principles:

  • 1) Obtain the lock of the object
  • 2) If the condition is not met, then call the object's wait() method, and still check the condition after being notified.
  • 3) Execute the corresponding logic when the conditions are met

The corresponding pseudocode is as follows:

synchronized(对象) {
    
    
	while (条件不满足) {
    
    
		对象.wait();
	}
	对应的处理逻辑
}

The notifying party follows the following principles:

  • 1) Obtain the lock of the object.
  • 2) Change the conditions.
  • 3) Notify all threads waiting on the object.

The corresponding pseudocode is as follows:

synchronized(对象) {
    
     
	改变条件;
	对象.notifyAll();
}

Use of Thread.join()

If a thread A executes thread.join(), the meaning is: when the previous thread A waits for the thread thread to terminate before returning from thread.join() Return.

线程 Thread 除了join()方法外还有join(long millis)join(long millis,int nanos)两个具备超时的方法,这两个方法表示:如果线程 thread 没有在指定时间内停止,那么线程 A 会从该超时方法返回

import java.util.concurrent.TimeUnit;

public class JoinDemo {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        // 获取当前线程信息
        Thread previousThread = Thread.currentThread();

        for (int i = 0; i < 10; i++) {
    
    
            Thread thread = new Thread(new Domino(previousThread));
            thread.start();
            previousThread = thread;
        }

        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    static class Domino implements Runnable {
    
    
        private Thread thread;

        public Domino(Thread thread) {
    
    
            this.thread = thread;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                thread.join();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

在代码清单4-13所示的例子中,创建了 10 个线程,编号 0~9,每个线程调用前一个线程的 join() 方法,也就是线程 0 结束了,线程 1 才能从 join() 方法中返回,而线程 0 需要等待 main 线程结束。

输出如下:

main terminate.
Thread-0 terminate.
Thread-1 terminate.
Thread-2 terminate.
Thread-3 terminate. 
Thread-4 terminate. 
Thread-5 terminate. 
Thread-6 terminate. 
Thread-7 terminate. 
Thread-8 terminate.
Thread-9 terminate.

从上述输出可以看到,每个线程终止的前提是前驱线程的终止,每个线程等待前驱线程终止后,才从 join() 方法返回,这里涉及了等待/通知机制(等待前驱线程结束,接收前驱线程结束通知)。

Insert image description here

当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。可以看到join()方法的逻辑结构和等待/通知经典范式一致,即加锁循环处理逻辑 3 个步骤。

Lock接口

Insert image description here
Insert image description here

队列同步器(AQS)

队列同步器AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作,并发包的作者期望它能够成为实现大部分同步需求的基础。

同步器的主要使用方式是继承子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的 3 个方法(getState()setState(int newState)compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLockReentrantReadWriteLockCountDownLatch等)。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

队列同步器的接口和示例

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法。

重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

  • (1) getState():获取当前同步状态。
  • (2) setState(int newState):设置当前同步状态。
  • (3) compareAndSetState(int expect,int update):使用 CAS 设置当前状态,该方法能够保证状态设置的原子性。

Insert image description here

实现自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法如下

Insert image description here

模板方法基本分为3类:独占式同步状态获取与释放、共享式同步状态获取与释放和查询同步队列中等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。

class Mutex implements Lock {
    
    
    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
    
    
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
    
    
            return getState() == 1;
        }

        // 当状态为0的时候获取锁
        public boolean tryAcquire(int acquires) {
    
    
            if (compareAndSetState(0, 1)) {
    
    
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
    
    
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 返回一个Condition,每个condition都包含了一个condition队列
        Condition newCondition() {
    
    
            return new ConditionObject();
        }
    }

    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();

    // 获取锁
    public void lock() {
    
    
        sync.acquire(1);
    }

    // 尝试非阻塞的获取锁
    public boolean tryLock() {
    
    
        return sync.tryAcquire(1);
    }

    // 释放锁
    public void unlock() {
    
    
        sync.release(1);
    }

    public Condition newCondition() {
    
    
        return sync.newCondition();
    }

    public boolean isLocked() {
    
    
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
    
    
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
    
    
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
    
    
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一条线程占有锁。Mutex定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquire)方法中,如果经过 CAS 设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int release)方法中只是将同步状态重置为0。用户使用Mutex时,并不会直接和内部同步器实现打交道。而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例:只需要在方法实现中调用同步器的模板方法acquire(int args)即可。当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义组件的门槛。

队列同步器的实现分析

同步队列

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态

同步队列中的节点(Node):用来保存获取同步状态失败的线程引用等待状态以及前驱和后继节点信息

Insert image description here

节点属性类型和名称以及描述如表5-5所示:

Insert image description here

节点是构成同步队列(即等待队列)的基础,同步器拥有首节点(head)尾节点(tail)没有成功获取同步状态的线程将会成为节点加入队列的尾部

同步队列的结构如下:

Insert image description here

同步器包含两个节点类型的引用,一个指向头节点,另一个一个指向尾节点。线程加入队列的过程必须保证线程安全,同步器提供了一个基于 CAS设置尾节点的方法:compareAndSetTail(Node expect,Node update),保证线程安全。

The process of the synchronizer adding nodes to the synchronization queue is shown in Figure 5-2.

Insert image description here

The synchronization queue followsFIFO, The first node is the node that successfully obtains the synchronization status ,When the thread of the first node releases the synchronization state, it will wake up the successor node, and the successor node will set itself as the first node when the synchronization state is successfully obtained.

The process is shown in Figure 5-3.

Insert image description here

In Figure 5-3, setting the first node is completed by the thread that successfully obtains the synchronization status. , Since only one thread can obtain the synchronization status, the method of setting the head node does not require CAS to guarantee it. It only needs to set the first node as the successor node of the original first node and disconnect the next reference of the original first node. Can.

After the node enters the queue, it enters aspin state. Each node (or each thread) is introspecting Observe that when the conditions are met and the synchronization status is obtained, you can exit from the spin process, otherwise it will still remain in the spin process (and will block the node's thread).

final boolean acquireQueued(final Node node, int arg) {
    
    
    boolean failed = true;
    try {
    
    
        boolean interrupted = false;
        for (;;) {
    
    
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
    
    
                setHead(node);
                p.next = null;
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
    
    
                interrupted = true;
            }
        }
    } finally {
    
    
        if (failed) {
    
    
            cancelAcquire(node);
        }
    }
}

In theacquireQueued(final Node node,int arg) method, the current thread tries to obtain the synchronization status in "Infinite Loop", and Only if the predecessor node is the head node can we try to obtain the synchronization status. Why is this? There are two reasons, as follows.

  • First, the head node is the node that successfully obtains the synchronization state. After the thread of the head node releases the synchronization state, it will wake up its successor node. After the thread of the successor node is awakened, it needs to check whether its predecessor node is the head node.
  • Second, maintain the FIFO principle of the synchronization queue.

In this method, the behavior of node spin to obtain synchronization status is shown in Figure 5-4.

Insert image description here

In Figure 5-4, the non-first node thread's predecessor node is dequeued or interrupted and returns from the waiting state. Then it checks whether its predecessor is the head node. If so, it tries to obtain the synchronization status. You can see The node and the node basically do not communicate with each other during the loop check process, but simply determine whether their predecessor is the head node, so that the release of the node complies with FIFO< /span>, and to facilitate the processing of premature notifications (premature notifications refer to threads whose predecessor nodes are not head nodes being awakened due to interruptions).

The exclusive synchronization status acquisition process is theacquire(int arg) method calling process, as shown in Figure 5-5.

Insert image description here

A brief summary of the exclusive synchronization status acquisition and release process:When acquiring the synchronization status, the synchronizer maintains a synchronization queue, and threads that fail to obtain the status will be added to the queue. And spin in the queue; the condition for moving out of the queue (or stopping spinning) is that the predecessor node is the head node and the synchronization status is successfully obtained. When releasing the synchronization state, the synchronizer calls the tryRelease(int arg) method to release the synchronization state, and then wakes up the successor nodes of the head node.

Condition interface

Any Java object has a set ofmonitor methods (defined on java.lang.Object), Mainly include wait(), wait(long timeout), notify() and notifyAll() methods, these methods are the same as synchronizedWith the synchronization keyword, you can achievewaiting/notification mode. The Condition interface also provides a monitor method similar to Object, which can be implemented in conjunction with Wait/notify mode, but there are still differences between the two in usage and functional characteristics. Lock

By comparing Object’s monitor method with Condition’s interface, you can learn more about the features of Condition a>

Insert image description here

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException {
    
    
    lock.lock();
    try {
    
    
        condition.await();
    } finally {
    
    
        lock.unlock();
    }
}

public void conditionSignal() throws InterruptedException {
    
    
    lock.lock();
    try {
    
    
        condition.signal();
    } finally {
    
    
        lock.unlock();
    }
}

As shown in the example, the Condition object is generally used as a member variable. After calling the await() method, the current thread will Release the lock and wait here, while other threads call the Condition object's signal() method to notify the current thread. The thread returns from the await() method and has acquired the lock before returning.

ConcurrentHashMap structure

ConcurrentHashMap is composed of Segment array structure and HashEntry array structure. Segment is a reentrant lock (implements ReentrantLock). HashEntry is used to store key-value pair data.

A ConcurrentHashMap contains an array of Segment. The structure of Segment is similar to HashMap. It is an array and linked list structure, but the number of Segment cannot be changed once initialized. .

aSegment contains an HashEntry array, each HashEntry is an element of a linked list structure, each a> array, you must first obtain the corresponding lock. array. When modifying the data of the Segment guards the elements in an HashEntryHashEntrySegment

Insert image description here
Insert image description here

What is blocking queue

BlockingQueue is a queue that supports two additional operations. These two additional operations support blocking insertion and removal methods.

  • Supports blocking insertion methods: When the queue is full, the queue blocks the thread inserting elements until the queue is full, such as the put() method.
  • Supports blocking removal methods: when the queue is empty, the thread that obtains the element will wait for the queue to become non-empty, such as the take() method.

Blocking queues are often used in producer-consumer scenarios. The producer is the thread that adds elements to the queue, and the consumer is the thread that takes elements from the queue. BlockingQueue is a container that stores production elements and is used by consumers to obtain elements.

When the blocking queue is unavailable, these two additional operations provide 4 processing methods, as shown in Table 6-1.

Insert image description here

  • Throws an exception: An exception is thrown if the attempted operation cannot be performed immediately. When the blocking queue is full, inserting elements into the queue will throw an exceptionIllegalStateException(“Queue full”). When the queue is empty, an NoSuchElementException exception will be thrown when retrieving elements from the queue.
  • Returns a special value: When inserting an element into the queue, it will return whether the element was successfully inserted. If successful, true will be returned. If it is a removal method, null is returned when the queue is empty.
  • Always blocked: If the attempted operation cannot be performed immediately, it will always be blocked or the response will be interrupted.
  • Timeout exit: If the attempted operation cannot be executed immediately, the method call will block until it can be executed, but the waiting time will not exceed the given value. Returns a specific value to tell whether the operation was successful, usually true / false.

Blocking queue in Java

JDK 7 provides 7 blocking queues, as follows:

  • ArrayBlockingQueue:A bounded blocking queue composed of an array structure
  • LinkedBlockingQueue:A blocking queue composed of a linked list structure (can be unbounded or bounded)
  • PriorityBlockingQueue:An unbounded blocking queue that supports priority sorting
  • DelayQueue:An unbounded blocking queue implemented using a priority queue
  • SynchronousQueue:A blocking queue that does not store elements
  • LinkedTransferQueue:An unbounded blocking queue composed of a linked list structure
  • LinkedBlockingDeque:A two-way blocking queue composed of a linked list structure

Implementation principle of blocking queue

Implemented using notification pattern. The so-called notification mode means that when the producer adds elements to a full queue, it will block the producer. When the consumer consumes an element in the queue, it will notify the producer that the current queue is available. By looking at the JDK source code, I found that ArrayBlockingQueue was implemented using Condition. The code is as follows.

private final Condition notFull;
private final Condition notEmpty;

public ArrayBlockingQueue(int capacity, boolean fair) {
    
    
    // 省略其他代码
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

public void put(E e) throws InterruptedException {
    
    
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    
    
        while (count == items.length) {
    
    
            notFull.await();
        }
        insert(e);
    } finally {
    
    
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    
    
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    
    
        while (count == 0) {
    
    
            notEmpty.await();
        }
        return extract();
    } finally {
    
    
        lock.unlock();
    }
}

private void insert(E x) {
    
    
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notFull.signal();
}

When we insert an element into the queue, if the queue is unavailable, the blocking producer is mainly implemented throughLockSupport.park(this);

Implementation principle of thread pool

After submitting a task to the thread pool, how does the thread pool handle the task?

Insert image description here

As can be seen from the figure, when a new task is submitted to the thread pool, the processing flow of the thread pool is as follows.

  • 1)The thread pool determines whether all threads in the core thread pool are executing tasks. If not, a new worker thread is created to perform the task. If all threads in the core thread pool are executing tasks, enter the next process.
  • 2)The thread pool determines whether the work queue is full. If the work queue is not full, newly submitted tasks are stored in this work queue. If the work queue is full, enter the next process.
  • 3)The thread pool determines whether all threads in the thread pool are in working status. If not, a new worker thread is created to perform the task. If it is full, it is handed over to the saturation strategy to handle this task.

ThreadPoolExecutorThe schematic diagram of the execution execute() method is shown in Figure 9-2.

Insert image description here

ThreadPoolExecutor execution execute method is divided into the following 4 situations.

  • 1) If there are less than corePoolSize currently running threads, create a new thread to perform the task (performing this step requires acquiring a global lock).
  • 2) If the running threads are equal to or more than corePoolSize, add the task to BlockingQueue.
  • 3) If the task cannot be added BlockingQueue (the queue is full), create a new thread to process the task (performing this step requires obtaining a global lock)
  • 4) If creating a new thread will cause the currently running threads to exceed maximumPoolSize, the task will be rejected and the RejectedExecutionHandler.rejectedExecution() method will be called.

ThreadPoolExecutor The overall design idea of ​​taking the above steps is to avoid acquiring global locks as much as possible when executing the execute() methodThreadPoolExecutor< a i=3>. After completes warm-up (the number of threads currently running is greater than or equal to corePoolSize), almost all execute() method calls are executed Step 2, and step 2 does not require acquiring the global lock.

Source code analysis: The above process analysis allows us to intuitively understand the working principle of the thread pool. Let us look at the source code to see how it is implemented. The method for the thread pool to perform tasks is as follows.

Insert image description here

Worker thread: When the thread pool creates a thread, it will encapsulate the thread into a worker thread Worker. After the Worker completes the task, it will also Loop through the tasks in the work queue to execute. We can see this from the method of the Worker class. run()

public void run() {
    
    
    try {
    
    
        Runnable task = firstTask;
        firstTask = null;
        while (task != null || (task = getTask()) != null) {
    
    
            runTask(task);
            task = null;
        }
    } finally {
    
    
        workerDone(this);
    }
}

ThreadPoolExecutorA schematic diagram of a thread executing a task is shown in the figure.

Insert image description here

There are two situations in which threads in the thread pool execute tasks, as follows.

1) When creating a thread in the execute() method, this thread will be allowed to perform the current task.
2) After this thread completes the task 1 in the above figure, it will repeatedly obtain tasks from BlockingQueue for execution.

Submit tasks to the thread pool

You can use two methods to submit tasks to the thread pool, namelyexecute() and submit () method.

The execute() method is used to submit tasks that do not require a return value, so it is impossible to determine whether the task is successfully executed by the thread pool.

It can be seen from the following code that the task input by theexecute() method is an instance of theRunnable class.

threadsPool.execute(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        // TODO Auto-generated method stub
    }
});

The submit() method is used to submit tasks that require return values. The thread pool will return an object of type Future, through this Future object You can determine whether the task was successfully executed, and you can pass Future.get() Method to get the return value, get() method will block the current thread until the task is completedget(long timeout,TimeUnit unit)< a i=13>, and using the method will block the current thread for a period of time and then return immediately. At this time, the task may not be completed.

Future<Object> future = executor.submit(task);
try {
    
    
    Object s = future.get();
} catch (InterruptedException e) {
    
    
    // 处理中断异常
} catch (ExecutionException e) {
    
    
    // 处理无法执行任务异常
} finally {
    
    
    // 关闭线程池
    executor.shutdown();
}

Close thread pool

The thread pool can be shut down by calling the thread pool's shutdown or shutdownNow methods. Their principle is to traverse the worker threads in the thread pool, and then call the thread's interrupt method one by one to interrupt the thread, so a task that cannot respond to interruption may never be terminated . But there are certain differences between them.shutdownNow first sets the status of the thread pool to STOP, then tries to stop all threads that are executing or suspending tasks, and returns the list of tasks waiting to be executed. shutdown just sets the state of the thread pool to SHUTDOWN state, and then interrupts all threads that are not executing tasks, while

The isShutdown method returns true whenever one of these two shutdown methods is called. When all tasks have been closed, the thread pool is successfully closed. At this time, calling the isTerminaed method will return true. As for which method we should call to close the thread pool, it should be determined by the characteristics of the task submitted to the thread pool. Usually shutdown is called to close the thread pool. If the task does not have to be completed, you can CallshutdownNow.

Properly configure the thread pool

In order to configure the thread pool reasonably, you must first analyze the task characteristics, which can be analyzed from the following perspectives:

  • Nature of tasks: CPU-intensive tasks, IO-intensive tasks, mixed tasks.
  • Task priority: high, medium, low.
  • Task execution time: long, medium, short.
  • Task dependencies: whether it depends on other system resources, such as database connections.

Tasks with different natures can be processed separately using thread pools of different sizes. Configure the smallest possible threads for CPU-intensive tasks, such as configuring a thread pool of Ncpu + 1 thread. For IO-intensive tasks, since threads are not always executing tasks, configure as many threads as possible, such as 2 * Ncpu. For mixed tasks, if it can be split, split it into a CPU-intensive task and an IO-intensive task, as long as these two If the difference in task execution time is not too big, then the throughput rate of decomposed execution is higher than the throughput rate of serial execution. If the difference in execution time of the two tasks is too big, there is no need to decompose it. We can get the CPU number of the current device through the Runtime.getRuntime().availableProcessors() method.

Tasks with different priorities can be processed using the priority queue PriorityBlockingQueue. It allows high-priority tasks to be executed first. It should be noted that if there are always high-priority tasks submitted to the queue, then low-priority tasks may never be executed.

Tasks with different execution times can be handed over to thread pools of different sizes for processing, or a priority queue can be used to let tasks with short execution times be executed first.

It is recommended to use bounded queues. Bounded queues can increase the stability and early warning capabilities of the system. They can be set larger as needed, such as several thousand.

Thread pool monitoring

Monitor through the parameters provided by the thread pool. There are some properties in the thread pool that can be used when monitoring the thread pool.

  • taskCount: The number of tasks that the thread pool needs to execute.
  • completedTaskCount: The number of tasks that the thread pool has completed during operation. Less than or equal to taskCount.
  • largestPoolSize: The maximum number of threads the thread pool has ever created. Through this data, you can know whether the thread pool is full. If it is equal to the maximum size of the thread pool, it means that the thread pool is full.
  • getPoolSize: The number of threads in the thread pool. If the thread pool is not destroyed, the threads in the pool will not be automatically destroyed, so the size will only increase but not decrease.
  • getActiveCount: Get the number of active threads.

Monitor by extending the thread pool. By inheriting the thread pool and overriding the thread pool's beforeExecute, afterExecute and terminated methods, we can perform tasks before and after execution. Do something before closing the thread pool. Such as monitoring the average execution time, maximum execution time and minimum execution time of tasks. These methods are empty methods in the thread pool. Such as:

protected void beforeExecute(Thread t,Runnable r) {
    
     } 

Implementation of FutureTask

FutureTaskThe implementation of is based on AbstractQueuedSynchronizer (hereinafter referred to as AQS). Many blockable classes in java.util.concurrent are implemented based on AQS. AQS is a synchronization framework that provides a general mechanism to atomically manage synchronization status, block and wake up threads, and maintain a queue of blocked threads. AQS is widely used in JDK 6, and the synchronizer is implemented based on AQS Includes: ReentrantLock, Semaphore, ReentrantReadWriteLock, CountDownLatch and FutureTask.

Each synchronizer implemented based onAQS will contain two types of operations, as follows:

  • At least oneacquire operation. This operation blocks the calling thread unless/untilthe state of AQS allows this The thread continues execution. The FutureTask's acquire operation is get()/get(long timeout,TimeUnit unit) method call.
  • At least onerelease operation. This operation changes the state of AQS. The changed state allows one or more blocked threads to be unblocked. FutureTask's release operation includes run() method and cancel(…) method.

Based on the principle of "Composition takes precedence over inheritance", FutureTaskdeclared an internal private class a>subclass. All will be delegated to this internal, calling all public methods of AQSSync, which is a subclass inherited from FutureTaskSync

Insert image description here

FutureTask.get()The method will call the AQS.acquireSharedInterruptibly() method. The execution process of this method is as follows:

  • 1) Call the AQS.acquireSharedInterruptibly(int arg) method. This method will first call back the method implemented in the subclass Sync. Determine whether the acquire operation can succeed. acquireThe conditions for the operation to succeed are: is in execution completion statusor has Cancellation status , and is not . tryAcquireShared()stateRANCANCELLEDrunnernull
  • 2) If successful, theget() method returns immediately. If it fails, go to the thread waiting queue and wait for other threads to performrelease operations.
  • 3) When other threads perform release operations (such as FutureTask.run() or FutureTask.cancel(…)) After waking up the current thread, if the current thread executes againtryAcquireShared(), it will return a positive value1, and the current thread will leave the thread waiting queue and wake up its successor thread.
  • 4) Finally return the calculated result or throw an exception.

FutureTask.run()The execution process is as follows:

  • 1) Perform the task specified in the constructor (Callable.call()).
  • 2) Update the synchronization status atomically (call AQS.compareAndSetState(int expect,int update), set state to the execution completion status RAN) . If this atomic operation succeeds, set the value of the variable result representing the calculation result to the return value of Callable.call(), and then call AQS.releaseShared(int arg).
  • 3)AQS.releaseShared(int arg) will first call back the implemented in the subclassSync to execute< a i=4>release operation (set the thread running the task to , and then return );, and then wake up the thread waiting for the first thread in the queue. tryReleaseShared(arg)runnernulltrueAQS.releaseShared(int arg)
  • 4) For tuning FutureTask.done().

When executing the FutureTask.get() method, ifFutureTask is not in the execution completion stateRAN or in the canceled state method, the first thread in the thread waiting queue will be awakened. method or the . When a thread executes the AQSCANCELLED, the current execution thread will wait in the thread waiting queue of FutureTask.run()FutureTask.cancel(…)

FutureTaskThe usage Demo reference is as follows:

/**
* Callable接口实例 计算累加值大小并返回
*/
public class CallableDemo implements Callable<Integer> {
    
    
    public final static String TAG = CallableDemo.class.getSimpleName();
    private int sum;
    
    @Override
    public Integer call() throws Exception {
    
    
        Log.e(TAG, "call: Callable子线程开始计算啦!");
        Thread.sleep(2000); 
        for(int i = 0 ; i < 5000; i++){
    
    
            sum = sum + i;
        }
        Log.e(TAG, "call: Callable子线程计算结束!");
        return sum;
    }
}
public class CallableTest {
    
    

    public static void main(String[] args) {
    
    
        //第一种使用方式
//        //创建线程池
//        ExecutorService es = Executors.newSingleThreadExecutor();
//        //创建Callable对象任务
//        CallableDemo calTask = new CallableDemo();
//        //提交任务并获取执行结果
//        Future<Integer> futureTask = es.submit(calTask);
//        //关闭线程池
//        es.shutdown();

        //第二种使用方式
        //创建线程池
        ExecutorService es = Executors.newSingleThreadExecutor();
        //创建Callable对象任务
        CallableDemo calTask = new CallableDemo();
        //创建FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(calTask);
        //执行任务
        es.submit(futureTask);
        //关闭线程池
        es.shutdown();
        try {
    
    
            Thread.sleep(2000);
            Log.e(CallableDemo.TAG, "main: 主线程在执行其他任务");
            if (futureTask.get() != null) {
    
    
                //输出获取到的结果
                Log.e(CallableDemo.TAG, "futureTask.get()-->" + futureTask.get());
            } else {
    
    
                Log.e(CallableDemo.TAG, "futureTask.get()未获取到结果");
            }
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        Log.e(CallableDemo.TAG, "主线程执行完成");
    }
}

Output:

Insert image description here

Thread pool and production messager pattern

The thread pool class in Java is actually an implementation of the producer and consumer patterns, but I think its implementation is more clever. The producer throws the task to the thread pool, and the thread pool creates threads and processes the tasks. If the number of tasks to be run is greater than the basic number of threads in the thread pool, the task is thrown into the blocking queue. This kind of This approach is obviously much smarter than using only one blocking queue to implement the producer and consumer model, because the consumer can process it directly, which is faster, and the producer stores it first and the consumer retrieves it later. Obviously slower.

Our system can also use thread pools to implement multiple producers and consumers. For example,Create N Java thread pools of different sizes to handle tasks of different natures. For example, after thread pool 1 reads the data into the memory, the threads in thread pool 2 will continue to process the compressed data. . Thread pool 1 mainly handles IO-intensive tasks, and thread pool 2 handles CPU-intensive tasks.

Readers can think about which scenarios can use the producer-consumer model in their daily work. I believe there should be many such scenarios, especially scenarios that require a long processing time, such as< a i=1>Upload attachments and process. After the user uploads the file to the system, the system throws the file into the queue, then immediately returns to tell the user that the upload is successful, and finally the consumer takes it out from the queue. File processing.

Interview question: Will core threads be created immediately after the thread pool is created?

  • No. When ThreadPoolExecutor is just created, the thread in the thread pool will not be started immediately, but will not be started until a task is submitted a> is called to start the core thread in advance (warm up). /Worker thread, unless prestartCoreThreadprestartAllCoreThreads

To warm up the thread pool, you can call the following two methods:

  • Start all:prestartAllCoreThreads()
    Insert image description here
  • Start only one:prestartCoreThread()
    Insert image description here

When the worker thread is created successfully, that is, Worker the object has been created. At this time, you need to start the worker thread and let the thread start working. < The a i=2> object is associated with a , so if you want to start the working thread, you only need to start the thread through . WorkerThreadworker.thread.start()

After is started, the Worker object's run method will be executed, because Worker is implemented Runnable interface, so essentiallyWorker is also a thread.

After is opened through threadstart, the method of Runnable will be called. In In the method of the object, the method is called, that is, the current object is passed to the method, so that He does it. runworkerrunrunWorker(this)runWorker

Question 2: Will the number of core threads be recycled? What settings are required?

  • The number of core threads will not be recycled by default. If you need to recycle the number of core threads, you need to call the following method:
    Insert image description here
    allowCoreThreadTimeOut This value defaults to false.
    Insert image description here

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/134842019