Multithreading

primary coverage

Learning objectives

Chapter 1 multithreading

Before, the programs we learned were executed from top to bottom without jump statements. Now we want to design a program to listen to music while playing games. How to design it?

To solve the above problems, we have to use multi - process or multi - thread

1.1 concurrency and parallelism

In the operating system, multiple programs are installed. Concurrency refers to that multiple programs run at the same time in a period of time. In a single CPU system, only one program can be executed at each time, that is, these programs run alternately in time-sharing, which just gives people the feeling that they run at the same time, because the time-sharing alternating running time is very short.

In multiple CPU systems, Then these programs that can be executed concurrently can be allocated to multiple processors (CPU) to realize multi-task parallel execution, that is, each processor is used to process a program that can be executed concurrently, so that multiple programs can be executed at the same time. At present, the multi-core CPU in the computer market is multi-core processor. The more cores, the more programs processed in parallel, can greatly improve the efficiency of computer operation.

1.2 threads and processes

We can right click the task bar at the bottom of the computer - > open the task manager to view the progress of the current task:

process

thread

Thread scheduling:

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在一边开着网页一边听歌,还挂着qq微信。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

实际上,cpu(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于cpu的一个核而言,某个时刻,只能执行一个线程,而 cpu的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让cpu的使用率更高。

1.3 create thread class

Java uses Java Lang. thread class represents threads. All thread objects must be instances of thread class or its subclasses. The role of each thread is to complete a certain task. In fact, it is to execute a program flow, that is, a piece of code executed in sequence. Java uses thread executors to represent this program flow. The steps to create and start a multithread by inheriting the thread class in Java are as follows:

The code is as follows:

Test class:

public class Demo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}

Custom thread class:

public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}

Chapter 2 thread

2.1 multithreading principle

First draw a multithreaded execution sequence diagram to reflect the execution flow of multithreaded programs.

The code is as follows:

Custom thread class:

public class MyThread extends Thread{ 
/*
* 利用继承中的特点 
* 将线程名称传递 进行设置
*/
public MyThread(String name){ 
           super(name); 
}
/*
* 重写run方法
* 定义线程要执行的代码 
*/ 
public void run(){
 for (int i = 0; i < 20; i++) {
             //getName()方法 来自父亲
             System.out.println(getName()+i);
             }
        }
  } 

Test class:

public class Demo {
	public static void main(String[] args) {
    	System.out.println("这里是main线程" );
    	MyThread mt = new MyThread("小强");
    	mt.start();//开启了一个新的线程
    	for (int i = 0; i < 20; i++){
         	System.out.println("旺财:"+i);
     	}
	}
}

flow chart:

When the program starts and runs main, the Java virtual machine starts a process, and the main thread main is created when main() is called. With the start method of the object calling MT, another new thread is started, so that the whole application runs under multithreading.

Through this figure, we can clearly see the multi-threaded execution process, so why can we complete concurrent execution? Let's talk about the principle again.

How does multithreading run in memory? The above procedures are illustrated as an example:

During multi-threaded execution, in stack memory, in fact, each execution thread has its own stack memory space. Press stack and spring stack of method.

When the task of executing the thread ends, the thread is automatically released in the stack memory. But when all the execution threads end, the process ends.

2.2 thread class

In the above content, we can complete the most basic thread startup, so we use Java Lang. thread class, which defines some methods related to threads in the API, as follows:

Construction method:

Common methods:

After reading the API, we know that there are two ways to create threads: one is to inherit the thread class, and the other is to implement the runnable interface. Mode 1 has been completed above. Next, we will explain the implementation of mode 2.

2.3 thread creation mode 2

Using Java Lang. runnable is also a very common method. We only need to rewrite the run method.

The steps are as follows:

The code is as follows:

public class MyRunnable implements Runnable {
	@Override
	public  void run{
		for (int i = 0; i < 20; i++)  {
			System.out.println(Thread.currentThread().getName()+ " "+i );
		}
	}
}

