一、线程安全的活跃态问题
线程安全的活跃性问题可以分为活锁、饥饿、死锁。
1、活锁
活锁,就是有时线程虽然没有发生阻塞,但是仍然会存在执行不下去的情况,活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试。
我们开发中使用的异步消息队列就有可能造成活锁的问题,在消息队列的消费端如果没有正确的ack消息,并且执行过程中报错了,就会再次放回消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。
解决活锁的方案很简单,尝试等待一个随机的时间就可以,会按时间轮去重试。
2、饥饿
饥饿,就是线程因无法访问所需资源而无法执行下去的情况。饥饿分为两种情况:
- 一种是其他的线程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态。
- 另一种是因为线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行。
解决饥饿的问题有几种方案:
- 保证资源充足,很多场景下,资源的稀缺性无法解决,
- 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源。
- 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短。
3、死锁
线程在对同一把锁进行竞争的时候,未抢占到锁的线程会等待持有锁的线程释放锁后继续抢占,如果两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁。
3.1 死锁发生的4个必要条件
(1)互斥条件:同一时间只能有一个线程获取资源。
(2)不可剥夺条件:一个线程已经占有的资源,在释放之前不会被其它线程抢占。
(3)请求和保持条件:线程等待过程中不会释放已占有的资源。
(4)循环等待条件:多个线程互相等待对方释放资源。
3.1 死锁的预防,那么需要破坏四个必要条件。
(1)互斥条件:由于资源互斥是资源使用的固有特性,无法改变,不进行讨论。
(2)破坏不可剥夺条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
(3)破坏请求与保持条件
- 第一种方法静态分配:即每个进程在开始执行时就申请他所需要的全部资源。
- 第二种方法动态分配:即每个进程在申请所需要的资源时他本身不占用系统资源。
(4)破坏循环等待条件:采用资源有序分配,其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
二、线程安全的竞态条件
同一个程序多线程访问同一个资源,如果对资源的访问顺序敏感,就称存在竞态条件,代码区成为临界区。 大多数并发错误一样,竞态条件不总是会产生问题,还需要不恰当的执行时序。
最常见的竞态条件为:
1、先检测后执行
执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定不可判断的,从而导致执行结果出现各种问题。
解决办法就是:在一个线程修改访问一个状态时,要防止其他线程访问修改,也就是加锁机制,保证原子性。
2、延迟初始化
典型为单例。