前言
最近开发中遇到一个问题,有一个请求非常慢,在本地环境需要6s左右,正式环境也需要1.5s以上,这个肯定不行,需要优化。
分析之后,发现接口需要5s左右,前端也没有使用异步请求,导致页面非常卡顿。
接口慢就需要针对接口进行优化,首先想到的就是SQL层面的优化,由于是一个很复杂的报表查询逻辑,数据库层面优化之后还是不理想,于是决定进行代码层面的处理。首先是将SQL进行了拆分,拆分成了三条SQL各自取值,然后在代码中合并计算。然后使用多线程,让三条SQL同步执行。最后优化后本地环境从最开始的6s左右的时间变成了1.5s以内,正式环境也从1.5s以上变成了800ms一下。
什么是多线程?
用到了多线程,那么就首先需要知道,什么是多线程。这里引用百度百科的解释。
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。
为什么要用多线程?
在我上面写道的【前言】中有写到我遇到的问题以及分析之后的结果还有我最终的处理方式。
显而易见,多线程能提升性能,但是它不止能提升性能,还有以下优点:
- 避免阻塞,提高I/O吞吐。典型的场景是,应用程序连接的Redis或者MySQL,它们提供的都是同步接口,一次只能处理一个请求。要想并发,办法是通过连接池和多线程,实现每个线程使用一个连接。好比在客户端和服务器之间开了多条通道,并行传输数据。
- 提高CPU利用率。通俗地讲,不能让CPU空闲着。当一个线程发生I/O时,会把该线程从CPU上调度下来,并把其他的线程调度上去,继续计算。
怎么使用多线程?
这里拿Java语言举例,Java中,创建一个线程需要继承Thread或者实现Runable接口。如果你用的JDK版本是1.5,你还能选择实现Callable接口或者Future接口,如果是1.8,还可以选择实现RunnableFuture接口。当然,它们有不同的用法,具体用法可以自行探索。
首先,根据业务需要选择合适的接口进行实现,有的业务需要返回值,有的不需要,视情况而定。我这里需要在线程结束后拿到返回值并且现在使用的JDK版本为1.8,我选择的是实现Callable接口。实现Callable接口主要是实现其中的call()方法,返回值与你所需要的返回值一致。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class OneThread implements Callable<String> {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
return this.name + ",执行了线程One。";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
OneThread oneThread = new OneThread();
oneThread.setName("张三");
FutureTask<String> futureTask = new FutureTask<String>(oneThread);
Thread thread = new Thread(futureTask);
thread.start();
String str = futureTask.get();
System.out.println(str);
}
}
这里能在控制台打印:张三,执行了线程One。
如果是不需要返回值,可以这么使用:
public class TwoThread implements Runnable {
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "执行了TwoThread");
}
public static void main(String[] args) {
TwoThread twoThread = new TwoThread();
twoThread.setName("李四");
Thread thread = new Thread(twoThread);
thread.start();
}
}
或者继承Thread重写run()方法:
public class MyThread extends Thread {
private String param;
public void setParam(String param) {
this.param = param;
}
@Override
public void run() {
System.out.println(this.param + "执行MyThread线程");
super.run();
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setParam("王五");
myThread.start();
}
}
如果需要返回值JDK的版本又比较低,那么还有种方法:
public class ThreeThread implements Runnable {
private String name;
private String result;
public void setName(String name) {
this.name = name;
}
public String getResult() {
return result;
}
@Override
public void run() {
this.result = this.name + "执行了线程Three";
}
public static void main(String[] args) throws InterruptedException {
ThreeThread threeThread = new ThreeThread();
threeThread.setName("赵六");
Thread thread = new Thread(threeThread);
thread.start();
thread.join();
String result = threeThread.getResult();
System.out.println(result);
}
}
RunnableFuture怎么使用可以自行探索。以上都是没有使用到线程池的,如果使用线程池,只需要改变一下调用方式即可,这里只贴一个例子,其他可以参考这个,如:
import java.util.concurrent.*;
public class OneThread implements Callable<String> {
public static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
return this.name + ",执行了线程One。";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
OneThread oneThread = new OneThread();
oneThread.setName("张三");
Future<String> submit = cachedThreadPool.submit(oneThread);
String str = submit.get();
System.out.println(str);
}
}
还有一个点需要注意,是阿里Java开发手册中提到的:
【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
上面的线程池使用Executors创建是为了方便。