线程相关知识点

基本概念

  • 初始状态 new
  • 可运行/运行状态 runnable
  • 休眠状态 blocked waiting timed_waiting
  • 终止状态 terminated

线程的生命周期和状态

  • 创建:new
  • 就绪:start(),可运行,等待获取 CPU 的使用权
  • 运行:执行
  • 阻塞:放弃 CPU 使用权,sleep,wait(sleep 是 Thread 的,wait 是 Object 的)
  • 死亡:执行完或异常退出

线程的 run() 和 start() 的区别

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码,run() 可以重复调用,而 start() 只能调用一次,如果直接调用线程的 run() 方法,相当于执行一个普通方法,不在整个线程的生命周期中

如何停止一个线程

  • 使用 flag
  • stop()
  • interrupt()

sleep(), wait(), join(), yield()

  • sleep: Thread 类的静态方法,不依赖于 synchronized,一般用于当前线程休眠,会释放锁
  • wait: Object 类的普通方法,依赖于 synchronized,一般用于多线程之间的通信,不会释放锁,要使用 notify() 或者 notifyAll() 唤醒
  • yield: 执行后线程进入就绪状态,马上释放了 CPU 的执行权,但是保留了 CPU 的执行资格,可能在下次调度的时候再次获得执行权
  • join: 执行后线程进入阻塞状态,让 join 的线程先执行完或中断

notify() 和 notifyAll() 的区别

notify() 随机唤醒一个线程,notifyAll() 唤醒所有的线程,当 notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,不成功则留在锁池等待锁释放后再次参与竞争

实现多线程的几种方式

  • 继承 Thread 类(实际上 Thread implements Runnable)
  • 实现 Runnable 接口
  • 实现 Callable 接口(通过 FutureTask 包装器来创建)
  • 通过线程池创建线程,使用线程池接口 ExecutorService 结合 Callable,Future 实现有返回值的多线程

runnable 有两个弊端(Callable 和 Runnable 的最大区别就是 Callable 可以有返回值):

  • 不能获取返回结果
  • 不能抛出异常

守护线程

守护线程是运行在后台的一种特殊进程,为所有非守护线程提供服务,比如 GC 线程就是特殊的守护线程

用法注意:thread.setDaemon(true) 必须在 thread.start() 之前设置,否则抛出异常 IllegalThreadStateException,因为不能把正在运行的常规线程设置为守护线程

ThreadLocal

ThreadLocal 不是多线程同步机制中的一种,而是一种解决思路,它解决的是多线程下成员变量的安全问题,不是共享变量的安全问题

线程同步机制是多个线程共享一个变量,而 ThreadLocal 是每个线程创建一个自己的单独变量副本,所以每个线程都可以独立地改变自己的变量副本,而不会影响到其他线程的变量副本

ThreadLocal 内部有一个非常重要的内部类:ThreadLocalMap,是真正实现线程隔离机制的关键,ThreadLocalMap 内部结构类似于 Map,由键值对 key 和 value 组成一个 entry,key 为 ThreadLocal 本身,value 是对应的线程变量副本(首先是 Thread,Thread 里面有 ThreadLocal.ThreadLocalMap,Map 的话存储的是 Entry 集合,每一个 Entry 的 key 为 ThreadLocal,value 为具体的值)

有两点需要注意

  • ThreadLocal 本身不存储值,它只是提供一个查找到值的 key
  • ThreadLocal 包含在 Thread 中,不是 Thread 包含在 ThreadLocal 中

ThreadLocalMap 和 HashMap 的功能类似,但是实现上却有很大的不同:

  • HashMap 的数据结构是数组 + 链表
  • ThreadLocalMap 的数据结构仅仅是数组
  • HashMap 是通过链地址法来解决哈希冲突,ThreadLocalMap 是通过开放地址法来解决哈希冲突(链地址:数组加链表,开放地址:寻找空的位置)
  • HashMap 里面的 Entry 内部类的引用都是强引用,ThreadLocalMap 里面的 Entry 内部类中的 key 是弱引用,value 是强引用
1
static class Entry extends WeakReference<ThreadLocal<?>>

从源码可以看出 ThreadLocal.ThreadLocalMap.Entry 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用,也就是说,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候 key 会被清理掉,而 value 不会被清理,这样一来,ThreadLocalMap 中就会出现 key 的 null 的 Entry,假如不做任何措施的话,value 永远无法被 GC,这个时候就可能产生内存泄漏

如何避免内存泄漏?

当一个变量不用的时候,要调用 remove() 删除掉(内部调用了 expungeStaleEntry(),将 value 设为 null,这样下一次垃圾回收时就会被彻底回收掉)

CAS

CAS 全称 Compare And Swap,比较并交换,是一条 CPU 的并发原语

原语的执行必须是连续的,在执行的过程中不允许被中断,因此不会造成数据不一致的问题,具有原子性。CAS 是一种重要的同步思想,判断内存中的值是否和预期的值一样,如果是,则将内存中的值更新为新值,否则会不断重试,直到一致为止

ABA 问题

比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题

比如两个线程 A 和 B

  • 一开始都从主内存中拷贝了原值为 1
  • 线程 A 执拿到值为 1 然后挂起
  • 线程 B 修改值为 2,执行完毕
  • 线程 B 觉得修改错误,把值重新设置为 1,
  • 线程 A 被唤醒,比较发现内存中的值和预期的值一样,修改成功(但是不知道这个值已经被 B 修改过了)

