Keywords:

Thread, Thread Pool, Single Thread, Multi-Thread, Benefits of Thread Pool, Thread Recycle, Creation Method, Core Parameters, underlying mechanism, Rejection Policy, Parameter Setting, Dynamic Monitoring, Thread Isolation

Threads and thread pool related knowledge, is the Java knowledge, learning or interview will meet this we will from the threads and processes, parallel and concurrent, single-threaded and multithreading, etc., have been explained to the thread pool, the benefits of the thread pool, creation method, the core of the important parameters, several important methods, the underlying implementation, refused to strategy, parameter setting, dynamic adjustment, Thread isolation and so on. The main outline is as follows (this article covers only threads; thread pools are covered in the next article) :

Processes and threads

Thread to process

When we talk about thread pools, we have to talk about threads. What is a thread?

thread(English: Thread) Yes
The operating systemIt’s able to do calculations
schedulingThe smallest unit of. It’s contained in
processIs,
processIn the actual operating unit.

So the question is, what is the process?

The process is the basic unit of protection and resource allocation in the operating system.

Is it a little muddled, the process can be felt visible? How exactly? Open the Windows task manager or Mac activity monitor, you can see that basically every open App is a process, but not necessarily, an application may have more than one process.

For example, the Typora below shows two processes, each of which is uniquely identified by a PID, which is also assigned by the system. In addition, each process can see how many threads are executing. For example, WeChat has 32 threads executing. Important word: A program is run after at least one process, a process can contain multiple threads.

<img src=”https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210508225417275.png” alt=”image-20210508225417275″ style=”zoom:50%;” />

Why a process?

Program, is the set of instructions, instructions set is simply a file, let the program run, in the execution of the program, is the process. A program is a static description of the text, while a process is a program execution activity, is dynamic. A process is a running program that has resources allocated by the computer.

It is impossible for us to have only one process for each computer, just as it is impossible for us to have only one city or department for the whole country. The computer is a giant, and its operation needs to be organized, so it needs to be divided into relatively independent units according to their functions and managed separately. Each process has its own responsibilities, but also has its own independent memory space, can not be mixed use, if all programs share a process will be messed up.

Each process has its own memory. Memory addresses are isolated between processes. The resources of the process, such as code snippets, data sets, heaps, etc., may also include some open files or semaphore, which are each process’s own data. At the same time, due to the isolation of processes, even if one process has a problem, it generally does not affect the use of other processes.

Processes In Linux systems, processes have a relatively important thing, called the process control block (PCB), only to understand:

A PCB is the unique identification of a process, which is realized by a linked list, for dynamic insertion and deletion. When a process is created, a PCB is generated, and when the process ends, the PCB is recovered. The PCB mainly includes the following information:

  • Process status
  • Process identification information
  • The timer
  • User-visible registers, control state registers, stack Pointers, etc.

How does the process switch?

Let’s start with a fact about computers: the CPU is super fast, so fast that only registers are nearly as fast as it is, but a lot of the time we need to read or write data from disk or memory, and these devices are too slow to be anywhere near as fast. (If not specified, the default is single-core CPU)

If a program/process has to write to a disk for some time, then the CPU is free, but no other program can be used, and the CPU will wait until the disk is written, then it will continue to execute. This is a waste of CPU is not a program, other applications also need to use. When you are not using the CPU, someone else will need it.

So CPU resources need to be scheduled. When program A is not in use, it can be cut off and used by program B, but when program A cuts back, how can it be sure that it can continue executing where it was before? The context has to be brought up at this point.

When program A (assuming A single process) abandons the CPU, the current context needs to be saved. What is the context? In other words, except for the CPU, the register or other state, just like the crime scene, need to take A picture, otherwise after the execution of other programs, how to know the next execution of program A, the previous execution of which step. To summarize: Save the execution state of the current program.

In general, the CPU will cache some data for the convenience of the next faster execution. Once the context switch is carried out, the original cache will be invalid and need to be cached again.

