实现多线程其实有很多种方式,大体上可以分为以下四种:
一、继承Thread类
通过继承Thread类的方式可以实现多线程,在这里模拟一下通过继承Thread类的方式来实现多线程消费业务数据:
- 定义一个类,继承Thread,并实现消费逻辑
/**
* 消费者
*/
@Data
@Slf4j
public class Consumer extends Thread {
/**
* 消费数据的过程
*/
@Override
public void run() {
log.info(Thread.currentThread().getName() + "正在消费数据!!!");
try {
Thread.sleep(1000L);
} catch (Exception e) {
log.info(Thread.currentThread().getName() + "数据消费出错!!!");
}
}
}
- 使用进行多线程消费
/**
* 消费数据
*/
@Override
public void comsumerData() {
// 启动10个线程进行消费数据
int maxThreadNum = 10;
for (int i = 0; i < maxThreadNum; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
}
也可以使用自定义的线程池进行消费
/**
* 自定义线程池
*/
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
/**
* 消费数据
*/
@Override
public void comsumerData() {
// 启动10个线程进行消费数据
int maxThreadNum = 10;
for (int i = 0; i < maxThreadNum; i++) {
Consumer consumer = new Consumer();
threadPoolExecutor.execute(consumer);
}
}
关于自定义线程池,定义方式以及参数构建可以参考《并发编程-线程池》.
二、实现Runnable接口
通过实现Runnable接口的方式,同样能够实现多线程.
- 定义一个类,实现Runnable接口,并且重写其run方法.
/**
* 消费者
*/
@Data
@Slf4j
public class Consumer02 implements Runnable {
/**
* 消费数据的逻辑
*/
@Override
public void run() {
log.info(Thread.currentThread().getName() + "正在消费数据!!!");
try {
Thread.sleep(1000L);
} catch (Exception e) {
log.info(Thread.currentThread().getName() + "数据消费出错!!!");
}
}
}
- 开启线程任务
Consumer02 threadTest01 = new Consumer02();
for (int i = 0; i < 10; i++) {
new Thread(threadTest01).start();
}
上面这种方式其实是不建议使用的,无法控制线程的数量等,可能会带来较大的影响,推荐使用自定义线程池的方式创建。
- 业务层,利用自定义的线程池,开启多线程处理数据
/**
* 自定义线程池
*/
@Autowired
private ThreadPoolExecutor threadPoolExecutor;
/**
* 消费数据
*/
@Override
public void consumerData() {
//模拟10次请求发送过来
for (int i = 0; i < 10; i++) {
//使用自定义的线程池对数据进行处理
Consumer02 consumer02 = new Consumer02();
threadPoolExecutor.execute(consumer02);
}
}
实现Runnable接口的方式与集成Thread的方式相比:
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
相比较之下,具有一定的优势,能够解决Java单继承所带来的缺陷.
三、实现Callable接口
对比,前两种方式创建线程的方式来说,这种方式的有点在于,它可以获取到线程任务执行后的返回值。
- 创建线程任务对象,并重写run
/**
* @Author: yichangqiao
* @Date: 2024/1/22 15:46
*/
@Data
@Slf4j
public class ThreadTest02 implements Callable<Integer> {
/**
* 业务逻辑代码
*
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
log.info(Thread.currentThread().getName() + "正在消费数据!!!");
try {
Thread.sleep(1000L);
return 200;
} catch (Exception e) {
log.info(Thread.currentThread().getName() + "数据消费出错!!!");
}
return 500;
}
}
- 使用该对象开启多线程执行
public static void testCallable() {
// 创建线程任务对象
ThreadTest02 threadTest02 = new ThreadTest02();
// 利用futureTask开启线程任务
FutureTask<Integer> integerFutureTask = new FutureTask<>(threadTest02);
for (int i = 0; i < 10; i++) {
// 开启线程执行任务
new Thread(integerFutureTask).start();
try {
Integer result = integerFutureTask.get();
log.info("线程{}执行结果:{}", i, result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
testCallable();
}
这里的执行与其他两种方式的执行不同,实现Callable接口的线程任务,需要使用futureTask来进行初始化,并且使用futureTask来获取线程任务的返回结果。
注意:FutureTask的返回结果获取,是一个阻塞式的,如果长时间获取不到返回结果,线程一直不会释放,可能导致线程数越来越多,有两种解决方式:
- 指定获取超时时间,超过多长时间没获取到返回结果,就不再获取了。
Integer result = integerFutureTask.get(4,TimeUnit.SECONDS);
- 使用线程池。
四、线程池
线程池的创建需要结合实际的业务以及资源情况进行创建,这里只做一个简单的示例:
- 创建线程池
/**
* 自定义线程池
*/
@Slf4j
@Configuration
public class ThreadPoolConfig {
@Bean(name = "threadPoolExecutor")
public ThreadPoolExecutor threadPoolExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
//核心线程数量
10,
//最大线程数
20,
//临时线程最大存活时间
10,
//存活时间单位
TimeUnit.SECONDS,
//任务等待队列
new ArrayBlockingQueue<>(20),
//创建线程到的线程工厂
Executors.defaultThreadFactory(),
//拒绝策略
new ThreadPoolExecutor.AbortPolicy());
log.info("线程池初始化完成 ===========> {}", LocalDateTime.now());
return executor;
}
}
- 使用线程池
// 假设10个请求同时发过来
int maxThreadNum = 10;
for (int i = 0; i < maxThreadNum; i++) {
// consumer实现多线程
Consumer consumer = new Consumer();
threadPoolExecutor.execute(consumer);
}
关于线程池的详细介绍和操作,可以看《并发编程-线程池》一文。