Loading

Java并发总结

并发问题的根源

  1. 可见性:一个线程的操作结果是否对另一个线程可见
  2. 原子性:一个线程进行操作时是否会被其它线程干扰

可见性问题的来源

  1. 缓存:每一个线程会有自己的工作内存来缓存主存中的内容,线程通过这个缓存操作主存,所以可能存在刷新不及时的问题
  2. 指令重排:CPU会对编译后的字节码指令进行重排序后执行,原则上这种重排序对单线程来说是透明的,但对于多个线程来说可能不是

安全发布对象的几种方式

影响对象安全发布的是可见性问题,所以我们只要将可见性问题解决就能安全发布对象

  1. 线程封闭:即不共享任何对象
  2. 不可变对象:发布不可变或事实不可变的对象,这样永远不会出现可见性问题,该对象总是安全的
  3. 静态初始化函数中初始化对象
  4. volatile或AtomicReference
  5. 保存到final域
  6. 放到由锁保护的域中:如并发容器,线程安全容器,自己编写的同步代码块,这样Happens-Before原则会保证可见性

保证线程安全的几种方式

线程安全是特定于业务的一个话题,主要是采取各种操作使得类中的不变性条件得到满足。

  1. 实例封闭:使用一个类包装另一个类,在外层类中委托内层类实现功能,并提供线程安全保护
  2. 委托其它线程安全组件:直接使用已有的线程安全组件,比如ConcurrentHashMap,可以直接使用也可以利用它们对类中的不变性约束进行保护
  3. 扩展现有的线程安全组件:你可以继承或组合Vector,实现新的功能,这要求现有的线程安全组件使用开放的加锁策略,比如锁定到this上。

Java中已有的多线程开发组件

  1. 同步容器类:Vector、Hashtable...
  2. 并发容器类:ConcurrentHashMap
  3. 同步工具类:CountDownLatch、信号量、栅栏...
  4. 阻塞队列

Executor&线程池

  1. Executor:任务的执行器,用于将任务与其执行方式解耦,但在并发编程中往往还是有很多隐式耦合
  2. ExecutorService:在Executor上进行扩展,可以被结束,提供了新的提交任务的方法——submit方法,该方法返回用于追踪并控制任务进度的Future对象
  3. 线程池ExecutorService的一些基于线程池的实现

线程池提交任务过程

  1. 如果线程池中线程个数小于corePoolSize,创建一个新线程
  2. 否则,判断线程池中是否有空闲线程,有就让它执行
  3. 否则,判断当前任务等待队列是否未满,是就让它进入队列排队
  4. 否则,判断当前线程池中线程数是否小于maximumPoolSize,是就创建线程
  5. 否则,拒绝该任务
填补核心线程 -> 复用空闲线程 -> 进入等待队列 -> 
创建大于核心线程数量的更多线程 -> 到达最大线程数限制,拒绝

线程池饱和策略

当一个任务被线程池拒绝时采取的策略

  • AbortPolicy——抛出RejectedExecutionException
  • CallerRunsPolicy——让调用者线程执行该任务
  • DiscardPolicy——静默抛弃该任务
  • DiscardOldestPolicy——静默抛弃队列中的第一个任务(在优先级队列中会抛弃优先级最高的任务)

常见线程池

FixedThreadPool

  1. 等待队列无界,线程池大小永远不会超过核心线程数,永远不会拒绝任务
  2. 核心大小和最大大小相同
  3. keepAliveTime为0,由于核心大小和最大大小一致,这个参数没有意义了

CachedThreadPool

  1. 最大线程数无界,永远不会拒绝任务
  2. 核心线程数为0,不会保留任何线程
  3. keepAliveTime为60,每一个执行任务结束后的线程有60秒的时间等待被复用
  4. 使用容量为0的同步队列,每一个新来的任务在没有可复用线程的情况下立即创建新线程

任务取消

interrupt

如果你直接操作线程,你可以调用线程的interrupt方法通知线程你希望打断它,这是一个协商的过程,任务是否会被取消的决定权在于线程。

Future.cancel

如果你使用ExecuterService,提交任务时会返回一个Future,可以使用Future.cancel方法来取消任务。

Future.cancel方法的参数是一个布尔类型,若为true,它会尝试interrupt底层线程,如果为false则不会。Future.cancel的另一个副作用会让Future.get抛出异常。

其它学问

  1. 有些阻塞操作是不响应中断的,比如socket.read/write,此时需要阅读对应API的文档找到中断的方式,并且可以通过改写执行线程的interrupt方法让它适配Java统一的任务取消模型
  2. 响应中断的方法(Thread.sleep)会在接收到中断时清除中断状态并抛出异常,你可以选择向上抛出该异常或重新调用底层线程的interrupt方法来恢复中断状态,任务不应该对底层线程处理中断的方式做任何假设

性能和可伸缩性

可伸缩性描述了当系统的计算资源(CPU)增加时,程序的执行效率是否能够相应增加

Amdahl定律指出可伸缩性受程序中必须串行执行部分的影响

  1. 缩小锁范围
  2. 降低请求频率
    1. 锁分解:将逻辑不相关的内容分解
    2. 锁分段:维护多个锁,降低单个锁被请求的频率(ConcurrentHashMap)
  3. 避免热点域:避免出现多线程都要频繁访问的数据,比如length,可以通过为每个分段维护自己的length以消除额外的保护
  4. 不使用独占锁:ReadWriteLock或CAS(原子变量)

ABA问题

在CAS操作中,你期待的旧值是A,但有的线程将它设置成了B后又设置成了A,你的操作无法检测到这一点

内存模型

程序顺序原则

在单一线程内部,不管怎么重排都必须保证结果和顺序执行一致

同步顺序原则(Synchronization Order)

符合下面原则中的操作,即使在多个线程之间,也会保证其先后顺序

  1. 对于同一个锁,一个解锁操作将与后续任何线程的加锁操作保持同步顺序
  2. 对一个volatile变量的写入与后续任何线程对它的读取保持同步顺序
  3. 线程的start操作与该线程中第一行语句的保持同步顺序
  4. 变量默认值的写入与每个线程的第一个动作保持同步顺序
  5. 线程T1中的最后一个操作与另一个线程T2检测到线程T1已经终结保持同步顺序
  6. 如果T1打断了T2,那么T1的中断与任何线程(包括T2)检测到T2已经被中断保持同步顺序

Happens-Before原则

符合Happens-Before原则的两个线程,前面线程的所有操作对后面的线程可见

  1. 一个获取锁的线程可以看到前面释放锁的线程所做的操作
  2. 对volatile进行读取的线程可以看到前面写入该变量的线程的操作
  3. 一个线程在调用另一个线程的start方法前的所有操作对另一个线程可见
  4. 一个线程在调用另一个线程的join方法时,当该方法返回后,另一个线程的所有操作对该线程可见
  5. 何对象的默认初始化方法都先行发生于对这个对象的所有操作之前
posted @ 2022-08-09 14:21  yudoge  阅读(50)  评论(0编辑  收藏  举报