There are two types of scheduling (usually based on the thread dimension). The CPU time is divided into very small time slices:

  • Time-sharing: The use of each thread or process in turnCPU, the average time is allocated to each thread or process.
  • Preemptive scheduling: the higher priority thread/process immediately preempts the next time slice, and if the priority is the same, a process is selected at random.

The time slice is super short, the CPU is super fast, and it gives us a very silky feeling, like multitasking at the same time

Our current operating system, or any other system, is basically preemptive. Why?

Because if the use of time-sharing scheduling, it is difficult to achieve real-time response, when the background chat program in the network transmission, the allocated time slice has not been used up, then I click the browser, there is no way to real-time response. In addition, if the previous process dies but continues to occupy the CPU, subsequent tasks will never be executed.

Because the processing power of the CPU is super fast, even a single core CPU, running multiple programs, multiple processes, through preemptive scheduling, each program is like the use of the CPU is the same smooth. Processes can effectively increase CPU usage, but there is a certain cost when the process context switching.

What is the relationship between threads and processes?

If we have processes, why do we need threads and multiple applications? If we have n things to do with each application, why not use n processes?

Yes, but there’s no need.

Process is generally composed of program, data set and process control block. The same application generally needs to use the same data space. If an application has many processes, even if it has the ability to share data space, the process context switch will consume a lot of resources. (Typically an application will not have many processes, most have one, few have several.)

Process granularity is relatively large, each execution needs context switch, if the code segment A, B, C in the same program, do different things, if distributed to multiple processes to deal with, then each execution will have to switch process context. This is terrible. The task of an application is that a family lives in the same house (the same memory space). Is it necessary for each room to be treated as each household and register as a hukou at the police station?

Process disadvantages:

  • Information sharing is difficult and space is independent
  • Need to switchfork(), switching context, overhead
  • You can only do one thing at a time
  • If a process is blocked and waiting for data to arrive from the network, other tasks that do not depend on the data cannot be done

But people say, well, I have a lot of things to do with an application, you can’t just have one process, and everything is waiting for it to do? Wouldn’t that block?

Yes, a single process can’t handle the problem, so we divide the process into smaller, which is divided into many threads, the family, everyone has their own things to do, then each of us is a thread, the family is a process, isn’t it better?

A process is a time fragment that describes CPU time slice scheduling, but a thread is a much finer time fragment, and the granularity of the two is different. Threads can be called lightweight processes. In fact, thread is not the beginning of the concept, but with the development of computers, more and more requirements for multi-task context switching, then abstract out of the concept. $$process time = CPU loading time of program context + CPU execution time + CPU saving time of program context $$

