多线程的优点
多线程能给程序带来比较多的好处,粗略概括如下几点:
- 提高资源利用率
- 提高程序响应性能
- 简化程序设计
提高资源利用率比较好理解,现代服务器都是多核CPU,单线程并不能充分发挥多核CPU的优势,要提高资源利用率,自然是充分利用CPU,那多线程就是最好的方式。一个人干活总是不如一帮人干活快的。生活中到处都是多线程工作提高效率的例子,试想一个场景,需要将100块砖头从1楼搬到5楼,一个人搬,一次只能搬10块砖头,那这个人他得搬10趟。假如他叫来10个人一起搬,那一个人只需要搬一趟,效率的上来了。
以上面搬砖的例子,分别以单线程和多线程编写程序,对比看看效率分别如何
一个人搬砖
import cn.hutool.core.util.ArrayUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
/**
* @author kangming.ning
* @date 2023-02-15 15:43
* @since 1.0
**/
public class MoveBrickSingleThreadTest {
private static Queue<Integer> brickQueue = new ArrayBlockingQueue<>(100);
static {
//准备100块砖头
for (int i = 1; i <= 100; i++) {
brickQueue.offer(i);
}
}
public static void main(String[] args) throws InterruptedException {
//记录总次数
int count = 1;
//记录耗时
long current = System.currentTimeMillis();
while (brickQueue.size() > 0) {
System.out.println(Thread.currentThread().getName() + " 小伙子开始搬砖,第" + count++ + "趟");
List<Integer> brickList = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
Integer poll = brickQueue.poll();
brickList.add(poll);
}
Thread.sleep(2 * 1000);
System.out.println(Thread.currentThread().getName() + " 小伙子已经把砖头" + ArrayUtil.toString(brickList) + "搬好了");
System.out.println("当前还剩下" + brickQueue.size() + "个砖头需要搬");
}
System.out.println("搬完收工,总耗时:" + (System.currentTimeMillis() - current));
}
}
打印如下
main 小伙子开始搬砖,第1趟
main 小伙子已经把砖头[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]搬好了
当前还剩下90个砖头需要搬
main 小伙子开始搬砖,第2趟
main 小伙子已经把砖头[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]搬好了
当前还剩下80个砖头需要搬
main 小伙子开始搬砖,第3趟
main 小伙子已经把砖头[21, 22, 23, 24, 25, 26, 27, 28, 29, 30]搬好了
当前还剩下70个砖头需要搬
main 小伙子开始搬砖,第4趟
main 小伙子已经把砖头[31, 32, 33, 34, 35, 36, 37, 38, 39, 40]搬好了
当前还剩下60个砖头需要搬
main 小伙子开始搬砖,第5趟
main 小伙子已经把砖头[41, 42, 43, 44, 45, 46, 47, 48, 49, 50]搬好了
当前还剩下50个砖头需要搬
main 小伙子开始搬砖,第6趟
main 小伙子已经把砖头[51, 52, 53, 54, 55, 56, 57, 58, 59, 60]搬好了
当前还剩下40个砖头需要搬
main 小伙子开始搬砖,第7趟
main 小伙子已经把砖头[61, 62, 63, 64, 65, 66, 67, 68, 69, 70]搬好了
当前还剩下30个砖头需要搬
main 小伙子开始搬砖,第8趟
main 小伙子已经把砖头[71, 72, 73, 74, 75, 76, 77, 78, 79, 80]搬好了
当前还剩下20个砖头需要搬
main 小伙子开始搬砖,第9趟
main 小伙子已经把砖头[81, 82, 83, 84, 85, 86, 87, 88, 89, 90]搬好了
当前还剩下10个砖头需要搬
main 小伙子开始搬砖,第10趟
main 小伙子已经把砖头[91, 92, 93, 94, 95, 96, 97, 98, 99, 100]搬好了
当前还剩下0个砖头需要搬
搬完收工,总耗时:20106
主线程小伙花了10趟才能把砖搬完,总耗时达到20106毫秒。下面再看多线程搬砖效率如何。
多个人搬砖
import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.util.ArrayUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
/**
* @author kangming.ning
* @date 2023-02-15 15:44
* @since 1.0
**/
public class MoveBrickMultiThreadTest {
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().setNamePrefix("搬砖工-").build();
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
10,
20,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
private static Queue<Integer> brickQueue = new ArrayBlockingQueue<>(100);
static {
//准备100块砖头
for (int i = 1; i <= 100; i++) {
brickQueue.offer(i);
}
}
public static void main(String[] args) throws Exception {
//记录总次数
int count = 1;
//记录耗时
long current = System.currentTimeMillis();
List<FutureTask<String>> taskList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Callable<String> future = new MoveBrickTask();
FutureTask<String> stringFutureTask = new FutureTask<>(future);
taskList.add(stringFutureTask);
//提交任务
threadPoolExecutor.execute(stringFutureTask);
}
for (FutureTask<String> task : taskList) {
System.out.println(task.get() + " 完成了搬砖任务");
}
System.out.println("搬完收工,总耗时:" + (System.currentTimeMillis() - current));
threadPoolExecutor.shutdown();
}
static class MoveBrickTask implements Callable<String> {
@Override
public String call() throws Exception {
//记录当前线程执行任务次数
int count = 1;
String threadName = Thread.currentThread().getName();
while (brickQueue.size() > 0) {
System.out.println(threadName + " 小伙子开始搬砖,第" + count++ + "趟");
List<Integer> brickList = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
Integer poll = brickQueue.poll();
brickList.add(poll);
}
Thread.sleep(2 * 1000);
System.out.println(Thread.currentThread().getName() + " 小伙子已经把砖头" + ArrayUtil.toString(brickList) + "搬好了,当前还剩下" + brickQueue.size() + "个砖头需要搬");
}
return threadName;
}
}
}
上面代码,通过把任务放到线程池,用多个线程同时执行,模拟多人同时搬砖的场景。我们看一下打印的结果
搬砖工-1 小伙子开始搬砖,第1趟
搬砖工-0 小伙子开始搬砖,第1趟
搬砖工-2 小伙子开始搬砖,第1趟
搬砖工-3 小伙子开始搬砖,第1趟
搬砖工-4 小伙子开始搬砖,第1趟
搬砖工-5 小伙子开始搬砖,第1趟
搬砖工-6 小伙子开始搬砖,第1趟
搬砖工-7 小伙子开始搬砖,第1趟
搬砖工-8 小伙子开始搬砖,第1趟
搬砖工-9 小伙子开始搬砖,第1趟
搬砖工-4 小伙子已经把砖头[41, 42, 43, 44, 45, 46, 47, 48, 49, 50]搬好了,当前还剩下0个砖头需要搬
搬砖工-5 小伙子已经把砖头[51, 52, 53, 54, 55, 56, 57, 58, 59, 60]搬好了,当前还剩下0个砖头需要搬
搬砖工-0 小伙子已经把砖头[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]搬好了,当前还剩下0个砖头需要搬
搬砖工-6 小伙子已经把砖头[61, 62, 63, 64, 65, 66, 67, 68, 69, 70]搬好了,当前还剩下0个砖头需要搬
搬砖工-2 小伙子已经把砖头[21, 22, 23, 24, 25, 26, 27, 28, 29, 30]搬好了,当前还剩下0个砖头需要搬
搬砖工-3 小伙子已经把砖头[31, 32, 33, 34, 35, 36, 37, 38, 39, 40]搬好了,当前还剩下0个砖头需要搬
搬砖工-1 小伙子已经把砖头[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]搬好了,当前还剩下0个砖头需要搬
搬砖工-8 小伙子已经把砖头[81, 82, 83, 84, 85, 86, 87, 88, 89, 90]搬好了,当前还剩下0个砖头需要搬
搬砖工-0 完成了搬砖任务
搬砖工-1 完成了搬砖任务
搬砖工-2 完成了搬砖任务
搬砖工-3 完成了搬砖任务
搬砖工-4 完成了搬砖任务
搬砖工-5 完成了搬砖任务
搬砖工-6 完成了搬砖任务
搬砖工-7 小伙子已经把砖头[71, 72, 73, 74, 75, 76, 77, 78, 79, 80]搬好了,当前还剩下0个砖头需要搬
搬砖工-9 小伙子已经把砖头[91, 92, 93, 94, 95, 96, 97, 98, 99, 100]搬好了,当前还剩下0个砖头需要搬
搬砖工-7 完成了搬砖任务
搬砖工-8 完成了搬砖任务
搬砖工-9 完成了搬砖任务
搬完收工,总耗时:2024
从总耗时可以看出,效率极大提高,每个人也就是搬一趟就可以了。
提高程序响应性能。比如在GUI程序中,UI线程负责界面的显示,网络请求数据时可能会使用新的线程,获取结果后将数据显示到界面上。如果在UI线程去请求网络数据,那么由于网络IO等待,应用界面就不能响应其它事件了,给人感觉就是卡住了,这对用户来说是不可接受的。
在某些情况下,多线程还能简化程序的设计。线程通常需要处理的任务可简单分为CPU密集型任务,耗时IO型任务。CPU密集型任务通常需要大量的计算,因此比较耗CPU资源,而IO型任务,通常是一些网络请求任务,这些任务耗时,不怎么耗费CPU资源,因为在等待IO数据时,CPU是可以被调度去干别的活的。多线程程序则很容易将这些不同类型的任务交由不同的线程池去处理,这样能最大提高资源利用率,并且更容易设计。相比需要同时管理多种类型的任务,一个顺序处理相同任务类型的程序,写起来更简单,更不容易出错,也更易于测试。
多线程的代价
多线程的代价主要有以下几点:
- 程序设计变复杂
- 上下文切换成本增加
- 资源消耗增加
多线程主要带来的代价就是程序设计变复杂了。如果是多线程编程,那么开发者在设计程序时得关注在多线程环境下,共享变量是否能按预期得到正确的结果,需要通过锁或其它机制来保证程序的正确。而单线程程序则没这么复杂,因为数据只有自己在用,绝不会有其他线程干扰。
另外,多线程相对于单线程来说,会有更多的线程上下文切换成本。当然,单线程也是会出现上下文切换,但更多的线程意味着更多的上下文切换。一条线程,只有有CPU分配了时间片,才能得以执行。当时间片用完,CPU被调度去执行其它线程,此时,当前线程就得通过程序计数器保存当前执行到哪里了,通过本地局部变量表保存本地变量数据,也就是线程执行现场得保留,这个现场就是上下文。然后等待CPU再次执行当前线程,CPU通过程序计数器得知接下来需要执行的代码,继续执行任务。这些过程就是线程的上下文切换,会带来一定的成本。有时候单线程能比多线程表现得更好,这是因为单线程没过多的线程切换成本,但这个都是看场景的。大多数场景,可能多线程都是要比单线程更优。Redis早期的设计就是使用单线程模型而非多线程也是从实际的场景出发考虑,比如单线程能带来更好的可维护性,方便开发和调试。
最后就是资源消耗增加,这个是自然的,因为多线程就是为了更好更充分的利用资源。每个进程启动后,操作系统都要给进程分配一定的内存,每一个线程启动后,进程也要给线程分配一定的内存,让线程来保存自己的私有数据。所以更多的线程会占用更多内存,但对于大内存服务器来讲,这些内存成本还是比较小的。