前言
最近一直在研究Java的并发编程,在此做一下学习总结。因为水平有限,所以文章中会有些地方直接照搬书上的概念,或者借鉴其他文章中的概念,引用到的书籍或者文章会在文中注明。如有侵权,请及时联系我删除,感谢。
什么是并发编程
什么是并发编程?我们看一下百度百科的概念:
所谓并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。
在单线程程序中,程序的执行都是线性执行的。现在试想一种情况:你经营了一个快递站点,站点里有很多个快递需要配送,站点里只有一个快递员去配送快递,效率非常低,可能一天下来快递也没配送完,那么你会如何解决这个问题呢?
这个问题的结果显而易见:多招几个快递员送快递就行了。在这个模拟的场景里,配送快递就是需要执行的任务,而快递员就是执行这个任务的线程。所谓的并发编程就是使用多个线程去并行的执行我们的任务,提升我们程序的执行速度。
那么,是不是用了多线程去执行任务,效率就一定会提升呢?不一定。
并发编程带来的问题
1、上下文切换
即使是单核的处理器,也是支持多线程执行任务的,CPU会给每个线程分配时间片来执行,每个线程执行一段时间,由CPU不停的进行切换线程,让用户感觉线程是并行执行的。在切换前,处理器会记录下线程的执行状态,在下一次切换回来时,会将原先的状态加载回来,这个切换的过程就叫做上下文切换。
从上下文切换的概念,我们可以理解到,线程的并行是由处理器不停的切换来实现的,那么如果一个任务的执行量不是那么大时,多线程并不一定会比单线程快,因为单线程没有线程间不停切换的开销。
2、死锁
死锁在多线程开发中,是个十分致命的问题。上下文切换只会影响效率,而死锁会导致线程无限的等待,死锁时间长的话,甚至可能导致应用程序的崩溃。那么什么是死锁呢?以一段代码为例。
public class DeadLock {
private static Object lock1 = new Object();
private static Object lock2 = new Object();
public static void main(String[] args) {
Thread task1 = new Thread(new Task1());
Thread task2 = new Thread(new Task2());
task1.start();
task2.start();
}
static class Task1 implements Runnable{
@Override
public void run() {
//获取lock1
synchronized (lock1){
try {
//线程睡眠1s再获取锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取lock2
synchronized (lock2){
System.out.println("task1 get lock2");
}
}
}
}
static class Task2 implements Runnable{
@Override
public void run() {
//获取lock2
synchronized (lock2){
try {
//线程睡眠1s再获取锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取lock1
synchronized (lock1){
System.out.println("task2 get lock1");
}
}
}
}
}
如代码所示,task1在获取lock1之后尝试获取lock2,而此时lock2已被task2获取了,task2也在等待lock1。这就造成了task1和task2互相等待对面释放锁的情况,这种情况就叫做死锁。
3、资源限制
在使用多线程的时候,不能盲目追求效率,而忽略资源的限制,资源限制主要分为两种:
(1) 硬件资源
硬件资源指的是服务器的带宽、CPU的处理速度、硬盘的读写速度等
(2) 软件资源
软件资源指的是数据库连接、Redis连接、Http连接等
笔者在使用多线程开发一个接口时,曾碰到过资源耗尽的情况,在此做个分享:
该接口在执行过程中,需调用数次第三方接口、Redis、mysql入库,并且这是个批量操作,可能会执行多次。最一开始使用单线程执行,在批量执行的次数少的时候,效率并不会用什么影响,但是上生产环境之后,批量执行的量变多,效率开始变慢,就考虑使用多线程去解决问题。最一开始使用了50个线程在笔者的笔记本上调试,cpu很快飙升至95+%,后来逐渐减少线程数,发现调用第三方接口时,经常等待http连接超时的情况。因为http连接池中的连接数量有限,所以很多线程在等待线程时超时。于是继续减少线程数,并且将接口的响应方式从同步改成异步,最终才解决了问题。
笔者碰到的问题就出现了硬件限制与软件限制,所以衡量资源的限制在多线程开发中也是件很重要的事情。
总结
本篇文章介绍了并发编程的概念以及并发编程会带来的问题。在开发中使用多线程之前,一定要衡量使用多线程的风险,虽然多线程在大多数时候会提升程序的执行速度,但也会带来相应的问题,并提升开发和调试时的复杂度。有些场景下可能并不需要使用多线程,比如一些不需要及时得到响应的场景下,完全可以使用异步获取结果的方式去单线程执行任务。如果需要使用多线程开发,建议使用Java的concurrent包下的容器和工具。
参考资料
《并发编程的艺术》 --方腾飞、魏鹏、程晓明