使用多线程的目的
在日常的Java开发中,进程(app、web)的功能一般都是单一的线程(我们也叫主线程)。打个比方,例如一个功能需要打印十个用户的名字(可以理解为一个人在纸上手写十个人的名字),我们一般的做法都是在for循环里面print,这是单个线程完成的事情。我们知道cpu是分片执行的,也就是说cpu不会让某一个进程的功能重头到尾执行结束再去执行其他进程的功能,而是再进程之间不断的来回切换。但是一个进程某些功能可能是一个或者多个线程。
因此,那我们如何让一个人在纸上手写十个人的名字,变成十个人每个人手写一个人的名字呢(也就是将单个线程做的事情变成多线程执行),这样将会使我们的效率达到十倍。
本章将会讲解通过 Runnable 接口, Thread 类, Callable接口、FutureTask 三种方式创建线程。线程池将在下面的一章详细讲述。
如何使用多线程
线程对象为Thread类,而Thread类中的run()方法则是线程获取到cpu资源时运行的方法,因此想要让线程执行自己的业务方法时,需要通过其他的办法来重写run()方法。可以通过实现Runnable接口,继承Thread类等
Thread(){
...省略上面其他方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
...省略下面其他方法
}
初始化执行数据
初始化业务需要处理的数据
{
//初始化需要执行的数据 -> userList
User zhangSan = new User(1,"zhangSan",21);
User liSi = new User(2,"liSi",22);
User wangWu = new User(3,"wangWu",23);
User zhaoLiu = new User(4,"zhaoLiu",24);
List<User> userList = new ArrayList<>();
userList.add(zhangSan);
userList.add(liSi);
userList.add(wangWu);
userList.add(zhaoLiu);
}
2、实现Runnable接口
创建线程类(每new一个 RunnableThreadTest 对象 相当于创建一个Thread)
package com.example.ThreadTest;
import com.example.exceldemo.User;
import lombok.Data;
/**
* 打印用户名的线程
* @Author huangdy
* @Date 2022/1/12 16:56
*/
@Data
public class RunnableThreadTest implements Runnable{
/**
* 当前线程对象
*/
private Thread thread;
/**
* 当前线程名称
*/
private String threadName;
/**
* 线程需要处理的数据
*/
private User user;
/**
* 有参构造
*/
public RunnableThreadTest(String threadName, User user) {
//创建线程
this.thread = new Thread(this);
//线程名赋值
this.threadName = threadName;
//线程执行数据
this.user = user;
}
/**
* 绪状态线程,获取cpu资源之后调用该方法
* 通过重写run()方法执行我们需要实际的处理业务
*/
@Override
public void run() {
//实际业务 -> 打印用户名称
System.out.println("当前线程 --->" + this.threadName + ", 执行对象 ---->" + this.user.getUserName());
}
/**
* 1、调用线程
* 只有调用线程的start()方法,线程才会真正进入就绪状态等待cpu资源
*/
public void startPrint(){
//调用线程
thread.start();
}
}
多线程执行业务 -> 打印用户名称
//多线程执行业务 -> 打印用户名称
int index = 0;
for (User user : userList) {
//创建线程
RunnableThreadTest thread = new RunnableThreadTest("thread-" + index, user);
//调用线程的start()方法
thread.start();
++index;
}
System.out.println("主线程");
执行结果
通过执行结果可以明白,for循环执行时,并不是依次执行(不会等待第0个线程执行后再依次执行1、2、3…),而是将需要处理的数据交给每一个线程之后,线程竞争cpu资源来执行各自需要处理的数据
主线程
当前线程 --->thread-0, 执行对象 ---->zhangSan
当前线程 --->thread-3, 执行对象 ---->zhaoLiu
当前线程 --->thread-1, 执行对象 ---->liSi
当前线程 --->thread-2, 执行对象 ---->wangWu
3、继承Thread类本身
package com.example.ThreadTest;
import com.example.exceldemo.User;
/**
* @Author huangdy
* @Date 2022/1/13 14:29
*/
public class ThreadTest extends Thread{
/**
* 当前线程对象
*/
private Thread thread;
/**
* 当前线程名称
*/
private String threadName;
/**
* 线程需要处理的数据
*/
private User user;
/**
* 有参构造
*/
public ThreadTest(String threadName, User user) {
//创建线程
this.thread = new Thread(this);
//线程名赋值
this.threadName = threadName;
//线程执行数据
this.user = user;
}
/**
* 绪状态线程,获取cpu资源之后调用该方法
* 通过重写run()方法执行我们需要实际的处理业务
*/
@Override
public void run() {
//实际业务 -> 打印用户名称
System.out.println("当前线程 --->" + this.threadName + ", 执行对象 ---->" + this.user.getUserName());
}
/**
* 1、调用线程
* 只有调用线程的start()方法,线程才会真正进入就绪状态等待cpu资源
*/
public void start(){
//调用线程
thread.start();
}
}
多线程执行业务 -> 打印用户名称
//多线程执行业务 -> 打印用户名称
int index = 0;
for (User user : userList) {
//创建线程
ThreadTest thread = new ThreadTest("thread-" + index, user);
//调用线程的start()方法
thread.start();
++index;
}
System.out.println("主线程");
执行结果
主线程
当前线程 --->thread-1, 执行对象 ---->liSi
当前线程 --->thread-0, 执行对象 ---->zhangSan
当前线程 --->thread-2, 执行对象 ---->wangWu
当前线程 --->thread-3, 执行对象 ---->zhaoLiu
4、通过 Callable 和 Future 创建线程
实现Callable接口,比较重要的就是实现call()方法,这里的call()方法就是我们编写需要处理的业务。执行的String为返回值类型
package com.example.ThreadTest;
import com.example.exceldemo.User;
import lombok.Data;
import java.util.concurrent.Callable;
/**
* @Author huangdy
* @Date 2022/1/13 15:02
*/
@Data
public class CallableTest implements Callable<String> {
/**
* 当前线程名称
*/
private String threadName;
/**
* 当前线程需要处理的数据
*/
private User user;
/**
* 构造参数
* @param threadName
* @param user
*/
public CallableTest(String threadName, User user) {
this.threadName = threadName;
this.user = user;
}
/**
* call()方法作为线程执行体,并且有返回值
*/
@Override
public String call() throws Exception {
//实际业务
System.out.println(this.threadName + ", 业务执行中...");
//返回结果
return "当前线程 --->" + this.threadName + ", 执行对象 ---->" + this.user.getUserName();
}
}
多线程执行任务
1、创建 CallableTest 实例 ,callableTest 对象里面的call()方法,就是每个线程需要处理的业务,通过有参构造是为了让不同线程处理不同的数据。
2、为什么要创建 FutureTask 对象来封装callable对象?上面我们说过线程获取cpu资源之后需要执行的方法是run()方法。实际上 FutureTask 类是实现了 Runnable 接口,并重写了run()方法,最重要的一点是,FutureTask 类的run()方法里面调用了 Callable 对象的call()方法。也就是说我们用来执行业务重写的call()会在 FutureTask 的run()方法里面被真正执行。
//多线程执行业务 -> 打印用户名称
int index = 0;
for (User user : userList) {
//创建callable
CallableTest callableTest = new CallableTest("thread-" + index, user);
//封装callable
FutureTask<String> futureTask = new FutureTask(callableTest);
try {
//创建线程
Thread thread = new Thread(futureTask);
//执行线程
thread.start();
//线程返回值
String result = futureTask.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
++index;
}
System.out.println("主线程");
执行结果
这里你会发现一个比较有意思的地方就是,线程按顺序执行,并且主线程是最后才执行结束。导致这种结果的原因是因为上面的代码调用的 futureTask.get() 方法,使线程进入了阻塞状态。当 FutureTask 处于未启动(未调用start方法)或已启动状态时(已调用run方法),执行 futureTask.get() 方法将导致调用线程阻塞。如果 FutureTask 处于已完成状态,调用 futureTask.get() 方法将导致调用线立马返回结果。因此个人不建议使用 futureTask.get() 。
thread-0, 业务执行中...
当前线程 --->thread-0, 执行对象 ---->zhangSan
thread-1, 业务执行中...
当前线程 --->thread-1, 执行对象 ---->liSi
thread-2, 业务执行中...
当前线程 --->thread-2, 执行对象 ---->wangWu
thread-3, 业务执行中...
当前线程 --->thread-3, 执行对象 ---->zhaoLiu
主线程
5、使用线程池创建线程
为什么要使用线程池创建线程?通过上面的例子不难看出来,假如我的 userList size为100万,那么在上面的代码中将会不断的创建线程和销毁线程,大量的时间花费在创建和销毁上,反而会导致效率低下,cpu资源的浪费。甚至可能出现100w个线程的执行时间 大于 单线程执行时间。
因此,线程池不仅仅解决了这个原因,甚至带来了其他许多好处。线程池将会创建固定大小的线程,并且线程在使用完毕之后不会立马销毁,而是进行反复的使用。在实际的开发中,基本也是常用线程池解决大数据量问题。线程池的详情使用以及优缺点本章不再介绍,将会在后面的篇章中进行详细讲解。
同时欢迎大家指正文章的缺陷和不足。