public class Demo {
	public static void main(String[] args) {       
    	//创建自定义类对象  线程任务对象
		MyRunnable mr = new MyRunnable();
  		//创建线程对象
  		Thread t = new Thread(mr,"小强");
  		t.start();
  		for (int i = 0; i < 20; i++){
        	System.out.println("旺财" + i);
	}
}

By implementing the runnable interface, this class has the characteristics of multi-threaded class. The run () method is an execution target of a multithreaded program. All multithreaded code is in the run method. The thread class is actually a class that implements the runnable interface.

When you start multithreading, you need to construct the object through the Thread class construction method Thread (Runnable target) and then call the start () method of the Thread object to run the multithreaded code.

In fact, all multithreaded code is run by running the start () method of thread. Therefore, whether you inherit the thread class or implement the runnable interface to realize multithreading, and finally control the thread through the API of the thread object, familiarity with the API of the thread class is the basis of multithreading programming.

2.4 difference between thread and runnable

If a class inherits thread, it is not suitable for resource sharing. However, if the runable interface is implemented, it is easy to realize resource sharing.

Summary:

Advantages of implementing runnable interface over inheriting thread class:

2.5 creating threads by anonymous inner classes

Using the anonymous internal class method of threads, it is convenient for each thread to perform different thread task operations.

Implement the runnable interface by using anonymous internal classes, and re the run method in the runnable interface:

public class NoNameInnerClassThread {     
	public static void main(String[] args) {
	//       new Runnable(){
	//       	public void run(){
	//               	for (int i = 0; i < 20; i++) {
	//                  	System.out.println("张宇:"+i); 
	//                  }
	//          } 
	//      }; //‐‐‐这个整体 相当于new MyRunnable()
		Runnable r = new Runnable(){ 
       		public void run(){
        		for (int i = 0; i < 20; i++) {                    
            		System.out.println("张宇:"+i);
         		}
     		} 
		};

		new Thread(r).start();
		for (int i =0; i < 20; i++){
          	System.out.println("费玉清:" +i); 
        }
    }
}

Chapter 3 thread safety

3.1 thread safety

If there are multiple threads running at the same time, these threads may run this code at the same time. The result of each run of the program is the same as that of a single thread, and the values of other variables are the same as expected, which is thread safe.

We demonstrate thread safety through a case:

The cinema sells tickets. We simulate the ticket selling process of the cinema. Assuming that the movie to be played is "huluwa vs. Altman", there are 100 seats in this movie (this movie can only sell 100 tickets).

Let's simulate the ticket window of the cinema and realize that multiple windows sell the movie tickets of "huluwa vs. Altman" at the same time (multiple windows sell these 100 tickets together).

If windows are needed, thread objects are used to simulate; The runnable interface subclass is required to simulate

Simulation ticket:

public class Ticket implements Runnable {
	private int ticket = 100;
	// 执行卖票操作
	@Override
	public void run() {
    	//每个窗口卖票的操作
    	//窗口 永远开启
    	while(true) {
        	if (ticket > 0) {
            	//有票可以卖则出票操作
            	//使用sleep模拟出票时间
            	try{
                	Thread.sleep(100);
            	} catch (InterruptedException e) {
                	e.printStackTrace();
            	}
            	//获取当前线程对象的名字
            	String name = Thread.currentThread().getName();
            	System.out.println(name + "正在卖:" + ticket--);
        	}
    	}
	}
}

Test class:

public class Demo {
	public static void main(String[] args) {
    	//创建线程对象任务
    	Ticket ticket = new Ticket();
    	//创建三个窗口对象
    	Thread t1 = new Thread(ticket,"窗口1");
    	Thread t2 = new Thread(ticket,"窗口2");
    	Thread t3 = new Thread(ticket,"窗口3");

    	//同时卖票
    	t1.start();
    	t2.start();
    	t3.start();
	}
}

Some of the results are as follows:

Two problems were found in the program:

For this problem, the number of votes in several windows (threads) is out of sync. This problem is called thread insecurity.

3.2 thread synchronization

When we use multiple threads to access the same resource, and multiple threads have write operations on the resource, thread safety problems are easy to occur.

To solve the security problem of multi-threaded concurrent access to a resource, that is, to solve the problem of duplicate tickets and non-existent tickets, Java provides a synchronized mechanism to solve it.

According to the case description:

窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺cpu资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象

In order to ensure that each thread can normally perform atomic operations, Java introduces thread synchronization mechanism.

So how to use it? There are three ways to complete synchronization:

3.3 synchronization code block

Format:

synchronized(同步锁){
	需要同步操作的代码
}

Synchronous lock:

The synchronization lock of an object is just a concept. You can imagine that a lock is marked on the object.

Use synchronized code blocks to resolve Code:

public class Ticket implements Runnable {
	private int ticket = 100;
	Object lock = new Object();
	// 执行卖票操作
	@Override
	public void run() {
    	//每个窗口卖票的操作
    	//窗口 永远开启
    	while(true) {
			synchronized(lock) {
				if (ticket > 0) {
            	//有票可以卖则出票操作
            	//使用sleep模拟出票时间
            	try{
                	Thread.sleep(100);
            	} catch (InterruptedException e) {
                	e.printStackTrace();
            	}
            	//获取当前线程对象的名字
            	String name = Thread.currentThread().getName();
            	System.out.println(name + "正在卖:" + ticket--);
        		}
			}
    	}
	}
}

When the synchronous code block is used, the above thread safety problem is solved.

3.4 synchronization method

Format:

public synchronized void method() {
	//可能会产生线程安全问题的代码
}

The synchronization method code is as follows:

public class Ticket implements Runnable {
	private int ticket = 100;
	// 执行卖票操作
	@Override
	public void run() {
    	//每个窗口卖票的操作
    	//窗口 永远开启
    	while(true) {
        	sellTicket();
    	}
	}
	//锁对象 是 谁调用这个方法 就是谁
	// 隐含 锁对象 就是 this
	public synchronized void sellTicket() {
		if (ticket > 0) {
            //有票可以卖则出票操作
            //使用sleep模拟出票时间
           	try{
               	Thread.sleep(100);
           	} catch (InterruptedException e) {
               	e.printStackTrace();
           	}
           	//获取当前线程对象的名字
           	String name = Thread.currentThread().getName();
           	System.out.println(name + "正在卖:" + ticket--);
       	}
	}
}

3.5 lock lock

java. util. concurrent. locks. Lock mechanism provides a wider range of locking operations than synchronized code blocks and synchronized methods. Synchronized code blocks / synchronized methods have the functions of lock. In addition, lock is more powerful and object-oriented.

Lock lock is also known as synchronous lock. The locking and releasing methods are as follows:

Use the following:

public class Ticket implements Runnable {
	private int ticket = 100;
	Lock lock = new reentrantlock();
	// 执行卖票操作
	@Override
	public void run() {
    	//每个窗口卖票的操作
    	//窗口 永远开启
    	while(true) {
			lock.lock();
        	if (ticket > 0) {
            	//有票可以卖则出票操作
            	//使用sleep模拟出票时间
            	try{
                	Thread.sleep(100);
            	} catch (InterruptedException e) {
                	e.printStackTrace();
            	}
            	//获取当前线程对象的名字
            	String name = Thread.currentThread().getName();
            	System.out.println(name + "正在卖:" + ticket--);
        	}
			lock.unlock();
    	}
	}
}

Chapter 4 thread status

4.1 thread status overview

When a thread is created and started, it neither enters the execution state as soon as it is started, nor is it always in the execution state. How many states are there in the life cycle of a thread? In the API, Java lang.Thread. State this enumeration gives six thread states:

The conditions for the occurrence of each thread state are listed here. Each state will be analyzed in detail below

We don't need to study the implementation principle of these states. We just need to know that such states exist in thread operation. How do we understand these States? It's easy to understand the creation and termination. Let's study the transformation of threads from runnable state to non running state.

4.2 timed waiting

Timed waiting is described in the API as: a thread that is waiting for another thread to perform a (wake-up) action for a limited time is in this state. It is mysterious to understand this sentence alone. In fact, we have touched this state in previous operations. Where is it?

In the case of selling tickets, in order to reduce the problem that the thread executes too fast and the phenomenon is not obvious, we add a sleep statement to the run method, which forces the currently executing thread to sleep (suspend execution) to "slow down the thread".

In fact, after we call the sleep method, the currently executing thread enters the "sleep state", which is actually the so-called timed waiting. Then we can deepen our understanding of this state through a case.

Implement a counter, count to 100, pause for 1 second between each number, and output a string every 10 numbers

code:

public class MyThread extends Thread{
	public void run() {
    	for (int i = 0; i < 100; i++) {
        	if (i % 10 == 0) {
            	System.out.println("--------" + i);
        	}
        	System.out.print(i);
        	try{
            	Thread.sleep(1000);
            	System.out.print("线程睡眠1s!\n");
        	} catch (InterruptedException e) {
            	e.printStackTrace();
        	}
    	}
	}

