多线程基础部分
- 1. 线程与进程的关系
- 1.1 多线程启动
- 1.2 线程标识
- 1.2.1 Thread与Runnable
- 1.3 线程状态
- 2.线程池入门
- 2.1 ThreadPoolExecutor
- 2.2 创建线程池
- 2.3 关闭线程池
- 创建线程的几种方法
- 参考
1. 线程与进程的关系
1个进程包含1个或多个线程。
1.1 多线程启动
线程有两种启动方式:实现Runnable接口;继承Thread类并重写run()方法。(这个是没有线程池的,后面会说到线程通过线程池实现的方式)
执行进程中的任务时才会产生线程,因此需要一种描述任务的方式,这可以由Runnable接口来提供。要想定义任务,需要实现Runnable接口并且重写run()方法,然后再将Runnable的实现对象作为参数传递给Thread类。
还可以采用继承Thread类并且重写run方法,然后调用start()启动线程。
通常情况下,实现Runnable接口然后启动线程是一个更好的选择,这可以提高程序的灵活性和扩展性,并且用Runnable接口描述任务也更容易理解。在后面的线程池调用中,也使用Runnable表示要执行的任务。
需要特别注意的是,执行start()方法的顺序不代表线程启动的顺序,在下面示例的ThreadTest中,我们按照顺序调用了8个线程的start()方法,但是线程的执行顺序并没有规律,而且每次运行的结果可能都不一样。
代码:
执行结果:
为什么会出现这样的运行结果呢?这主要是因为任务的执行靠CPU,而处理器采用分片轮询方式执行任务,所有的任务都是抢占式执行模式,也就是说任务是不排序的。可以设置任务的优先级,优先级高的任务可能会优先执行(多数时候是无效的)。任务被执行前,该线程处于自旋等待状态。
1.2 线程标识
Thread类用于管理线程,如设置线程优先级、设置Daemon属性、读取线程名字和ID、启动线程任务、暂停线程任务、中断线程等。
为了管理线程,每个线程在启动后都会生成一个唯一的标识符,并且在其生命周期内保持不变。当线程被终止时,该线程ID可以被重用。而线程的名字更加直观,但是不具有唯一性。
Name:Thread-0
id :8
1.2.1 Thread与Runnable
Runnable接口表示线程要执行任务。当Runnable中的run()方法执行时,表示线程在激活状态,run()方法一旦执行完毕,即表示任务完成,则线程将被停止。
1.3 线程状态
线程对象在不同的运行时期存在着不同的状态,在Thread类中通过一个内部枚举类State保存状态信息,了解线程状态对于并发编程非常重要。
Java中的线程存在6种状态,分别是NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。我们可以通过Thread类中的Thread.getState()方法获取线程在某个时期的线程状态。在给定的时间点,线程只能处于一种状态。
-
NEW状态
-
RUNNABLE状态
-
BLOCKED状态
BLOCKED为阻塞状态,表示当前线程正在阻塞等待获得监视器锁。当一个线程要访问被其他线程synchronized锁定的资源时,当前线程需要阻塞等待。
代码测试如下,在主函数中分别启动了两个线程,它们都需要获得object对象的监视器锁后执行任务。第一个线程启动后,首先获得了object监视器锁。由于在run()方法中使用了死循环while(true),因此第一个线程获得了object监视锁后不会释放,这导致第二个线程长期处于阻塞等待状态。
第一个线程的状态经历了NEW→RUNNABLE状态的变化;第二个线程的状态经历了NEW→RUNNABLE→BLOCKED等几个状态的变化
测试结果如下:
Thread-0:状态:NEW
Thread-1:状态:NEW
Thread-0:状态:RUNNABLE
Thread-1:状态:RUNNABLE
Thread-0:状态:RUNNABLE
Thread-1:状态:BLOCKED
主线程:main:状态:RUNNABLE
4.WAITING状态
// todo
2.线程池入门
线程池与数据库连接池非常相似,目的是提高服务器的响应能力。线程池可以设置一定数量的空闲线程,这些线程即使在没有任务时仍然不会释放。线程池也可以设置最大线程数,防止任务过多,压垮服务器。
2.1 ThreadPoolExecutor
ThreadPoolExecutor是应用最广的底层线程池类,它实现了Executor和ExecutorService接口。在如下类的描述中,列举了ThreadPoolExecutor的构造函数和最常用的几个方法。
2.2 创建线程池
下面创建一个线程池,通过调整线程池构造函数的参数来了解线程池的运行特性。把核心线程数设置为3,最大线程数设置为8,阻塞队列的容量设置为5。
(1)当要执行的任务数小于核心线程数时,直接启动与任务数相同的工作线程。
package com.company;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadTest {
public static void main(String[] args) {
BlockingQueue<Runnable> bq = new LinkedBlockingQueue<>(5);
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 8, 2000, TimeUnit.MILLISECONDS, bq);
for (int i = 0; i < 2; i++) {
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + " is running...");
try {
Thread.sleep(800);
} catch
(Exception e) {
}
}
});
}
pool.shutdown();
}
}
(2)当任务数量大于核心线程数时,超过核心线程数的任务会自动加入阻塞队列中,直到把阻塞队列装满。
调整任务数量为5:for(int i=0;i<5;i++) {…},观察程序运行结果如下。前面的3个任务启动了3个线程并加入线程池,后面的两个任务加入阻塞队列,等待前面的3个任务执行完毕。等前面3个任务完成后,程序会从阻塞队列中取出后面两个任务,然后仍然使用核心线程执行。因此会发现执行最后两个任务的线程号与前面的相同
8 is running…
10 is running…
.9 is running…
10 is running…
.8 is running…
(3)继续增加任务数量为10:for(int i=0;i<10;i++) {…},观察程序的运行结果如下。仔细观察会发现一共启动了5个线程。为什么线程池中的工作线程为5呢?
原因如下:核心线程数为3,因此前面的3个任务会启动3个工作线程。阻塞队列数量为5,因此第4、5、6、7、8这5个任务会自动加入阻塞队列。这时阻塞队列已满,第9、10两个任务会再启动两个新线程。注意:现在的工作线程数量一共为5,小于线程池设置的最大线程数8
9 is running…
11 is running…
.12 is running…
.8 is running…
10 is running…
.9 is running…
11 is running…
.10 is running…
.12 is running…
.8 is running…
(4)继续增加任务数量为15:for(int i=0;i<15;i++) {…},观察程序的运行结果可以发现,当任务数大于“最大线程数+阻塞队列容量”时,会抛出RejectedExecutionException(拒绝执行任务)异常。当前线程池的设置参数,最大容量是8+5=13,当任务数超过13时,都会被拒绝
8 is running…
10 is running…
.12 is running…
.9 is running…
13 is running…
.11 is running…
.14 is running…
.15 is running…
.Exception in thread “main” java.util.concurrent.RejectedExecution
Exception:Task com.icss.pool.MyPool$1@1c7c054 rejected from…
12 is running…
.9 is running…
8 is running…
13 is running…
.10 is running…
2.3 关闭线程池
调用ThreadPoolExecutor的shutdown()方法或shutdownNow()方法,可以关闭线程池
shutdownNow()与shutdown()的主要区别是:shutdownNow()可以把已提交但是未执行的任务主动取消,并返回未执行的任务列表。
// Executor接口 TODO
创建线程的几种方法
创建线程一共有哪几种方法?
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
- 使用线程池例如用Executor框架
第一个:
继承Thread类创建线程,首先继承Thread类,重写run()
方法,在main()
函数中调用子类实实例的start()
方法。
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法正在执行");
}
}
public class TheadTest {
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
}
}
结果:(谁前谁后不一定)
main main()方法执行结束
Thread-0 run()方法正在执行
第二个:
实现Runnable接口创建线程:首先创建实现Runnable接口的类RunnableDemo,重写run()方法;创建类RunnableDemo的实例对象runnableDemo,以runnableDemo作为参数创建Thread对象,调用Thread对象的start()方法。
public class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中");
}
}
public class RunnableTest {
public static void main(String[] args) {
RunnableDemo runnableDemo = new RunnableDemo ();
Thread thread = new Thread(runnableDemo);
thread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
第三个:
使用Callable和Future创建线程:
- 创建Callable接口的实现类CallableDemo,重写call()方法。
- 以类CallableDemo的实例化对象作为参数创建FutureTask对象
- 以FutureTask对象作为参数创建Thread对象。
- 调用Thread对象的start()方法。
import java.util.concurrent.Callable;
public class CallableDemo implements Callable<Integer> {
@Override
public Integer call(){
System.out.println(Thread.currentThread().getName() + " call()方法执行中");
return 0;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new CallableDemo());
Thread thread = new Thread(futureTask);
thread.start();
System.out.println("返回结果 " + futureTask.get());
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
}
DEMO:
// 组装考试成绩sheet信息
FutureTask<List<List<String>>> scoreSheetTask = new FutureTask<>(() ->
generateScoreAnswerSheet(courseName, courseSerial, scheduleTitle, scheduleSerial, studentInfoMap, studentExamList, checkList)
);
Thread scoreSheetThread = new Thread(scoreSheetTask);
scoreSheetThread.start();
// 组装考试明细-首次回答sheet的信息 考试明细-最高分数sheet的信息
FutureTask<HashMap<String, List<List<String>>>> answerSheetTask = new FutureTask<>(() ->
generateAnswerSheet(studentInfoMap, examList, examContentList, checkList)
);
Thread answerSheetThread = new Thread(answerSheetTask);
answerSheetThread.start();
// 组装能力评估明细sheet的信息
FutureTask<List<List<String>>> assessmentSheetTask = new FutureTask<>(() ->
generateAssessmentAnswerSheet(courseName, courseSerial, scheduleTitle, scheduleSerial, studentInfoMap, studentAssessmentList, checkList)
);
Thread assessmentSheetThread = new Thread(assessmentSheetTask);
assessmentSheetThread.start();
第四个:
使用线程池例如用Executor框架: Executors
可提供四种线程池,分别为:
newCachedThreadPool
:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool
:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool
:创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor
:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
下面以创建一个定长线程池为例进行说明
public class ThreadDemo extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行");
}
}
class TestFixedThreadPool {
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 = new ThreadDemo();
Thread t2 = new ThreadDemo();
Thread t3 = new ThreadDemo();
Thread t4 = new ThreadDemo();
Thread t5 = new ThreadDemo();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
}
result:
pool-1-thread-1正在执行
pool-1-thread-1正在执行
pool-1-thread-2正在执行
pool-1-thread-1正在执行
pool-1-thread-2正在执行
参考
1.书籍《Java多线程与线程池技术详解》肖海鹏 牟东旭
2.牛客并发编程部分