目录
多线程是java项目开发中常见的技术栈,也是进阶程序员面试必然涉及的技术点。
什么是多线程
要了解多线程,首先得知道什么是线程。我们知道一个java应用服务启动后会通过jvm运行产生一个进程,而进程内的执行单元就是线程,比如我们启动了一个电商网站服务,当在页面发起的每次点击都会在后端服务通过一个线程来执行,也就是说一个进程里可以启动多个线程。
线程之间堆内存和方法区内存是共享的,但是栈内存独立,一个线程一个栈,这就是所谓的线程本地内存变量区。可见线程之间互不干扰,各自执行各自的处理逻辑。
还是例如电商网站的服务,不同用户点击请求发起的进程处理之间互相不需要等待,但是一次请求产生的线程的执行是同步的,比如一次勾选购物车里多个订单进行提交,同步过程是先处理订单1再处理2这样顺序下去,如此需要的时间可能就会比较长,而在这个线程开启多个子线程来并发处理所有订单的处理后这个过程就会快很多了,这就是多线程并发的概念,所以多线程可以提高效率,java中多线程机制主要是为了提高处理效率。
图:单机模式下应用进程线程关系
实现多线程的几种方式
1继承java.lang.Thread类
Thread代表线程类,它最主要的两个方法run()【线程运行时所执行的代码】,start()【启动线程】。
继承类必须重写 run() 方法,该方法是新线程的执行入口点。但是必须通过调用 start() 方法执行。
package com.test.mutithread;
public class ThreadTest {
// 继承Thread类实现多线程
static class ThreadDemo extends Thread {
private Thread t;
private String orderNo;
ThreadDemo( String name) {
orderNo = name;
}
// 线程执行方法
public void run() {
System.out.println("订单处理开始 " + orderNo );
try {
//模拟订单处理时间
Thread.sleep(50);
}catch (InterruptedException e) {
System.out.println("Thread " + orderNo + " interrupted.");
}
System.out.println("订单处理结束 " + orderNo );
}
// 线程执行入口
public void start () {
System.out.println("进入订单处理 " + orderNo );
if (t == null) {
t = new Thread (this, orderNo);
t.start ();
}
}
}
public static void main(String args[]) {
for(int i =0;i<5;i++){
ThreadDemo T1 = new ThreadDemo( "订单"+i);
T1.start();
}
}
}
执行结果如下,多次执行发现,各个订单处理线程的结束顺序并不固定,也就是说并行执行的:
2实现 Runnable 接口
实现 Runnable,一个类只需要执行一个方法调用 run()。
相比继承Thread的方式,由于一个类可以实现多个接口,因此实现的方式没有类的单继承性的局限性,实现Runnable接口的方式来完成多线程更加实用。
package com.test.mutithread;
public class RunableTest {
// 实现Runnable接口实现多线程
static class RunnableThread implements Runnable {
private String orderNo;
RunnableThread(String orderNo){
this.orderNo=orderNo;
}
@Override
public void run() {
System.out.println("订单处理开始 " + orderNo );
try {
//模拟订单处理时间
Thread.sleep(50);
}catch (InterruptedException e) {
System.out.println("Thread " + orderNo + " interrupted.");
}
System.out.println("订单处理结束 " + orderNo );
}
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
//创建实现类的对象
RunnableThread runnableThread01 = new RunnableThread("订单"+i);
//创建Thread类的对象,并将参数传入
Thread t1 = new Thread(runnableThread01);
//使用Thread类的对象去Thread类的start()方法:启动线程 ,Thread中的run()调用了Runnable中的run()
t1.start();
}
}
}
执行结果和继承Thread是一样的。
3实现Callable接口
上面两种方式是实现了多线程并发,但是主线程并不知道子线程的执行过程结果,也就是说每个订单的处理进展和结果是不可控的。这个时候可以采用实现Callable接口方案。与Runnable相比,Callable 可以借助FutureTask类获取子线程返回值。
package com.test.mutithread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest {
//创建一个实现Callable的实现类
static class OrderThread implements Callable<String> {
private String orderNo;
OrderThread(String orderNo){
this.orderNo=orderNo;
}
//实现call方法
@Override
public String call() throws Exception {
System.out.println("订单处理开始 " + orderNo );
try {
//模拟订单处理时间
Thread.sleep(50);
}catch (InterruptedException e) {
System.out.println("Thread " + orderNo + " interrupted.");
}
System.out.println("订单处理结束 " + orderNo );
return orderNo+"执行完毕";
}
}
public static void main(String[] args) {
// 订单线程执行结果LIST
List<FutureTask<String>> futureTasks = new ArrayList<>();
for (int i = 0; i < 5; i++) {
//创建Callable接口实现类的对象
OrderThread orderThread = new OrderThread("订单"+i);
//传递参数,创建FutureTask对象
FutureTask<String> futureTask = new FutureTask(orderThread);
futureTasks.add(futureTask);
//创建Thread对象,并调用start()
new Thread(futureTask).start();
}
try {
//Callable中Call方法的返回值
for (FutureTask<String> futureTask: futureTasks ) {
String result = futureTask.get();
System.out.println(result);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
执行结果,可以看到主线程获得了每个订单线程的处理返回结果:
4线程池方式
上面三种方式就是java里最基本的多线程实现方式了。但是以上都需要手动创建线程对象,手动调用,而创建Java线程还涉及线程分配堆栈内存以及初始化内存问题,频繁创建和销毁线程会大大降低系统的运行效率。
对此java提供了线程池的方案(Executors),主要思路是:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中;避免频繁创建销毁、实现重复利用。线程池能独立负责线程的创建、维护和分配。
先看下线程池的核心实现类原码及其参数
线程池核心参数
参数名称 参数意义 说明 corePoolSize 线程池核心线程大小 默认情况下,线程池中是没有线程的,初始化的线程池容量为0,在有线程来临的时候,直接创建 corePoolSize 个线程 maximumPoolSize 最大线程的数量
线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
在核心线程都被占用的时候,后加入的任务会被放在等待队列,当等待队列满了的时候,线程池就会把线程数量创建 maximumPoolSize 个
keepAliveTime 空闲线程存活时间 当线程池中的线程数量大于 corePoolSize 时这个参数就会生效,即当大于 corePoolSize 的线程在经过 keepAliveTime 仍然没有任务执行,则销毁线程 unit 空闲线程存活时间单位 workQueue 工作队列 核心线程都被占有时,后加入任务被房子在任务队列 ThreadFactory 线程工厂 主要用来创建线程,一般默认 handler 拒绝策略 当工作队列中的任务和线程池中的线程数量都达到最大限制,如果还有新任务提交进来,就用拒绝策略进行处理。jdk中提供了4中拒绝策略【拒绝、丢弃、主线程处理、丢弃最早的等待任务】
下面我们实现一个简单的线程池,用来处理上面多线程提交订单的业务。
简单的线程池实现
package com.test.mutithread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ThreadPoolExecutorTest {
// 线程池参数定义
private static final int corePoolSize = 4;
private static final int maximumPoolSize = 10;
private static final long keepAliveTime = 2;
private static final TimeUnit unit = TimeUnit.SECONDS;
private static final int QueueSize = 100;//将参数设定为固定值
//创建一个实现Callable的实现类
static class OrderThread implements Callable<String> {
private String orderNo;
OrderThread(String orderNo) {
this.orderNo = orderNo;
}
//实现call方法
@Override
public String call() throws Exception {
System.out.println("订单处理开始 " + orderNo);
try {
//模拟订单处理时间
Thread.sleep(50);
} catch (InterruptedException e) {
System.out.println("Thread " + orderNo + " interrupted.");
}
System.out.println("订单处理结束 " + orderNo);
return orderNo + "执行完毕";
}
}
public static void main(String[] args) {
//构造线程池,采用抛弃策略
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
new ArrayBlockingQueue<>(QueueSize),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 订单线程执行结果LIST
List<Future<String>> futureTasks = new ArrayList<>();
//5个任务
for (int i = 0; i < 10; i++) {
OrderThread o = new OrderThread("i");
Future<String> futureTask = executorPool.submit(o);
futureTasks.add((Future<String>) futureTask);
}
try {
//Callable中Call方法的返回值
for (Future<String> futureTask : futureTasks) {
String result = futureTask.get();
System.out.println(result);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executorPool.shutdown();//关闭线程池
}
}
线程池常见种类
java提供了几种常见的线程池创建方式,但是建议直接使用线程池ThreadPoolExecutor的构造器创建合适业务场景的线程池
FixThreadPool 可重用固定线程池
- 线程池的大小一旦达到“固定数量”就会保持不变
SingleThreadExcutor 单线程化的线程池
- 只有一个线程的线程池,
- 任务按照提交的次序顺序执行的
CachedThreadPool 可缓存线程池
- 线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小
线程池提交方式
execute():用于提交不需要返回值的任务;在启动任务执行后,任务执行过程中可能发生的异常调用者并不关心。适用Runable。
submit():用于提交需要返回值的任务,在线程执行完Submit提交的任务后,会返回一个 future 对象,这个 future 对象可以用来判断任务是否执行成功,并且可以用 future 的get()方法来获取其返回值,同时可以进行异步执行过程中的异常捕获。适用Callable。
线程池的关闭方式
shutdown 等待当前工作队列中的剩余任务全部执行完成之后,执行关闭
shutdownNow 立即关闭线程池,此方法会打断正在执行的工作线程,并且清空当前工作队列中的剩余任务,返回尚未执行的任务
awaitTermination 等待线程池完成关闭, shutdown()与shutdownNow()方法都不会主动等待线程池关闭完成
关于线程池的创建方式、参数调优以及各种不同场景的使用,可以参考高阶资料。
扩展
多线程或者线程池如何控制事务统一回滚:使用Callable,配合计数器发令枪CountDownLatch和子线程返回异常标志控制事务回滚。
注意事项
多线程开启后,主线程的变量不共享,特别注意比如登录信息等session信息会丢失。
线程安全问题,配合加锁或同步代码解决。