	public static void main(String[] args) {
    	new MyThread().start();
	}
}

Through the case, we can find that the sleep method is still very simple to use. We need to remember the following:

Timed waiting thread state diagram:

4.3 blocked

The blocked state is described in the API as: a thread that is blocking and waiting for a monitor lock (lock object) is in this state.

We have learned the synchronization mechanism, so this state is very easy to understand. For example, the same lock is used in the code of thread a and thread B. if thread a obtains the lock and thread a enters the runnable state, thread B enters the blocked lock blocking state.

This is from the runnable state to the blocked state. In addition, the waiting and time waiting States will also enter the blocking state in some cases, and this part will lead you to understand as an expanded knowledge point.

Blocked thread state diagram

4.4 waiting

The watching state is described in the API as: a thread that is waiting for another thread to perform a special (wake-up) action indefinitely is in this state.

So have we encountered this state before? Answer is no, but it does not prevent us from having a simple and in-depth understanding. Let's learn from a piece of code:

public class WaitingTest { 
	public static Object obj = new Object(); 

	public static void main(String[] args) { 
	// 演示waiting 
    	new Thread(new Runnable() {
 			@Override 
        	public void run() { 
            	while (true){ 
               		synchronized (obj){
                    	try {
                        	System.out.println( Thread.currentThread().getName() +"=== 获取到锁对象,调用wait方法,进入waiting状态,释放锁对象"); 
                            obj.wait(); //无限等待      
                            //obj.wait(5000); //计时等待,5秒 时间到,自动醒来 
                            } catch (InterruptedException e) { 
                              e.printStackTrace(); 
                            }
                            System.out.println( Thread.currentThread().getName() + "=== 从waiting状 态醒来,获取到锁对象,继续执行了");
                          }
                        }
                   } 
         },"等待线程").start(); 

         new Thread(new Runnable() { 
         	@Override 
         	public void run() {
          	// while (true){ //每隔3秒 唤醒一次
           		try {
           			System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 等待3秒钟"); 
           			Thread.sleep(3000); 
           		} catch (InterruptedException e) { 
           			e.printStackTrace(); 
           		}
           		
				synchronized (obj){ 
           			System.out.println( Thread.currentThread().getName() +"‐‐‐‐‐ 获取到锁对 象,调用notify方法,释放锁对象"); 
           			obj.notify(); 
           		} 
			}
		// } 
		},"唤醒线程").start(); 
	} 
}

Through the above case, we will find that an object that calls an object The thread of the wait method will wait for another thread to call the object of this object Notify() method or object Notifyall() method.

In fact, the waiting state is not the operation of a thread. It reflects the communication between multiple threads. It can be understood as the cooperative relationship between multiple threads. Multiple threads will strive for locks, and there is a cooperative relationship between them. Just like you and your colleagues in the company, you may have competition for promotion, but more often you work together to complete some tasks.

When multiple threads cooperate, such as A or B thread, if the A thread calls the wait () method in the Runnable (runtime) state, then the A thread enters Waiting (infinite wait) state, and at the same time loses the synchronization lock. If this time the B thread gets the synchronization lock and invokes the notify () method in the running state, it will wake the waiting thread of A thread. Note: if the lock object is obtained, thread a will enter the runnable state after waking up; if the lock object is not obtained, it will enter the blocked state.

Waiting thread state diagram

4.5 supplementary knowledge points

So far, we have a basic understanding of thread status. For more details, see the following figure:

Chapter V waiting wake-up mechanism

5.1 inter thread communication

Concept: multiple threads are processing the same resource, but the processing actions (thread tasks) are different.

For example, thread a is used to generate steamed stuffed buns and thread B is used to eat steamed stuffed buns. Steamed stuffed buns can be understood as the same resource. The actions processed by thread a and thread B are production and consumption, so there is a thread communication problem between thread a and thread B.

Why handle inter thread communication:

When multiple threads execute concurrently, by default, the CPU switches threads randomly. When we need multiple threads to complete a task together, and we want them to execute regularly, some coordinated communication is needed between multiple threads to help us operate a piece of data together.

How to ensure inter thread communication and make effective use of resources:

When multiple threads are processing the same resource and different tasks, thread communication is needed to help solve the use or operation of the same variable between threads. That is, multiple threads avoid contention for the same shared variable when operating the same data. That is, we need to use certain means to make each thread make effective use of resources. This means is the waiting wake-up mechanism.

5.2 waiting for wake-up mechanism

What is the waiting wake-up mechanism

This is a collaboration mechanism between multiple threads. When it comes to threads, we often think of the race between threads, such as competing for locks, but this is not the whole story. There will also be a cooperation mechanism between threads. For example, in the company, you and your colleagues may have competition for promotion, but more often, you work together to complete some tasks.

After a thread performs a specified operation, it enters the wait state (wait ()) and waits for other threads to wake up after executing their specified code (notify ()); When there are multiple threads waiting, you can use notifyall() to wake up all waiting threads if necessary.

Wait / notify is a cooperative mechanism between threads.

Methods in waiting for wake-up

The wake-up waiting mechanism is used to solve the problem of inter thread communication. The meanings of the three methods used are as follows:

Details of calling wait and notify methods

5.3 producer and consumer issues

The waiting wake-up mechanism is actually a classic problem of "producers and consumers".

Take the production of steamed stuffed buns and the consumption of steamed stuffed buns as an example. How can the waiting wake-up mechanism make effective use of resources

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

Code demonstration:

Package sub resource class:

public class BaoZi {
     String  pier ;
     String  xianer ;
     boolean  flag = false ;//包子资源 是否存在  包子资源状态
}

Feeding thread class:

public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//没包子
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();
            }
        }
    }
}

