随着当今CPU的高速发展,4核、8核甚至16核CPU已经面世了。在以往单核CPU的时代,每一个线程只能争抢一个CPU去获取运行的权利。在多核CPU的场景下,一个线程已经无法充分地利用多个CPU了,再者,数字化时代更加加剧了用户对应用的性能需求,传统的单线程应用已经逐渐被淘汰了, 通过多线程并发执行的形式可以将CPU的计算能力发挥到极限 ,这是为什么需要学习并发编程的一个重要原因。
即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配时间片来实现多线程并发执行。当任务A时间片执行完以后会切换到下一个任务,此时需要保存当前任务A的状态,在下一次再次轮到任务A执行的时候需要恢复这个状态, 这样的一次保存和恢复称为上下文切换。频繁的上下文切换会耗费系统大量资源!
减少上下文切换的方法有
concurrentHashMap
并发编程环境下,多线程有可能带来的安全问题有:
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
,在读数据时不会对这个临界资源加锁,但是在对它添加、删除或更新数据时就会把整个集合锁住,其它线程必须等待该线程操作完后才能够访问。