基本概念
- 初始状态 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 | public ThreadPoolExecutor( |
核心参数:
- 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 连接池等都属于池化技术,主要是为了减少每次获取资源的消耗,提高对资源的利用率。线程池提供了一种限制和管理资源(包括执行一个任务),每个线程池还维护一些基本统计信息例如已完成任务的数量
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高相应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控