一.多线程
1.1 什么是多线程
什么是进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
什么是线程:线程与进程类似,一个进程在其执行的过程中可以产生多个线程。在Java中,同一个进程的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。java 通过 Java.lang.Thread 类来代表线程。
什么是多线程:多线程是指从软硬件上实现多条执行流程的技术。
1.2 Thread类的常用API
Thread类在Java中是用于创建和操作线程的类,它允许开发人员在程序中实现并发执行。其常用API如下:
构造器
//为当前线程指定名称
public Thread(String name)
//封装Runnable对象成为线程对象
public Thread(Runnable target)
//封装Runnable对象成为线程对象,并指定线程名称
public Thread(Runnable target, String name)
常用方法
//获取当前线程的名称,默认线程名称是Thread-索引
String getName()
//设置线程名称
void setName(String name)
//返回对当前正在执行的线程对象的引用
public static Thread currentThread()
//让线程休眠指定的时间,单位为毫秒
public static void sleep(long time)
//线程任务方法
public void run()
//线程启动方法
public void start()
1.3 线程创建方式
1.3.1 继承Thread类
继承Thread类创建线程编码简单。但是,线程类已经继承Thread,无法继承其他类,不利于扩展。具体步骤如下
//1.创建一个类,继承Thread类,重写run方法
public class MyThread extends Thread {
public void run() {
System.out.println("This is a thread created by extending Thread class.");
}
public static void main(String[] args) {
//2.创建这个类的对象
MyThread myThread = new MyThread();
//3.调用start方法启动线程
myThread.start(); // 启动线程
}
}
1.3.2 实现Runnable接口
实现Runable接口可以继续继承类,扩展性强。编码比第一种复杂点。具体步骤如下:
-
定义一个类实现 Runnable 接口,重写 Run 方法。
-
创建这个类的对象,并在创建 Thread 对象时把这个对象作为构造方法的参数。
-
调用 Thread 对象的 start 方法启动线程。
//1.创建一个类,实现Runnable接口
public class MyRunnable implements Runnable {
//2.实现run方法
public void run() {
System.out.println("This is a thread created by implementing Runnable interface.");
}
public static void main(String[] args) {
//3.创建Thread对象时,将这个类的对象作为构造函数的参数
Thread myThread = new Thread(new MyRunnable());
//4.调用Thread对象的start方法启动线程
myThread.start();
}
}
1.3.3 利用Callable、Future
继承 Thread 类或实现 Runnable 接口虽然都可以创建线程,但这两种方法都有一个问题就是:线程执行完一个任务之后,无法返回一个值。针对此问题,Java 中提供了 Future 和 Callable 来解决这个问题。
public interface Future<V> {
// 取消任务的执行,参数表示是否立即中断任务执行,或者等任务结束
boolean cancel(boolean mayInterruptIfRunning);
// 任务是否已经取消,任务完成前将其取消,则返回true
boolean isCancelled();
// 任务是否已经完成
boolean isDone();
// 等待任务执行结束,返回泛型结果.中断或任务执行异常都会抛出异常
V get() throws InterruptedException, ExecutionException;
// 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
FutureTask:Future 是一个用于表示异步计算结果的接口,它提供了检查任务是否完成、等待任务完成、获取任务结果的方法。FutureTask 是 Future 的具体实现,同时还实现了 Runnalbe 接口。因此FutureTask 既可以被当做 Runnable 来执行,也可以被当做 Future 来获取 Callable 的返回结果。
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
/** The underlying callable; nulled out after running */
// 任务
private Callable<V> callable;
/** The result to return or exception to throw from get() */
// 执行结果或异常
private Object outcome; // non-volatile, protected by state reads/writes
/** The thread running the callable; CASed during run() */
// 执行任务的线程
private volatile Thread runner;
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
FutureTask的使用:创建 Thread 对象时,可以传入 FutureTask 对象,创建 FutureTask 对象时,可以传入 Callable 对象。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//1.创建Callable对象
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
};
//2.创建FutureTask对象,将Callable对象作为参数传递进去
FutureTask<Integer> futureTask = new FutureTask<>(callable);
//3.创建线程并启动
Thread thread = new Thread(futureTask);
thread.start();
//4.获取异步计算结果
int result = futureTask.get();
System.out.println("1~100的和为:" + result);
}
}
Future 本身也确实存在着许多限制,比如:
-
阻塞操作:Future 提供的 get 方法是阻塞获取结果,这会影响性能。
-
无法对多个任务进行链式调用:Future 无法做到一个任务完成后紧接着执行其它任务。
-
无法组合多个任务:Future 不支持将多个任务组合成一个复杂的操作。比如运行了 10 个任务,并期望在它们全部执行结束后执行特定动作,在 Future 中这是做不到的;
-
没有异常处理:Future 接口中没有关于异常处理的方法;
Java 中可以使用 CompletionService 或 CompletableFuture 对这些局限进行解决。但是,由于CompletionService 存在着一个回调地狱的问题,所以这里就只介绍 CompletableFuture 的使用。
1.4 CompletableFuture
CompletableFuture 实现了 Future 接口,并在此基础上进行了丰富的扩展,完美弥补了 Future 的局限性,实现了对任务编排的能力。
1.4.1 简单使用
1.supplyAsync:有返回值
最常见的创建方式是使用 CompletableFuture.supplyAsync(),来进行一个简单的异步计算。这个方法需要一个 Supplier 函数接口,能够将任务的执行结果返回。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟耗时的计算
simulateTask("数据加载中");
return "结果";
});
2.使用runAsync:无返回值
如果不关心异步任务的结果,只想执行一个异步操作,那就可以用 runAsync。它接收一个Runnable 函数接口,不返回任何结果:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
simulateTask("正在执行一些处理");
});
3.手动完成
complete 或 completeExceptionally 方法用于手动完成一个 CompletableFuture。它允许你在异步操作完成之前直接设置结果或异常,从而控制异步任务的状态。
CompletableFuture<String> future = new CompletableFuture<>();
// 设置结果
future.complete("Hello, World!");
// 设置异常
future.completeExceptionally(new RuntimeException("Something went wrong"));
future.thenAccept(result -> System.out.println("Result: " + result))
.exceptionally(ex -> {
System.out.println("Error: " + ex.getMessage());
return null;
});
1.4.2 链式调用
链式调用是指一系列任务依次执行,前一个任务的结果作为下一个任务的输入。CompletableFuture 支持多种链式调用方法,比如 thenApply、thenAccept 和 thenRun。
-
thenApply 用于处理上一个 CompletableFuture 的结果,并将处理后的结果交给下一个任务。
-
thenAccept 用于消费 CompletableFuture 的结果。
-
thenRun 则不关心前一个任务的结果,只是在前一个任务执行完后,执行一些后续操作。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
simulateTask("查询数据库");
return "查询结果";
});
future.thenApply(result -> {
// 对结果进行处理
return "处理后的结果:" + result;
}).thenAccept(processedResult -> {
// 消费处理后的结果
System.out.println("最终结果:" + processedResult);
}).thenRun(() -> {
// 执行一些不需要前一个结果的操作
System.out.println("所有操作完成");
});
1.4.3 异常处理
1.exceptionally方法
在CompletableFuture的世界里,如果异步操作失败了,异常会被捕获并存储在Future对象中。咱们可以使用exceptionally方法来处理这些异常。这个方法会返回一个新的CompletableFuture,它会在原来的Future抛出异常时执行。
//这里,创建了一个可能会失败的异步操作。如果抛出异常,exceptionally方法就会被调用,
//返回一个包含错误信息的回退结果。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (new Random().nextBoolean()) {
throw new RuntimeException("出错啦!");
}
return "正常结果";
}).exceptionally(ex -> {
return "错误的回退结果:" + ex.getMessage();
});
future.thenAccept(System.out::println);
2.handle方法
无论异步操作是成功还是失败,handler 方法都会被调用。如果有异常,它会处理异常;如果没有,就处理正常结果。
//在这个例子中,无论异步操作是成功还是失败,handle方法都会被调用。如果有异常,
//它会处理异常;如果没有,就处理正常结果。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (new Random().nextBoolean()) {
throw new RuntimeException("出错啦!");
}
return "正常结果";
}).handle((result, ex) -> {
if (ex != null) {
return "处理异常:" + ex.getMessage();
}
return "处理结果:" + result;
});
future.thenAccept(System.out::println);
3.管道式异常处理
CompletableFuture 还允许咱们创建一个异常处理的“管道”,这样就可以把多个异步操作链接起来,并在链的任意位置处理异常。
//在这个例子中,创建了一个包含三个步骤的异步操作链。如果第二步出错,异常会被捕
//获并处理,然后处理结果被传递到第三步。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 第一个异步操作
return "第一步结果";
}).thenApply(result -> {
// 第二个异步操作,可能会出错
throw new RuntimeException("第二步出错啦!");
}).exceptionally(ex -> {
// 处理异常
return "在第二步捕获异常:" + ex.getMessage();
}).thenApply(result -> {
// 第三个异步操作
return "第三步使用结果:" + result;
});
future.thenAccept(System.out::println);
1.4.4 组合与依赖
1.组合多个Future
最常用的方法之一是 thenCombine。这个方法允许你组合两个独立的 CompletableFuture,并且当它们都完成时,可以对它们的结果进行一些操作。
//在这个例子中,future1和future2代表两个独立的异步操作。只有当两者都完成时,
//thenCombine里面的函数才会执行,并且合并它们的结果。
0CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
simulateTask("加载用户信息");
return "用户小黑";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
simulateTask("加载订单数据");
return "订单123";
});
CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (userInfo, orderInfo) -> {
return "合并结果:" + userInfo + "," + orderInfo;
});
combinedFuture.thenAccept(System.out::println);
2.依赖关系的处理
如果你的一个异步操作依赖于另一个异步操作的结果,那么可以使用 thenCompose 方法。这个方法允许你在一个 Future 完成后,以其结果为基础启动另一个异步操作。
//这个例子中,dependentFuture的执行依赖于masterFuture的结果。
CompletableFuture<String> masterFuture = CompletableFuture.supplyAsync(() -> {
simulateTask("获取主数据");
return "主数据结果";
});
CompletableFuture<String> dependentFuture = masterFuture.thenCompose(result -> {
return CompletableFuture.supplyAsync(() -> {
simulateTask("处理依赖于" + result + "的数据");
return "处理后的数据";
});
});
dependentFuture.thenAccept(System.out::println);
3.处理多个Future
有时候,咱们可能有多个异步操作,需要等所有操作都完成后再进行下一步。这时候,可以使用CompletableFuture.allOf。
//allOf会等待所有提供的Futures完成,然后执行后续操作。
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
simulateTask("任务一");
return "结果一";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
simulateTask("任务二");
return "结果二";
});
CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);
allFutures.thenRun(() -> {
System.out.println("所有任务完成");
});
1.4.5 最佳实践
1.使用合适的线程池
CompletableFuture提供了多种执行异步任务的方法,比如runAsync和supplyAsync。默认情况下,它们使用公共的ForkJoinPool,但在某些场景下,你可能想要使用自定义的线程池来更好地控制资源。
ExecutorService customExecutor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "使用自定义线程池";
}, customExecutor);
1.5 线程同步
1.5.1 线程同步概述
线程安全问题是指多个线程并发访问同一个共享资源且存在修改该共享资源的情况。线程同步指的是多个线程在并发执行过程中通过一定的机制来协调彼此的执行顺序,以确保它们按照一定的逻辑或顺序进行执行,避免出现线程安全问题。
1.5.2 synchronized关键字
synchronized 是实现线程同步的方式之一,被修饰的方法或者代码块在不管什么时候只能有一个线程执行。要注意的是线程间串行执行只是线程同步的一种,而且性能很差。synchronized 关键字的使用方式主要有下面 3 种:
修饰实例方法:给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
synchronized void method() {
//业务代码
}
修饰静态方法:给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 "当前 class 的锁",也就是类对象的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
synchronized static void method() {
//业务代码
}
修饰代码块:对 synchronized 后面括号里指定的对象或者类加锁:
-
synchronized(object) 表示进入同步代码库前要获得给定对象的锁。
-
synchronized(类.class) 表示进入同步代码前要获得给定类对象的锁。
synchronized(this) {
//业务代码
}
二.线程池
2.1 什么是线程池
线程池是用来管理线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程不会立即被销毁,而是等待下一个任务。线程池的优点在于:
- 降低性能开销。创建线程和销毁线程是会造成性能开销的,线程池可以重复利用已经创建好的线程,避免这部分开销。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 控制并发线程数量。 线程池可以限制并发执行的线程数量,防止系统因为线程过多而崩溃。
创建线程池有二种方式,分别是通过 ThreadPoolExecutor 创建线程池对象、Executors创建线程池对象。线程池的常用方法如下:
//执行没有返回值的任务,一般用来执行Runnable任务
void execute(Runnable command)
//执行任务,返回Future对象获取线程执行结果,一般用来执行Callable任务
Future<T> submit(Callable<T> task)
//等任务执行完毕后关闭线程池
void shutdown
//立刻关闭,停止正在执行的任务,并返回队列中未执行的任务
List<Runnable> shutdownNow()
//用于提交一组Callable任务任务并等待它们全部完成。返回一个List的Future对象,代表每个任务的结果。
List<Future<T>> invokeAll(List<Callable<T>>)
2.3 ThreadPoolExecutor
ThreadPoolExecutor 创建线程池对象时,需要指定如下七个参数
public ThreadPoolExecutor(
int corePoolSize,//不能小于0
int maximumPoolSize,//最大数量>=核心线程数量
long keepAliveTime,//不能小于0
TimeUnit unit,/时间单位
BlockingQueue<Runnable> workQueue,//不能为null
ThreadFactory threadFactory,//不能为null
RejectedExecutionHandler handler//不能为null
)
-
核心线程数:核心线程数是线程池在空闲的时候,也可以保持的线程数量。
-
最大线程数:线程池最多可以存在的线程数。
-
存活时间:临时线程处于空闲时的最大存活时间。
-
时间单位:存活时间的单位。
-
工作队列:工作队列用于存储尚未执行的任务。当线程池中的线程都处于忙碌状态时,新提交的任务会被存储在工作队列中,等待线程池中的线程来执行。
-
线程工厂:线程工厂用于创建新的线程,可以对线程的创建过程进行自定义,例如设置线程的名称、优先级等。
-
拒绝策略:当线程池已经饱和并且任务队列也满了,无法接收新任务的时候,拒绝策略决定了如何处理这些新任务。
2.4 Executors
线程池的工具类通过调用方法返回不同特点的线程池对象。但是,Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。创建线程池对象的方法如下所示:
三.定时器
3.1 什么是定时器
定时器是一种控制任务延时调用,或者周期调用的技术。常用于闹钟、定时邮件的发送。定时器的实现方式有
-
JDK提供的Timer、ScheduledExecutorService定时器
-
Spring提供的@scheduled注解
-
各种分布式任务调度框架,如非常常用的xxl-job
3.2 Timer定时器
Timer是JDK提供的一个定时器,在后台会创建一个延迟的任务队列,然后由后台的一个单线程去处理这些任务。但是,存在以下缺点:
-
由于是一个线程顺序执行所有任务,所以定时任务执行时会与设置定时器的时间有出入。
-
任务队列中的某个任务如果出现异常会使线程挂掉,从而影响后续任务执行
//创建Timer定时器对象
public Timer()
//开启一个定时器,按照计划处理TimerTask任务(和时间相关的参数都是以毫秒为单位)
//参数一:定义一个继承TimerTask的类,并实现其中的run方法
//参数二:调用了schedule方法后,经过delay这么长时间后真正执行里面的业务代码
//参数三:周期, 第一次调用之后,后续每隔多长时间调用一次 run() 方法
public void schedule(TimerTask task, long delay, long period)
3.3 ScheduledExecutorService
ScheduledExecutorService 是 jdk1.5 中引入的,目的是为了弥补 Timer 的缺陷,内部基于线程池,每个任务的执行不会影响其它定时任务的执行。
//得到ScheduledExecutorService定时器的对象
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
//通过ScheduledExecutorService对象开启一个定时器
public ScheduledFuture<?> scheduledAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
3.4 xxl-job
xxl-job是一个分布式任务调度框架,其设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。xxl-job中有二个角色,分别是
-
xxl-job-admin调度中心:统一管理任务调度平台上的调度任务,负责触发调度执行,并且提供任务管理平台。
-
xxl-job-executor执行器:执行器通常是我们的业务系统,如经常见到的springboot项目。
具体使用可参考:https://www.cnblogs.com/fuqian/p/17286970.html
五.线程的生命周期
5.1 什么是线程的生命周期
线程的生命周期就是线程从生到死的过程,以及中间的各种状态及状态转换。Java总共定义了 6 种状态,6 种状态都定义在 Thread 类中的内部枚举类中。
public class Thread{
...
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
...
}
5.2 线程状态间的转换
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
初始状态(NEW):线程创建完成之后,调用 start() 方法之前。
可运行状态(RUNNABLE):线程调用start方法之后就进入可运行状态。可运行状态还可以再细分为就绪和运行两种状态。
-
就绪(READY):线程对象创建出来并调用了 start() 方法之后,这个状态的线程就处于就绪状态,等待 cpu 执行。
-
运行中(RUNNING):就绪状态的线程获得了cpu 时间片,开始执行程序代码。
阻塞状态(BLOCKED):线程在等待某个资源的时候被阻塞。常见的阻塞包括等待IO操作完成、等待锁释放等。
等待状态(WAITING):线程可以通过wait()方法使自己进入等待状态,等待其它线程调用notify()或notifyAll()方法唤醒自己。
超时等待状态(TIME_WAITING):相当于在等待状态的基础上增加了超时限制。当超时时间结束后,线程会返回运行状态。通过 sleep(long millis)方法或 wait(long millis)方法进入的都是超时等待状态
终止状态(TERMINATED):线程执行完成或者因异常退出时,线程进入终止状态。