之前写过一篇手记 面试中并发类问题的准备和学习 ,得到许多学员不错的反馈。但依然有这样的问题,许多人已经知道并发有这些内容,也知道这些知识点该如何用,但是却不知道该如何在实际项目中使用。基于以上,这篇手记将在之前手记的基础上,继续介绍多线程并发的使用、学习与测试。
也希望这篇手记能帮助 Java并发编程与高并发解决方案 的学员更好的学习课程。
使用先说说多线程的使用。多线程本身是与我们开发的项目密不可分的,我们只要提供了接口,那么他就可能被多个线程同时调用,就会产生并发问题,因此开发者在实际开发中对多线程的理解至关重要。当然,这属于最基本的认识。
就这一方面而言,对于开发者及学过并发的学员,平时开发中需要掌握的主要包括:
- 先说涉及最多的。看到static修饰的变量时,能想到涉及到多线程资源共享。如果是可变的,需要注意变化中的线程安全;如果是不可变的,需要知道如何保证其不可变,而不是简单的放在那,当前代码没有修改他就认为他是不变的。
- 看到final、单例等定义时,能分析出是否是线程安全的。
- 在看到集合、Map等结果时候时,知道该选什么类更适合,以及什么时候适合使用并发类,什么时候适合使用普通的类,这里本质上有对线程封闭的理解。
- 在遇到SimpleDateFormat等线程不安全的类时,能知道相关的并发风险点
- 在需要完成一些大任务时,知道如何借助线程池等进行提速,以及借助AQS相关组件进行一些线程的调度
- 知道常用的线程安全手段,能根据场景去分析是否需要加锁来保证线程安全。
说了这么多,相信很多人已经能感受到并发在日常开发中的存在,也能对并发多一些认识。虽然你可能很难用上并发一些手段,但你需要知道你看到的代码是否有问题,并发问题更多的存在于一些细节的处理中。这也是很多学员问我为什么不出一门关于并发实战性质的课程、而是选择根据在知识点的讲解过程中介绍使用他们的一些场景的愿意。同时,由于并发的知识点多而繁杂,单独一门课程的几个场景很难覆盖整个并发知识体系,这样直接会导致许多人学完了并发却还是和之前一样,只是巩固了一遍知识点,别人问起来心里依旧很虚。而且,如果为了覆盖知识点去刻意的在某些场景中使用并发的某些知识点,反而可能会造成很多误解,效果并不一定理想。
继续谈多线程的使用。之前介绍的是我们不得不处理的多线程,接下来介绍主动创建的多线程的使用,即我们在代码里主动使用多线程去处理一些业务。
- 提速,充分利用CPU:这个理解起来应该比较容易,同一个任务,多个线程同时处理,让很多处理并行,达到更快完成的目的。这里介绍的一个场景,之前项目做大发布,要清洗历史数据(比如要根据id取出某一行,并计算某个值填进去),由于数据量很大,单线程清洗可能要1个小时左右,为了能尽快完成,不影响发布的时间,因此我们把单线程改为10个线程并行,测试发现10个线程还是有些慢,后来调整为20个线程同时处理,这样最终在3min左右完成。
- 降速,协调资源的使用:刚说完提速,这里又说降速,许多人可能会很奇怪。这里举具体例子来说明一下,比如项目里要发短信,目前有第三方的限制是并发量最多10条,而每天高峰期时段同一时刻需要发送的短信量可能远高于10条,这时怎么办呢?就可以在发短信时引入线程池及多线程池处理,通过semaphore等控制同时发短信的线程不超过10,这样就起到了降速的作用。
- 异步处理,解耦。通常我们在做一个业务实现时,都有主流程和次流程之分,比如我们用户下单时,同时还要完成记录核心日志(核心的日志可能要记录的数据库中)以及通知用户下单完成等非核心业务,这些非核心的业务在处理时,有时可能会很耗时,出任何错还不能让主流程失败,出错了还需要重试慢慢完成,这种的就可以开启新的线程去处理,异步解耦,让主流程快速完成并返回。当然,也不能无限制的开启新线程,放在线程池里控制更好。
许多人不知道如何使用多线程,希望这几点总结能让加深你的理解。关于多线程的使用,在什么场景该使用什么场景不该使用不是背下来的,而是需要结合实际场景分析,是否需要进行提速、降速、异步处理等,如此才能在工作中较好的使用多线程。
学习之前在面试中并发类问题的准备和学习大致介绍过,这里就不做过多的讲解,做个简单的总结:
还是课程里这张图,这张图不是简单的覆盖了并发的知识点,而是希望你能将这幅图印在脑子里。当你向别人讲述并发时,能先从整体描述并发的几个大方面,然后能挨个大方面进行细化。这也是你学习并发比较好的方法,脑海中有这个图,你就可以迅速的把知识点过一遍,哪个知识点模糊了你可以去补充一下就可以了,而不是每次需要学习并发时都是从头来一遍,而且每次看完都感觉还差点啥。
这个方法呢,也特别适合在面试中使用。面试官在让你说说对并发的了解时,你能先整体概括再局部细化,会让你在这个环节大大加分。不少学员已经从中受益,希望这个也能帮助到你。当然,这种先整体概括再局部细化的方式,也适合大家学习其他方面的知识,会让你的记忆更深刻,后续巩固也容易很多。
测试说了并发的使用与学习,之后就是验证了,也属于并发的测试。并发的测试通常可以选择通过接口和方法两个方向去验证,可以根据不同需要进行选择。
先说根据接口测试。这种的通常要借助工具,Postman、JMeter、Apache Bench(AB)等都是不错的选择,其中JMeter、Apache Bench(AB)更适合测试人员来使用,Postman更适合开发人员来使用。
再说说根据方法测试。方法的测试更适合写代码去测试,这里给出一个模板:
public class ConcurrencyTest {
// 请求的总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
testMethod();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
// 所有线程执行完,之后才能执行的部分
}
private static void testMethod() {
// 待验证的方法
}
}
testMethod这个方法替换为要测试的方法就可以了。如果不需要在所有线程执行完执行某些代码,可以去掉CountDownLatch的使用;如果不需要控制同一时间同时并行的线程数,可以去掉Semaphore的使用。
这里顺便补充一个技巧,有时为了让测试结果更明显一些,testMethod方法里可以考虑通过Thread.sleep方法让测试方法执行的慢一些,让并发测试的结果更显著。
另外希望一定注意的是,在实际项目中,尤其是分布式系统并发场景,核心点要有日志记录核心变量的值。但一定不要使用System.out去输出日志,而我们通常讨论的slf4j、logback等,则是推荐使用的,因为他们会包含我们分析时能用到的:线程名称、代码行、及输出日志的时间等,System.out不光是没这些重要信息,而且在输出时使用了synchronized,这直接导致输出日志那里会出现线程阻塞。更多日志的规范可以参考我之前的手记:Java项目中使用log记录日志的一些总结。
实际项目中,尤其是在分布式系统里,很多问题的分析,我们都要依靠项目运行过程中输出的日志。线程名称、代码行、输出日志的时间、相关的变量值这几项又是分析过程中最关键的,希望大家能引起重视。
结尾说了这么多关于多线程并发的内容,最后说一句,学习并发时,一定记得借助这篇手记辅助学习哦:《Java并发编程与高并发解决方案》课程相关手记汇总
热门评论
这里写错了,应该是happens-before吧
老师您好,关于多线程提速这块儿想问一下您。
一个CPU密集型的任务,中间使用了多线程提高处理效率,但是有好几处使用了读写锁。这样的一个程序在一台配置较好6核U的笔记本上运行的速度比在一台2块14核U的服务器上跑的快很多,无论在服务器上如何限制并发处理线程运行结果都差不多。但是在另外的单块AMD32核U的服务器上跑的也很快。
查看耗时基本都发生在加锁上,这是由于运行太快所以导致都在等锁么?还是有其他原因呢?
很好啊