开发过程中的多线程思维方式_多线程程序开发
作为一个多项目管理者,我每天经常有大量并行的工作内容,尤其是以前在工作里同时协调着十几个经销商、技术外包、设计团队和工厂、还同时进行着公司内部的开发和管理工作的时候,曾经一度崩溃。
以我的经验来看,最有效的办法是让自己养成一种“多线程工作思维”。
我们的时间都去哪了?
确实能感觉到,这些年大家工作普遍越来越忙,同时要处理的工作任务变多也变得越来越常见。
可每当这些时候,相信你和我都有一个感受,那就是:如果每个工作任务的工作量是1,那么一起来5个并行工作任务的工作时间,往往是大于5的,甚至远大于5。
那时间到底消耗在了哪些地方?
其实这种情况对于机器来言也是一样的,如何最大化利用一切可以利用的时间!
之前对接某部的数据平台时需要在规定时间内进行增量数据的访问文件、压缩、切片、传输等操作,然后频繁执行上链操作,经常遇到线程阻塞超时等现象,所以采用了多线程的方式来处理该业务逻辑;并且在与银行等机构进行数据交互时通常也会用到多线程操作。
创建线程对象不像其他对象一样在JVM分配内存即可,还要调用操作系统内核的API,然后操作系统为线程分配一系列的资源,这个成本就很高了。所以线程是一个重量级对象,如同数据库连接一样,应该避免频繁创建和销毁等操作,这个操作其实完全可以和类似C3P0数据库连接池等同对待。
多线程使用的主要目的在于?
1、吞吐量:做WEB开发的话,使用的中间件容器帮我们做了多线程,tomcat内部采用的就是多线程,但是也只能帮我们做到请求层面。换句话说就是一个请求一个线程或者多个请求一个线程。如果是单线程,那就同时只能处理一个用户的请求。如果不采用多线程机制,上百个人同时访问一个web应用的时候,tomcat就得排队串行处理了,那样客户端根本是无法忍受那种访问速度的。
2、伸缩性:也就是说,可以通过增加CPU的核数来提升性能。如果是单线程,那么程序执行到死也就利用了单核,肯定没办法通过增加CPU核数来提升性能。
因为我们都是做web开发的,第1点可能我们几乎不会涉及,所以这里主要说下第2点。
假设有个请求,这个请求服务端的处理需要执行3个很缓慢的IO操作(比如数据库查询或文件查询),那么正常的顺序可能是(括号里面代表粗略估算执行时间):
a、读取文件1 (10ms)
b、处理1的数据(1ms)
c、读取文件2 (10ms)
d、处理2的数据(1ms)
e、读取文件3 (10ms)
f、处理3的数据(1ms)
g、整合1、2、3的数据结果 (1ms)
单线程总共就需要34ms。
那如果你在这个请求内,把ab、cd、ef分别分给3个线程去做,就只需要12ms了。
所以多线程不是没怎么用,而是,平常要善于发现一些可优化的点。然后评估方案是否应该使用。
假设还是上面那个相同的问题:但是每个步骤的执行时间不一样了。
a、读取文件1 (1ms)
b、处理1的数据(1ms)
c、读取文件2 (1ms)
d、处理2的数据(1ms)
e、读取文件3 (28ms)
f、处理3的数据(1ms)
g、整合1、2、3的数据结果 (1ms)
单线程总共就需要34ms。
如果还是按上面的划分方案(上面方案和木桶原理一样,耗时取决于最慢的那个线程的执行速度),在这个例子中是第三个线程,执行29ms。那么最后这个请求耗时是30ms。比起不用单线程,就节省了4ms。但是有可能线程调度切换也要花费个1、2ms。因此,这个方案显得优势就不明显了,还带来程序复杂度提升。不太值得。
那么现在优化的点,就不是第一个例子那样的任务分割多线程完成。而是优化文件3的读取速度,当然也可以是采用缓存和减少一些重复读取来避免。
首先,假设有一种情况,所有用户都请求这个请求,那其实相当于所有用户都需要读取文件3。那你想想,100个人进行了这个请求,相当于你花在读取这个文件上的时间就是28×100=2800ms了。那么,如果你把文件缓存起来,那只要第一个用户的请求读取了,第二个用户不需要读取了,从内存取是很快速的,可能1ms都不到。
public class MyService {
private static Map<String, String> fileName2Data = new HashMap<String, String>();
private void processFile(String fileName) {
String data = fileName2Data.get(fileName);
if (data == null) {
data = readFromFile(fileName);// 耗时28ms
fileName2Data.put(fileName, data);
}
// TODO:process with date
}
private String readFromFile(String fileName) {
return "";
}
}
看起来好像还不错,建立一个文件名和文件数据的映射。如果读取一个map中已经存在的数据,那么就不用读取文件了。
可是问题在于,服务请求是并发操作,上面会导致一个很严重的问题:死循环。因为,HashMap在并发修改的时候,可能是导致循环链表的构成。(具体可以自行阅读HashMap源码)。
那就用ConcurrentHashMap吧,Concurrent意思是并存的、同时发生的;他是一个线程安全的HashMap,这样就能轻松解决问题。
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class MyService {
private static ConcurrentMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();
private void processFile(String fileName) {
String data = fileName2Data.get(fileName);
if (data == null) {
data = readFromFile(fileName);// 耗时28ms
fileName2Data.put(fileName, data);
}
// TODO:process with date
}
private String readFromFile(String fileName) {
return "";
}
}
这样虽然只要有用户访问过文件a,那另一个用户想访问文件a,也会从fileName2Data中拿数据,然后也不会引起死循环。可是,你会发现一个问题,1000个用户首次访问同一个文件的时候,居然读取了1000次文件(这是最极端的,可能只有几百)。
因为Servlet是多线程的,那么下面注释的“偶然事件”,这是完全有可能的,因此,这样做还是有问题。
private void processFile(String fileName) {
String data = fileName2Data.get(fileName);
// "偶然事件"--1000个线程同时执行到这里并且同时发现为null
if (data == null) {
data = readFromFile(fileName);// 耗时28ms
fileName2Data.put(fileName, data);
}
// TODO:process with date
}
因此,可以自己简单的封装一个任务来处理。接口
util.concurrent.ExecutorService 表述了异步执行的机制,并且可以让任务在后台执行。
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
public class MyService {
private static ConcurrentMap<String, FutureTask> fileName2Data = new ConcurrentHashMap<String, FutureTask>();
private static ExecutorService exec = Executors.newCachedThreadPool();
private void processFile(String fileName) {
FutureTask data = fileName2Data.get(fileName);
// "偶然事件"--1000个线程同时执行到这里并且同时发现为null
if (data == null) {
data = newFutureTask(fileName);
FutureTask old = fileName2Data.putIfAbsent(fileName, data);
if (old == null) {
exec.execute(data);
}
}
String str = data.get();
// TODO:process with date
}
private FutureTask newFutureTask(final String fileName) {
return new FutureTask(new Callable<String>() {
public String call() {
return readFromFile(fileName);
}
private String readFromFile(String fileName) {
return "";
}
});
}
}
多线程最多的场景:web服务器本身;各种专用服务器(如游戏服务器);
多线程的常见应用场景:
1、后台任务,例如:定时向大量(100w以上)的用户发送邮件;
2、异步处理,例如:发微博、记录日志等;
3、分布式计算等