Java并发总结
并发问题的根源
- 可见性:一个线程的操作结果是否对另一个线程可见
- 原子性:一个线程进行操作时是否会被其它线程干扰
可见性问题的来源
- 缓存:每一个线程会有自己的工作内存来缓存主存中的内容,线程通过这个缓存操作主存,所以可能存在刷新不及时的问题
- 指令重排:CPU会对编译后的字节码指令进行重排序后执行,原则上这种重排序对单线程来说是透明的,但对于多个线程来说可能不是
安全发布对象的几种方式
影响对象安全发布的是可见性问题,所以我们只要将可见性问题解决就能安全发布对象
- 线程封闭:即不共享任何对象
- 不可变对象:发布不可变或事实不可变的对象,这样永远不会出现可见性问题,该对象总是安全的
- 静态初始化函数中初始化对象
- volatile或AtomicReference
- 保存到final域
- 放到由锁保护的域中:如并发容器,线程安全容器,自己编写的同步代码块,这样Happens-Before原则会保证可见性
保证线程安全的几种方式
线程安全是特定于业务的一个话题,主要是采取各种操作使得类中的不变性条件得到满足。
- 实例封闭:使用一个类包装另一个类,在外层类中委托内层类实现功能,并提供线程安全保护
- 委托其它线程安全组件:直接使用已有的线程安全组件,比如ConcurrentHashMap,可以直接使用也可以利用它们对类中的不变性约束进行保护
- 扩展现有的线程安全组件:你可以继承或组合Vector,实现新的功能,这要求现有的线程安全组件使用开放的加锁策略,比如锁定到
this
上。
Java中已有的多线程开发组件
- 同步容器类:Vector、Hashtable...
- 并发容器类:ConcurrentHashMap
- 同步工具类:CountDownLatch、信号量、栅栏...
- 阻塞队列
Executor&线程池
- Executor:任务的执行器,用于将任务与其执行方式解耦,但在并发编程中往往还是有很多隐式耦合
- ExecutorService:在
Executor
上进行扩展,可以被结束,提供了新的提交任务的方法——submit
方法,该方法返回用于追踪并控制任务进度的Future
对象 - 线程池:
ExecutorService
的一些基于线程池的实现
线程池提交任务过程
- 如果线程池中线程个数小于
corePoolSize
,创建一个新线程 - 否则,判断线程池中是否有空闲线程,有就让它执行
- 否则,判断当前任务等待队列是否未满,是就让它进入队列排队
- 否则,判断当前线程池中线程数是否小于
maximumPoolSize
,是就创建线程 - 否则,拒绝该任务
填补核心线程 -> 复用空闲线程 -> 进入等待队列 ->
创建大于核心线程数量的更多线程 -> 到达最大线程数限制,拒绝
线程池饱和策略
当一个任务被线程池拒绝时采取的策略
- AbortPolicy——抛出RejectedExecutionException
- CallerRunsPolicy——让调用者线程执行该任务
- DiscardPolicy——静默抛弃该任务
- DiscardOldestPolicy——静默抛弃队列中的第一个任务(在优先级队列中会抛弃优先级最高的任务)
常见线程池
FixedThreadPool
- 等待队列无界,线程池大小永远不会超过核心线程数,永远不会拒绝任务
- 核心大小和最大大小相同
- keepAliveTime为0,由于核心大小和最大大小一致,这个参数没有意义了
CachedThreadPool
- 最大线程数无界,永远不会拒绝任务
- 核心线程数为0,不会保留任何线程
- keepAliveTime为60,每一个执行任务结束后的线程有60秒的时间等待被复用
- 使用容量为0的同步队列,每一个新来的任务在没有可复用线程的情况下立即创建新线程
任务取消
interrupt
如果你直接操作线程,你可以调用线程的interrupt
方法通知线程你希望打断它,这是一个协商的过程,任务是否会被取消的决定权在于线程。
Future.cancel
如果你使用ExecuterService,提交任务时会返回一个Future
,可以使用Future.cancel
方法来取消任务。
Future.cancel
方法的参数是一个布尔类型,若为true
,它会尝试interrupt
底层线程,如果为false
则不会。Future.cancel
的另一个副作用会让Future.get
抛出异常。
其它学问
- 有些阻塞操作是不响应中断的,比如
socket.read/write
,此时需要阅读对应API的文档找到中断的方式,并且可以通过改写执行线程的interrupt
方法让它适配Java统一的任务取消模型 - 响应中断的方法(Thread.sleep)会在接收到中断时清除中断状态并抛出异常,你可以选择向上抛出该异常或重新调用底层线程的
interrupt
方法来恢复中断状态,任务不应该对底层线程处理中断的方式做任何假设
性能和可伸缩性
可伸缩性描述了当系统的计算资源(CPU)增加时,程序的执行效率是否能够相应增加
Amdahl定律指出可伸缩性受程序中必须串行执行部分的影响
- 缩小锁范围
- 降低请求频率
- 锁分解:将逻辑不相关的内容分解
- 锁分段:维护多个锁,降低单个锁被请求的频率(ConcurrentHashMap)
- 避免热点域:避免出现多线程都要频繁访问的数据,比如
length
,可以通过为每个分段维护自己的length
以消除额外的保护 - 不使用独占锁:ReadWriteLock或CAS(原子变量)
ABA问题
在CAS操作中,你期待的旧值是A,但有的线程将它设置成了B后又设置成了A,你的操作无法检测到这一点
内存模型
程序顺序原则
在单一线程内部,不管怎么重排都必须保证结果和顺序执行一致
同步顺序原则(Synchronization Order)
符合下面原则中的操作,即使在多个线程之间,也会保证其先后顺序
- 对于同一个锁,一个解锁操作将与后续任何线程的加锁操作保持同步顺序
- 对一个volatile变量的写入与后续任何线程对它的读取保持同步顺序
- 线程的start操作与该线程中第一行语句的保持同步顺序
- 变量默认值的写入与每个线程的第一个动作保持同步顺序
- 线程T1中的最后一个操作与另一个线程T2检测到线程T1已经终结保持同步顺序
- 如果T1打断了T2,那么T1的中断与任何线程(包括T2)检测到T2已经被中断保持同步顺序
Happens-Before原则
符合Happens-Before原则的两个线程,前面线程的所有操作对后面的线程可见
- 一个获取锁的线程可以看到前面释放锁的线程所做的操作
- 对volatile进行读取的线程可以看到前面写入该变量的线程的操作
- 一个线程在调用另一个线程的
start
方法前的所有操作对另一个线程可见 - 一个线程在调用另一个线程的
join
方法时,当该方法返回后,另一个线程的所有操作对该线程可见 - 何对象的默认初始化方法都先行发生于对这个对象的所有操作之前