尽管线程 A CAS 操作成功,但不代表没有问题,有的需求,只注重头和尾,只要首尾一致,就接受,但是有的需求,还看重过程,中间不能发生任何修改,这就引出了 AtomicStampedReference 原子引用

AtomicStampedReference 内部维护了一个版本号 stamp,在进行 CAS 操作的时候,不仅要比较当前值,还要比较版本号,只有两者都相等,才执行更新操作(有点类似乐观锁)

任何技术都不是完美的,CAS 也有自己的缺点,CAS 实际上是一种自旋锁

  • 一直循环,开销比较大
  • 只能保证一个变量的原子操作,多个变量依然要加锁
  • 引出了 ABA 问题(AtomicStampedReference 可解决)

CAS 的使用场景适合在一些并发量不高,线程竞争较少的情况,但是一旦线程冲突严重的情况,循环时间太长,会给 CPU 带来很大的开销

线程池

如何创建线程池?

JDK 中提供了创建线程池的类,Executors,但是一般不推荐

Executors 类只是个静态工厂,提供创建线程池的几个静态方法(内部屏蔽了线程池参数配置细节),而真正的线程池类是 ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)

核心参数:

  • corePoolSize:核心线程数。如果等于 0,则任务执行完毕后,没有任务请求进入时,销毁线程池中的线程。如果大于 0,即使本地任务执行完毕,核心线程也不会被销毁。设置得过大会浪费系统资源,设置过小会导致线程频繁创建
  • maximumPoolSize:最大线程数。必须大于等于 1,且大于等于 corePoolSize。如果与 corePoolSize 相等,则线程池大小固定。如果大于 corePoolSize,则最多创建 maximumPoolSize 个线程执行任务
  • keepAliveTime:线程空闲时间。线程池中线程空闲时间达到 keepAliveTime 值时,线程会被销毁,直到剩下 corePoolSize 个线程为止。默认情况下,线程池的最大线程数大于 corePoolSize 时,keepAliveTime 才会起作用。如果 allowCoreThreadTimeOut 被设置为 true,即使线程池的最大线程数等于 corePoolSize,keepAliveTime 也会起作用(回收超时的核心线程)
  • unit:TimeUnit 表示时间单位
  • workQueue:缓存队列。当请求线程数大于 corePoolSize 时,线程进入 BlockingQueue 阻塞队列
  • threadFactory:线程工厂。用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀,是否为守护线程以及线程的优先级等。设置有意义的名称前缀可以很快知道线程是由哪个线程工厂创建的,方便调试
  • handler:执行拒绝策略对象。当任务数达到缓存上限时(即超过 workQueue 参数能存储的任务数),执行拒绝策略,相当于限流保护

上面复杂概念的简洁描述:

  • 如果线程池当前状态不是 running,直接拒绝
  • 如果 worker < pool,创建新线程
  • 如果 worker >= pool,queue 未满,将任务添加到 queue
  • 如果 pool <= worker < max,并且 queue 已满,开启新线程
  • 如果 worker > max,并且 queue 已满,拒绝策略(默认直接抛异常)

线程池种类

  • FixedThreadPool:固定长度的线程池,核心线程数等于最大线程数,不存在空闲线程,keepAliveTime 为 0,可以控制线程的最大并发数,超出的线程会放到队列中
  • SingleThreadExecutor:单线程线程池,核心线程数和最大线程数都是 1,它只会用唯一的线程来执行任务,相当于串行执行,超出的线程会放到队列中
  • CachedThreadPool:可缓存的线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE,如果数据过多,它会不断地创建新的线程,存在 OOM 风险,keepAliveTime 为 60,工作线程处于空闲状态超过 keepAliveTime 会回收线程
  • WorkStealingPool:JDK 1.8 引入,核心是工作窃取,没怎么见人用过
  • ScheduledThreadPool:用于定时执行任务的线程池

禁止直接使用 Executors 创建线程池的原因(除 Executors.newWorkStealingPool 方法之外,其他方法都有 OOM 风险):

  • Executors.newCachedThreadPool 和 Executors.newScheduledThreadPool 两个方法最大线程数为 Integer.MAX_VALUE,如果线程数太多,会有 OOM 的风险
  • Executors.newSingleThreadPool 和 Executors.newFixedThreadPool 两个方法的 workQueue 参数为 LinkedBlockingQueue,容量为 Integer.MAX_VALUE,如果瞬间请求过大,导致队列中任务过多,会有 OOM 风险

线程池拒绝策略

ThreadPoolExecutor 提供了四个公开的静态内部类:

  • AbortPolicy:默认,丢弃任务并抛出 RejectedExecutionException
  • DiscardPolicy:丢弃任务,但是不抛出异常(不推荐)
  • DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前的新任务加入到队列中
  • CallerRunsPolicy:调用任务的 run() 方法绕过线程池直接执行

如何自定义拒绝策略

实现 RejectedExecutionHandler 接口或继承已有策略,重写 rejectedExecution() 方法

为什么要用线程池

线程池,数据库连接池,HTTP 连接池等都属于池化技术,主要是为了减少每次获取资源的消耗,提高对资源的利用率。线程池提供了一种限制和管理资源(包括执行一个任务),每个线程池还维护一些基本统计信息例如已完成任务的数量

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高相应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控