目录
5 实战,使用 FutureTask 模拟查询和统计学生考试成绩。
1 线程与进程的理解
什么是进程?
进程是程序运行资源分配的最小单位,当你运行一个程序的时候,你就启动了一个进程。
进程之间是相互独立的,互不干扰。
什么是线程?
线程是CPU调度的最小单位。
线程不能单独存在,线程必须被包含在一个进程中,进程与线程的关系是1对多。
在你启动一个程序的时候,通常会启动一个程序进程,进程中拥有多个线程,线程之间相互协
作、共同执行一个应用程序。
即使是单核CPU也支持多线程,CPU通过给每个线程分配CPU时间片来实现CPU时间片轮转。
时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感
觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
上下文切换
CPU在从一个时间片段切换到另一个时间片段执行任务前,会保存上一个任务的状态,以便下一次
切换回这个任务时,可以加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换,频繁的
上下文切换会消耗大量的CPU资源。
并发编程的目的是为了让程序运行的更快,并不是创建线程越多越好,而是要结合cpu的性能合理的设计,
而且要考虑到上下文切换、资源限制等问题。
2 Java天生就是多线程的
当一个Java程序执行main()方法后,会默认创建多个线程来工作。
测试代码
/**
* 打印java程序启动时线程堆栈信息
*/
public class DumpMain {
public static void main(String[] args) {
//Java 虚拟机线程系统的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// main启动后的线程堆栈信息
ThreadInfo[] threadInfos =
threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,打印线程ID和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] "
+ threadInfo.getThreadName());
}
}
}
打印信息
[5] Monitor Ctrl-Break // 监控Ctrl-Break中断信号的
[4] Signal Dispatcher // 分发处理发送给JVM信号的线程
[3] Finalizer // 调用对象finalize方法的线程
[2] Reference Handler // 清除Reference的线程
[1] main // 程序入口,主线程
3 Java中对线程的支持。
a) Thread
Thread是Java对线程的抽象。
通过继承Thread类,并重写Run()方法来定义一个线程任务。
/**
* 自定义线程类
*/
public class MyThread extends Thread{
// 重写接口方法,任务逻辑
@Override
public void run() {
System.out.println("我是自定义线程任务");
}
public static void main(String[] args) {
// 创建线程对象
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
// 获取线程Id
System.out.println(myThread.getId());
// 获取线程名字
System.out.println(myThread.getName());
// 获取线程优先级
System.out.println(myThread.getPriority());
// 是否为守护线程。
// 守护线程,是指在程序运行的时,后台提供一种通用服务的线程。比如java的垃圾回收。
// 守护线程在所有用户线程终止后自动终止。
System.out.println(myThread.isDaemon());
}
}
只有执行了start()方法后,才实现了真正意义上的启动线程。
Thread 实现了 Runnable 接口,Runnable 接口只有一个方法 run() 来实现任务(业务
逻辑)代码。
线程的优先级和守护线程。
java 中的线程优先级的范围是1~10,默认的优先级是5,“高优先级线程”会优先于“低
优先级线程”执行。
守护线程,是指在程序运行的时,后台提供一种通用服务的线程。比如java的垃圾回收。
守护线程在所有用户线程终止后自动终止。
要设置守护线程,设置myThread.setDaemon(true); 设置守护线程必须在线程运行(调用start)
之前。
b) Runnable
Runnable 是Java对任务(业务逻辑)的抽象。
Runnable 接口代码
public interface Runnable {
public abstract void run();
}
有了Thread为什么还要设计Runnable呢?
首先是Java设计者对两者的定义不同,Thread是Java对线程的抽象,而Runnable是对任务
(业务逻辑)的抽象,一般实现Runnable接口定义业务逻辑,然后交由Thread来执行。
其次,Java是单继承的,如果你的类继承了Thread,就不能再继承别的类了。
使用Runnable 的代码
/**
* 实现Runnable来定义线程类
*/
public class MyRunnable implements Runnable{
// 重写接口方法,任务逻辑
@Override
public void run() {
System.out.println("我是自定义线程任务");
}
public static void main(String[] args) {
// 创建任务(业务逻辑)对象
MyRunnable myRunnable = new MyRunnable();
// 创建 Thread 执行对象
// 将任务交给 Thread 执行
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
4 认识 FutureTask
FutureTask 是 java 除了 Thread 和 Runnable 外支持的第三种线程类,它是有返回值的。
首先看一下 FutureTask 的类图
FutureTask 实现了Runnable接口 ,也就是说它可以作为任务交由Thread执行。
再看 FutureTask 的一个构造函数
public FutureTask(Callable<V> callable) {
}
它支持传入一个带泛型结果的 Callable 对象,FutureTask 会在run() 方法里调用 callable
的 call() 方法,所以我们可以创建一个Callable对象,重写call() 方法,然后在构造 FutureTask 对
象的时候传进去。
这个 call() 方法就是任务(业务逻辑)的定义代码,并会返回任务结果V。
实现一个自己的任务处理对象
// Callable 实现类 返回结果为 Integer
public class MyCallable implements Callable<Integer>{
// 返回结果
@Override
public Integer call() throws Exception {
// 业务处理,模拟返回结果
return 1;
}
public static void main(String[] args) throws Exception {
// 创建 Callable 对象
MyCallable myCallable = new MyCallable();
// 获取返回结果
int result = myCallable.call();
// 打印
System.out.println(result);
}
}
单独实现一个 Callable 是没有什么意义的,一般结合 FutureTask 使用。
那么 FutureTask 是如果获取返回结果呢?
再看类图,FutureTask 同时实现了Future 接口,所以通过 get() 方法就可以返回执行结果。
这个get() 方法是阻塞方法,即 get() 之后的代码必须等到线程运行完毕后才会继续执行。
用 FutureTask 执行任务代码
// 自定义 FutureTask
public class MyFutrueTask {
public static void main(String[] args) {
// 定义任务(业务逻辑)和返回值
MyCallable callable = new MyCallable();
// 以callable 为参数创建 FutureTask 对象
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 交由 thread 执行
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
try {
// 阻塞并获取结果
Integer result = futureTask.get();
// 打印结果
System.out.println(result);
// 捕获中断异常
} catch (InterruptedException e) {
// 打印异常信息
e.printStackTrace();
// 捕获 执行异常
} catch (ExecutionException e) {
// 打印异常信息
e.printStackTrace();
}
}
}
FutureTask 的一些常用方法
// 任务是否被取消
futureTask.isCancelled();
// 取消任务执行
futureTask.cancel(true);
// 任务是否完成
futureTask.isDone();
5 实战,使用 FutureTask 模拟查询和统计学生考试成绩。
设计要求
分别统计某学生的语文,数学和英语的分数并计算总分。
实现代码
/**
* 学生考试分数查询任务类
* 实现Callable,返回Integer
*/
public class StudentCall implements Callable<Integer> {
// 科目名字
private String courseName;
// 带科目名字的构造参数
public StudentCall(String courseName) {
this.courseName = courseName;
}
// 重写任务(业务逻辑)方法 查询科目考试分数
@Override
public Integer call() throws Exception {
// 开始时间,毫秒
long startTimeMillis = System.currentTimeMillis();
// 模拟查询科目耗时操作 0~4秒
int sleepMillis = (int)(Math.random()*5)*1000;
Thread.sleep(sleepMillis);
// 任务结束时间
System.out.println(courseName+"任务结束时间:"+
new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss:SSS") .format(new Date()));
// 结束时间,毫秒
long endTimeMillis = System.currentTimeMillis();
// 耗时
System.out.println(courseName+"任务耗时:"+(endTimeMillis - startTimeMillis)+"ms");
// 模拟返回查询科目分数
int score = (int)(Math.random()*100);
// 返回结果
return score;
}
}
/**
* FutureTask 实现
* 并发查询学生各科成绩和总分
*/
public class MyFutrue4Score {
// 模拟查询科目
public static String[] courseNames = {"语文","数学","英语"};
public static void main(String[] args) {
// 开始时间,毫秒
long startTimeMillis = System.currentTimeMillis();
// 总分
int sumScore = 0;
// 保存查询科目分数任务
List<FutureTask<Integer>> scoreList = new ArrayList<>();
// 遍历科目
for(String courseName : courseNames){
// 创建查询科目分数任务
StudentCall call = new StudentCall(courseName);
// 创建 FutureTask,传入 Callable 对象
FutureTask<Integer> futureTask = new FutureTask<>(call);
// 将 futureTask 添加到 List 容器中供以后使用
scoreList.add(futureTask);
// 交由 Thread 执行
new Thread(futureTask).start();
}
// 遍历任务
for(int i=0;i<scoreList.size();i++){
try {
// 调用 futureTask 的阻塞方法,获取任务结果
int singleScore = scoreList.get(i).get();
// 打印科目分数
System.out.println(courseNames[i]+":"+singleScore);
// 计算总分数
sumScore = sumScore + singleScore;
// 捕获异常并打印异常信息
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 打印总分数,必须在所有任务执行完毕后才会输出
System.out.println("总分:"+sumScore);
// 结束时间,毫秒
long endTimeMillis = System.currentTimeMillis();
// 耗时
System.out.println("任务总耗时:"+(endTimeMillis - startTimeMillis)+"ms");
}
}
执行结果:
数学任务结束时间:2023-06-15-17:14:42:447
数学任务耗时:2023ms
英语任务结束时间:2023-06-15-17:14:43:430
英语任务耗时:3006ms
语文任务结束时间:2023-06-15-17:14:43:431
语文任务耗时:3008ms
语文:17
数学:24
英语:91
总分:132
任务总耗时:3008ms
运行时间分析
虽然使用了多个线程同时查看学生每门功课分数,但是程序总耗时是由其中最耗时的线程
执行时间而决定的。
6 源代码下载和补充
源码下载地址 gitcode
部署前请先查看readme.txt说明文档。