不堆概念、换个角度聊多线程并发编程
俗话说,双拳难敌四手。
俗话还说,人多力量大。
在现实生活中,我们通过团队化的方式来获得比单兵作战更高的单位时间内整体产出速度。同样,在编码世界中,为了提升处理效率,并发一直以来都是软件开发设计场景中无法绕过的话题。不管是微观层面的单个进程内多线程处理模式,还是宏观层面整个系统集群化多节点部署策略,为了提升系统的整体并发吞吐量,程序员们可谓是煞费苦心。
当然,俗话也说,人多眼杂、林子大了什么鸟都有。在现实中,团队中多人一起配合工作的时候,一系列的问题又会显现:
同一个事情,老王和小张都以为还没处理,结果都去处理了,最后造成了成员工作量的浪费、甚至因为重复处理了一遍导致数据错误两个有关联的事情分别给了老王和翠花,结果老王在等待翠花先给出结果再开始处理自己的事情,翠花也在等待老王先给出结果然后再处理自己的事情,结果两个人就这么一致等下去,事情一直没完成同一个文档,小张和翠花各自更新的时候,出现相互覆盖的情况…
编码源于生活、代码世界其实也处处体现着生活中的朴素哲学思维。纵然并发场景存在一些可能的隐患问题,但我们也不必因噎废食,正所谓先了解它、再掌控它。
作为提升吞吐性能的不二良方,下面我们就一起来尝试按照问题解决型的思路一步步推进,换个角度探讨下多线程并发相关的内容,全面了解下多线程并发世界的各种关联,进而更从容优雅的让并发为我们所用,成为我们提升系统性能的神兵利器。
多线程——并发第一步
并发探险的第一关,就是如何支持并发。下面大概列举下常见的几种方式:
子线程
一些简单的场景中,我们为了提升主线程的处理性能,会将过程中一些耗时操作放到一个单独的子线程中进行同步处理。在代码中可以通过创建临时子线程的方式来执行即可:
public void buyProduct() {
int price = getPrice();
// 子线程同步处理部分操作
new Thread(this::printTicket).start();
// 主线程继续处理其它逻辑
doOtherOperations(price);
}线程池
频繁创建线程、销毁线程的操作属于一种消耗性能的操作,而且创建线程的数量不可控。所以对于一些固定需要在子线程中并行处理的任务场景,我们可以通过创建线程池的方式,固定维护着一批可用线程,循环利用,去处理任务,以实现提升效率与便于管控的诉求:
private ExecutorService threadPool = Executors.newFixedThreadPool(3);
public void testReleaseThreadLocalSafely() {
// 任务直接放到线程池中进行处理
threadPool.submit(this::mockServiceOperations);
}定时器
定时器是一种比较特殊的多线程并发场景,也是经常可能会被忽视掉的一种情况。定时器也是在子线程中执行的,多个定时器之间、定时器线程与主线程之间、定时器线程与业务子线程之间都会以多线程的形式并发处理。
@Scheduled(cron = "0 0/10 * * * ?")
public void syncBusinessInfo() {
// do something here...
}Tomcat等容器
常见的服务运行容器,比如Tomcat等,都是支持并发请求执行的。而常见的基于SpringBoot实现的服务,其service类都是由Spring进行托管的单例对象。这种场景是比较常见的多线程场景。
改为多线程并发执行,虽然效率是提升了,但是问题也来了——数据执行结果不准确。
结果不对,显然是我们无法接受的。所以摆在我们面前的下一难题,就是要保证执行结果数据的准确。
synchronized与lock
在JAVA中提到线程同步,使用最简单、应用频率最高的非synchronized关键字莫属了。它是 Java 内建的一种同步机制,代表了某种内在锁定的概念,当一个线程对某个共享资源加锁后,其他想要获取共享资源的线程必须进行等待,synchronized