$$ 线程时间段 = CPU加载线程上下文的时间 + CPU执行时间 + CPU保存线程上下文的时间$$ **最重要的是,进程切换上下文的时间远比线程切换上下文的时间成本要高**,如果是同一个进程的不同线程之间抢占到`CPU`,切换成本会比较低,因为他们**共享了进程的地址空间**,线程间的通信容易很多,通过共享进程级全局变量即可实现。 况且,现在多核的处理器,让不同进程在不同核上跑,进程内的线程在同个核上做切换,尽量减少(不可以避免)进程的上下文切换,或者让不同线程跑在不同的处理器上,进一步提高效率。 进程和线程的模型如下: ![image-20210509163642149](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509163642149.png) ### 线程和进程的区别或者优点 – 线程是程序执行的最小单位,进程是操作系统分配资源的最小单位。 – 一个应用可能多个进程,一个进程由一个或者多个线程组成 – 进程相互独立,通信或者沟通成本高,在同一个进程下的线程共享进程的内存等,相互之间沟通或者协作成本低。 – 线程切换上下文比进程切换上下文要快得多。 ## 线程有哪些状态 现在我们所说的是`Java`中的线程`Thread`,一个线程在一个给定的时间点,只能处于一种状态,这些状态都是虚拟机的状态,不能反映任何操作系统的线程状态,一共有六种/七种状态: – `NEW`:创建了线程对象,但是还没有调用`Start()`方法,还没有启动的线程处于这种状态。 – `Running`:运行状态,其实包含了两种状态,但是`Java`线程将就绪和运行中统称为可运行 – `Runnable`:就绪状态:创建对象后,调用了`start()`方法,该状态的线程还位于可运行线程池中,等待调度,获取`CPU`的使用权 – 只是有资格执行,不一定会执行 – `start()`之后进入就绪状态,`sleep()`结束或者`join()`结束,线程获得对象锁等都会进入该状态。 – `CPU`时间片结束或者主动调用`yield()`方法,也会进入该状态 – `Running` :获取到`CPU`的使用权(获得CPU时间片),变成运行中 – `BLOCKED` :阻塞,线程阻塞于锁,等待监视器锁,一般是`Synchronize`关键字修饰的方法或者代码块 – `WAITING` :进入该状态,需要等待其他线程通知(`notify`)或者中断,一个线程无限期地等待另一个线程。 – `TIMED_WAITING` :超时等待,在指定时间后自动唤醒,返回,不会一直等待 – `TERMINATED` :线程执行完毕,已经退出。如果已终止再调用start(),将会抛出`java.lang.IllegalThreadStateException`异常。 ![image-20210509224848865](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509224848865.png) 可以看到`Thread.java`里面有一个`State`枚举类,枚举了线程的各种状态(`Java`线程将**就绪**和**运行中**统称为**可运行**): “`Java public enum State { /** * 尚未启动的线程的线程状态。 */ NEW, /** * 可运行线程的线程状态,一个处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。 */ RUNNABLE, /** * 等待监视器锁而阻塞的线程的线程状态。 * 处于阻塞状态的线程正在等待一个监视器锁进入一个同步的块/方法,或者在调用Oject.wait()方法之后重新进入一个同步代码块 */ BLOCKED, /** * 等待线程的线程状态,线程由于调用其中一个线程而处于等待状态 */ WAITING, /** * 具有指定等待时间的等待线程的线程状态,线程由于调用其中一个线程而处于定时等待状态。 */ TIMED_WAITING, /** * 终止线程的线程状态,线程已经完成执行。 */ TERMINATED; } “` 除此之外,Thread类还有一些属性是和线程对象有关的: – long tid:线程序号 – char name[]:线程名称 – int priority:线程优先级 – boolean daemon:是否守护线程 – Runnable target:线程需要执行的方法 介绍一下上面图中讲解到线程的几个重要方法,它们都会导致线程的状态发生一些变化: – `Thread.sleep(long)`:调用之后,线程进入`TIMED_WAITING`状态,但是不会释放对象锁,到时间苏醒后进入`Runnable`就绪状态 – `Thread.yield()`:线程调用该方法,表示放弃获取的`CPU`时间片,但是不会释放锁资源,同样变成就绪状态,等待重新调度,不会阻塞,但是也不能保证一定会让出`CPU`,很可能又被重新选中。 – `thread.join(long)`:当前线程调用其他线程`thread`的`join()`方法,当前线程不会释放锁,会进入`WAITING`或者`TIMED_WAITING`状态,等待thread执行完毕或者时间到,当前线程进入就绪状态。 – `object.wait(long)`:当前线程调用对象的`wait()`方法,当前线程会释放获得的对象锁,进入等待队列,`WAITING`,等到时间到或者被唤醒。 – `object.notify()`:唤醒在该对象监视器上等待的线程,随机挑一个 – `object.notifyAll()`:唤醒在该对象监视器上等待的所有线程 ## 单线程和多线程 单线程,就是只有一条线程在执行任务,串行的执行,而多线程,则是多条线程同时执行任务,所谓同时,并不是一定真的同时,如果在单核的机器上,就是假同时,只是看起来同时,实际上是轮流占据CPU时间片。 下面的每一个格子是一个时间片(每一个时间片实际上超级无敌短),不同的线程其实可以抢占不同的时间片,获得执行权。**时间片分配的单位是线程,而不是进程,进程只是容器** ![image-20210511002923132](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511002923132.png) ### 如何启动一个线程 其实`Java`的`main()`方法本质上就启动了一个线程,但是**本质上不是只有一个线程**,看结果的 5 就大致知道,其实一共有 5 个线程,主线程是第 5 个,大多是**后台线程**: “`java public class Test { public static void main(String[] args) { System.out.println(Thread.currentThread().toString()); } } “` 执行结果: “`txt Thread[main,5,main] “` 可以看出上面的线程是`main`线程,但是要想创建出有别于`main`线程的方式,有四种: – 自定义类去实现`Runnable`接口 – 继承`Thread`类,重写`run()`方法 – 通过`Callable`和`FutureTask`创建线程 – 线程池直接启动(本质上不算是) #### 实现Runnable接口 “`java class MyThread implements Runnable{ @Override public void run(){ System.out.println(“Hello world”); } } public class Test { public static void main(String[] args) { Thread thread = new Thread(new MyThread()); thread.start(); System.out.println(“Main Thread”); } } “` 运行结果: “`txt Main Thread Hello world “` 如果看底层就可以看到,构造函数的时候,我们将`Runnable`的实现类对象传递进入,会将`Runnable`实现类对象保存下来: “`java public Thread(Runnable target) { this(null, target, “Thread-” + nextThreadNum(), 0); } “` 然后再调用`start()`方法的时候,会调用原生的`start0()`方法,原生方法是由`c`或者`c++`写的,这里看不到具体的实现: “`java public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { // 正式的调用native原生方法 start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } “` `Start0()`在底层确实调用了`run()`方法,并且不是直接调用的,而是启用了另外一个线程进行调用的,这一点在代码注释里面写得比较清楚,在这里我们就不展开讲,我们将关注点放到`run()`方法上,调用的就是刚刚那个`Runnable`实现类的对象的`run()`方法: “`java @Override public void run() { if (target != null) { target.run(); } } “` #### 继承Thread类 由于`Thread`类本身就实现了`Runnable`接口,所以我们只要继承它就可以了: “`java class Thread implements Runnable { } “` 继承之后重写run()方法即可: “`java class MyThread extends Thread{ @Override public void run(){ System.out.println(“Hello world”); } } public class Test { public static void main(String[] args) { Thread thread = new Thread(new MyThread()); thread.start(); System.out.println(“Main Thread”); } } “` 执行结果和上面的一样,其实两种方式本质上都是一样的,一个是实现了`Runnable`接口,另外一个是继承了实现了`Runnable`接口的`Thread`类。两种都没有返回值,因为`run()`方法的返回值是`void`。 #### Callable和FutureTask创建线程 要使用该方式,按照以下步骤: – 创建`Callable`接口的实现类,实现`call()`方法 – 创建`Callable`实现类的对象实例,用`FutureTask`包装Callable的实现类实例,包装成`FutureTask`的实例,`FutureTask`的实例封装了`Callable`对象的`Call()`方法的返回值 – 使用`FutureTask`对象作为`Thread`对象的`target`创建并启动线程,`FutureTask`实现了`RunnableFuture`,`RunnableFuture`继承了`Runnable` – 调用`FutureTask`对象的`get()`来获取子线程执行结束的返回值 “`java import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) throws Exception{ Callable<String> callable = new MyCallable<String>(); FutureTask<String> task = new FutureTask<String>(callable); Thread thread = new Thread(task); thread.start(); System.out.println(Thread.currentThread().getName()); System.out.println(task.get()); } } class MyCallable<String> implements Callable<String> { @Override public String call() throws Exception { System.out.println( Thread.currentThread().getName() + ” Callable Thread”); return (String) “Hello”; } } “` 执行结果: “`txt main Thread-0 Callable Thread Hello “` 其实这种方式本质上也是`Runnable`接口来实现的,只不过做了一系列的封装,但是不同的是,它可以实现返回值,如果我们期待一件事情可以通过另外一个线程来获取结果,但是可能需要消耗一些时间,比如异步网络请求,其实可以考虑这种方式。 `Callable`和`FutureTask`是后面才加入的功能,是为了适应多种并发场景,`Callable`和`Runnable`的区别如下: – `Callable` 定义方法是`call()`,`Runnable`定义的方法是`run()` – `Callable`的`call()`方法有返回值,`Runnable`的`run()`方法没有返回值 – `Callable`的`call()`方法可以抛出异常,`Runnable`的`run()`方法不能抛出异常 #### 线程池启动线程 本质上也是通过实现`Runnable`接口,然后放到线程池中进行执行: “`java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + ” : hello world”); } } public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { MyThread thread = new MyThread(); executorService.execute(thread); } executorService.shutdown(); } } “` 执行结果如下,可以看到五个核心线程一直在执行,没有规律,循环十次,但是并没有创建出十个线程,这和线程池的设计以及参数有关,后面会讲解: “`txt pool-1-thread-5 : hello world pool-1-thread-4 : hello world pool-1-thread-5 : hello world pool-1-thread-3 : hello world pool-1-thread-2 : hello world pool-1-thread-1 : hello world pool-1-thread-2 : hello world pool-1-thread-3 : hello world pool-1-thread-5 : hello world pool-1-thread-4 : hello world “` 总结一下,启动一个线程,其实本质上都离不开`Runnable`接口,不管是继承还是实现接口。 ### 多线程可能带来的问题 – 消耗资源:上下文切换,或者创建以及销毁线程,都是比较消耗资源的。 – 竞态条件:多线程访问或者修改同一个对象,假设自增操作`num++`,操作分为三步,读取`num`,`num`加1,写回`num`,并非原子操作,那么多个线程之间交叉运行,就会产生不如预期的结果。 – 内存的可见性:每个线程都有自己的内存(缓存),一般修改的值都放在自己线程的缓存上,到刷新至主内存有一定的时间,所以可能一个线程更新了,但是另外一个线程获取到的还是久的值,这就是不可见的问题。 – 执行顺序难预知:线程先`start()`不一定先执行,是由系统决定的,会导致共享的变量或者执行结果错乱 ## 并发与并行 并发是指两个或多个事件在同一时间间隔发生,比如在同`1s`中内计算机不仅计算`数据1`,同时也计算了`数据2`。但是两件事情可能在某一个时刻,不是真的同时进行,很可能是抢占时间片就执行,抢不到就别人执行,但是由于时间片很短,所以在1s中内,看似是同时执行完成了。当然前面说的是单核的机器,并发不是真的同时执行,但是多核的机器上,并发也可能是真的在同时执行,只是有可能,这个时候的并发也叫做并行。 ![image-20210511012516227](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511012516227.png) 并行是指在同一时刻,有多条指令在多个处理器上同时执行,真正的在同时执行。 ![image-20210511012723433](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511012723433.png) 如果是单核的机器,最多只能并发,不可能并行处理,只能把CPU运行时间分片,分配给各个线程执行,执行不同的线程任务的时候需要上下文切换。而多核机器,可以做到真的并行,同时在多个核上计算,运行。**并行操作一定是并发的,但是并发的操作不一定是并行的。** ### 关于作者 秦怀,公众号【**秦怀杂货店**】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指Offer,LeetCode等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。 [2020年我写了什么?](http://aphysia.cn/archives/2020) [开源编程笔记](https://damaer.github.io/Coding/#/) [150页的剑指Offer PDF领取](https://mp.weixin.qq.com/s?__biz=MzA3NTUwNzk0Mw==&tempkey=MTExNF9zZ2FPelJtWkNCdlZ6dTRuVThBSDdNc01JNFZuSTBrVlZWU0dCRk45dzlLVmx3SWx3NXlHVE5DWkRTSFBnNWVhRFV6RkNKOURjSmhUTExZeVp4QndwbEZ4Q2NfWUlzMzI2bDQzSm51TVJ4SE14QVhsUFIxSWJkcWtGQVhhLVVwZGRPZ0cwRHFDaGJvZ2pPeDM3NXdzcGF5N3A5bFdRaE9JU1Rpbi1Rfn4%3D&chksm=383018090f47911fd2458fe7c2ee89cbde7a7875dcba06d9f2e4daca191c7c0ab6409777f14d#rd)