为什么要使用并发编程
随着当今CPU的高速发展,4核、8核甚至16核CPU已经面世了。在以往单核CPU的时代,每一个线程只能争抢一个CPU去获取运行的权利。在多核CPU的场景下,一个线程已经无法充分地利用多个CPU了,再者,数字化时代更加加剧了用户对应用的性能需求,传统的单线程应用已经逐渐被淘汰了,通过多线程并发执行的形式可以将CPU的计算能力发挥到极限,这是为什么需要学习并发编程的一个重要原因。

并发编程也有缺点
频繁的上下文切换
即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配时间片来实现多线程并发执行。当任务A时间片执行完以后会切换到下一个任务,此时需要保存当前任务A的状态,在下一次再次轮到任务A执行的时候需要恢复这个状态,这样的一次保存和恢复称为上下文切换。频繁的上下文切换会耗费系统大量资源!

减少上下文切换的方法有
- 无锁并发编程:可以参照
concurrentHashMap
锁分段的思想,每个桶都对应一个数据段,让不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。 - CAS算法:Java的Atomic包使用CAS算法来更新数据,不需要加锁。
- 使用最少线程:避免创建不必要的线程,以免造成大量线程处于等待状态。
- 协程:在单线程里实现多线程的调度,并在单线程里维持多个任务之间的切换。
线程安全问题
并发编程环境下,多线程有可能带来的安全问题有:
- 线程死锁:每个线程互相等待对方释放正在占有的资源。
- 线程饥饿:线程永远无法得到CPU的时间片,永远处于等待状态。
public class DeadLockDemo {
private static String resource_a = "A";
private static String resource_b = "B";
public static void main(String[] args) {
deadLock();
}
public static void deadLock() {
Thread threadA = new Thread(()->{
synchronized (resource_a) {
System.out.println("get resource a");
try {
Thread.sleep(3000);
synchronized (resource_b) {
System.out.println("get resource b");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(()->{
synchronized (resource_b) {
System.out.println("get resource b");
synchronized (resource_a) {
System.out.println("get resource a");
}
}
});
threadA.start();
threadB.start();
}
}
上面这段代码演示了死锁这个场景,通过jps
查看进程号,jstack
查看应用线程状态,可以看到两个线程都在等待对方正在占有的资源,自身却不释放资源,造成永久地相互等待现象。

避免死锁的几种常见方法:
- 避免一个线程同时请求获取多个锁
- 给资源加上编号,线程只能按照编号按顺序获取锁
- 获取锁的时间加上一个期限值,不要让线程永久地请求锁
- 银行家算法,在分配锁之前判断是否会造成死锁,如果是则拒绝本次获取锁的请求
在学习并发编程前需要了解的概念
同步和异步
同步:方法A调用方法B后,需要等待方法B执行结束后才能继续执行方法A。
异步:方法A调用方法B后,方法A可以继续处理自己的业务逻辑,无需等待方法B执行结束。
比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。
并发与并行
并发:多个线程不断地交替执行,在同一时刻只有一个线程在执行。
并行:真正意义上的多个线程同时执行,多个线程分配在多个CPU上在同一时刻一起执行。
比如,在单核CPU中,不存在并行执行线程的概念,只存在并发执行的概念。因为多个线程必须共享一个CPU,CPU不断地切换线程上下文让不同的线程执行(真的累)。在多核CPU中,多个线程都可以分配在不同的CPU上在同一时刻同时执行。
阻塞和非阻塞
阻塞:如果一个线程A占用了资源X,当线程B请求操作资源X时必须等待线程A释放资源X,等待的过程就称为阻塞。
非阻塞:和上面的例子相似,不同的是线程B请求操作资源X时不需要等待线程A释放,多个线程可以对资源X进行随意地访问。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程访问。但是每个线程对临界区资源进行操作时,一旦临界区资源被一个线程占有,那么其他线程必须等待。
例如Java中的CopyOnWriteArrayList
,在读数据时不会对这个临界资源加锁,但是在对它添加、删除或更新数据时就会把整个集合锁住,其它线程必须等待该线程操作完后才能够访问。
总结
这是入坑并发编程之前必须了解的一些重要概念,掌握了这些概念,对接下来的学习会有很好的帮助!之后会持续更新并发系列的文章,希望能与你们交流,如果这篇文章对你有一点点帮助,你的一个小小的点赞能让我高兴一整天!感谢你的阅读!
巨人的肩膀:
《Java并发编程的艺术》