Package thread class:

public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子资源  存在
                    try {

                        bz.wait();

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 没有包子  造包子
                System.out.println("包子铺开始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大葱
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程 (吃货)
                bz.notify();
            }
        }
    }
}

Test class:

public class Demo {
    public static void main(String[] args) {
        //等待唤醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃货",bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz);

        ch.start();
        bzp.start();
    }
}

Execution effect:

包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
包子铺开始做包子
包子造好了:薄皮牛肉大葱
吃货来吃吧
吃货正在吃薄皮牛肉大葱包子
包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子

Chapter 6 thread pool

6.1 overview of thread pool idea

When we use threads, we create a thread, which is very easy to implement, but there will be a problem:

If there are a large number of concurrent threads and each thread executes a task for a short time, the efficiency of the system will be greatly reduced because it takes time to create and destroy threads frequently.

So is there a way to make threads reusable, that is, after executing a task, they can continue to execute other tasks without being destroyed?

In Java, this effect can be achieved through thread pool. Let's explain the thread pool of Java in detail.

6.2 thread pool concept

Since many operations in the thread pool are related to optimizing resources, we will not repeat them here. Let's understand the working principle of thread pool through a diagram:

Rational utilization of thread pool can bring three benefits:

6.3 use of thread pool

The top-level interface of thread pool in Java is Java util. concurrent. Executor, but strictly speaking, executor is not a thread pool, but just a tool for executing threads. The real thread pool interface is Java util. concurrent. ExecutorService。

Configuring a thread pool is complex, especially when the principle of thread pool is not very clear, it is likely that the configured thread pool is not better, so in Java util. concurrent. Executors thread factory class provides some static factories to generate some common thread pools. Officials recommend using the executors project class to create thread pool objects.

There is a method in the executors class to create a thread pool, as follows:

If a thread pool executorservice object is obtained, how to use it? Here, a method to use the thread pool object is defined as follows:

To use thread objects in the thread pool:

Runnable implementation class code:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("我要一个教练");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}

Thread pool test class:

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyRunnable中的run()

        // 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}
The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>