进程和线程
进程是操作系统资源分配的最小单位,线程是 CPU 调度的最小单位。
一个标准的线程主要由3部分构成:线程描述信息(线程ID、线程名称、线程状态、线程优先级、其他)、程序计数器 和 栈内存。其中,程序计数器它记录着线程下一条指令的代码段内存地址;每个线程在创建的时候默认被分配 1M 大小的栈内存大小,和堆内存不同,栈内存不受垃圾回收器管理。
当线程执行流程进入方法时,JVM 就会为方法分配一个对应的栈帧压入栈内存;当线程流程跳出方法时,JVM 就从栈内存弹出该方法的栈帧,此时方法帧的局部变量的内存空间就会被回收。
创建线程的4种方法
Thread 类中有个属性, private boolean deamon = false; 该属性保存 Thread 线程实例的守护状态,默认为false ,表示普通用户线程,而不是守护线程。 守护线程是在进程运行提供某种后台服务的线程,比如GC 线程。
继承Thead 类创建线程类(Thread 本身就实现了 Runnable )
实现 Runnable 接口创建线程目标类(传入 Thread 中时,Thread将其以 target 字段保存)
使用 Callable 和 FutureTask 创建线程
通过线程池创建
前面2种创建方法有一个共同的缺陷:不能获取异步执行的结果。在此基础上,才有了使用 Callable 和 FutureTask 创建线程的方法。
Callable 接口
Callable 是个泛型接口,它的源码如下:
1 | public interface Callable<V> { |
Callable 接口类似与 Runnable ,不同的是 Runnable 的run 方法没有返回值,也没有接收异常的异常声明,因此,感觉 Callable 更强大一些,只是 Callable 暂时还没能和 Thread 产生关系。不过,一个在 Callable 接口与 Thread 线程之间搭桥的重要接口 RunnableFuture 接口就要登场了。
RunnableFuture 接口
RunnableFuture 接口与 Runnable 接口、Thread 类紧密相关,源码如下:
1 | public interface RunnableFuture<V> extends Runnable, Future<V> { |
可以看出,它继承了 Runnable 接口,从而可以作为 Thread 的 target ,同时还继承了 Future 接口,保证了可以获取未来的异步执行结果。
Future 接口
Future 接口至少提供了三大功能:
能够取消异步执行中的任务
判断异步任务是否执行完成
获取有任务完成后的执行结果
它的源码如下:
1 | public interface Future<V> { |
虽然它提供了这么多功能,但它终归只是个接口,通过它没法直接完成对异步任务的操作,为此,JDK 提供了一个默认实现类: FutureTask 。
FutureTask 类
FutureTask类实现了RunnableFuture接口,相当于既实现了 Runnable 接口,又实现了 Future 接口。所以FutureTask既能作为一个 Runnable 类型的 target 被Thread执行,又能作为Future异步任务来获取Callable的计算结果。
使用Callable和FutureTask创建线程的步骤
1 | public static void main(String args[]) throws InterruptedException, ExecutionException { |
returnableThread线程首先执行 thread.run() 方法,然后在其中执行其target(futureTask任务)的run()方法;接着在futureTask.run()方法中会执行 callable 成员的 call()方法。Callable的call()方法执行完成后,会将结果保存在FutureTask内部的outcome实例属性中。这里有两种情况:
futureTask的结果outcome不为空,callable.call()执行完成,futureTast.get会直接取回outcome结果返回给结果获取线程。
futureTask的结果outcome为空,callable.call()还没有执行完。则结果获取线程会被阻塞住直到callable.call()执行完成。当执行完后,最终结果会保存到outcome中,futureTask会唤醒结果获取线程。
1.4 线程的核心原理
1.4.1 线程的调度与时间片
在不同的操作系统、不同的CPU上,线程的 CPU 时间片长度都不同,windows xp 的时间片长度为 20 毫秒,线程调度模型主要分为 2 种:
分时调度。系统平均分配 CPU 的时间片,人人平等
抢占式调度。按照线程的优先级分配时间片,如果大家优先级都相同,就随机选择一个,目前大部分操作系统选择的是这种调度方式
1.4.2 线程优先级
对于优先级,有以下结论:
整体而言,高优先级的线程获得的执行机会更多
执行机会的获取具有随机性,优先级高的不一定获得机会多(文中的例子10级的优先级比9级的优先级获得的机会还少,个人认为可能的一个因素是:Java的线程映射到系统线程时,10级与 9 级并没有区分)
1.4.3 线程的生命周期
Java 的线程有6种状态:
NEW: 新建,创建成功,但是没有调用 start() 方法
RUNNABLE:可执行,包含操作系统的就绪、运行2种状态
BLOCKED:阻塞
WAITTING:等待
TIMED_WAITTING:限时等待,包括Thread.sleep(n)、Object.wait(n)、Thread.join(n)、LockSupport.parkNanos(n)、LockSupport.parkUntil(n) 等
TERMINATED:终止。线程的 run 方法执行完毕(或者执行run方法被异常终止)
1.4.5 使用 Jstack 工具查看线程状态
Jstack 是Java 虚拟机自带的 堆栈跟踪工具,用于生成或者导出(Dump)JVM 运行实例当前时刻的线程快照。命令的语法格式如下:
jstack
//pid表示 Java 进程的 id,可以用 jps 命令查看
1.5 线程的基本操作
1.5.2 线程的sleep 操作
sleep 的作用是让线程休眠,让 CPU 执行其他的任务,从状态来讲就是从执行状态变成限时阻塞状态。当睡眠时间满后,线程不一定立即得到执行,因为此时CPU可能正在执行其他任务,所以还需要等待分配时间片。
1.5.3 线程的 interrupt 操作
Java 语言提供了 stop() 方法终止正在运行的方法,但后来不建议使用了,因为这像突然关闭计算机电源一样,无法知道这个线程在处于什么状态,它可能持有某把锁,强行停止可能会导致锁无法释放;或者可能正在操作数据库,强行停止导致数据不一致。
一个线程什么时候可以退出只有线程自己直到,所以,这里介绍的 interrupt 方法本质不是用来中断一个线程,而是将线程设置为中断状态。当我们调用线程的 interrupt 方法时,它有2个作用:
如果此线程处于阻塞状态,就会立马退出阻塞,并抛出 InterruptedException 异常,线程可以通过捕获 InterruptedException 异常来做一定处理,从而提早终结被阻塞状态。
如果线程正在运行,就不受任何影响,仅仅只是中断标记被置为 true 了。
如果 interrupt()方法先被调用,然后线程开始调用阻塞方
法进入阻塞状态,InterruptedException异常依旧会抛出;如果线程
捕获InterruptedException异常后,继续调用阻塞方法,将不再触发
InterruptedException异常。
1.5.4 线程的join操作
也就是线程的合并操作,可以用一个例子来说明: 假设线程 A 和 B,现在 A 执行过程中对 B 的执行有依赖:A 需要将 B 的执行流程合并到自己执行的流程中,这就是线程合并,伪代码大概如下:
1 | class ThreadA extends Thread { |
上述代码中,执行 threadb.join() 这行代码的当前线程为合并线程(线程 A)会进入 TIMED_WAITING 等待状态,让出 CPU。所以上述过程应该是等 B线程执行完成之后,A 线程再继续执行
1.5.5 线程的 yield 操作
线程的 yield(让步)操作的作用是让目前正在执行的线程放弃当前的执行,让出 CPU执行权限,之后变为 RUNNABLE 状态,从操作系统层面来讲就是进入了就绪状态(而不是阻塞)。在线程 yield 时,线程放弃和重占 CPU 的时间是不确定的,可能刚放弃了 CPU ,马上又获得了 CPU 的执行权限,接着开始执行了。
1.5.6 线程的daemon操作
守护线程也称为后台线程,专门指在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个JVM进程,都会在后台运行一系列的 GC 线程,这些 GC 线程就是守护线程。
只要 JVM 中尚存任何一个用户线程没有结束,守护线程就能执行自己的工作,只有当最后一个用户线程结束,守护线程随同 JVM 一同结束工作。
守护线程的要点
使用守护线程时,有几点需要特别注意:
线程必须在start() 方法调用前设置其为守护线程(即调用setDaemon(true)方法) ,否则会抛出 InterruptedException异常
在守护线程中创建的线程,新的线程都是守护线程。如果要创建用户线程,需要显式调用 setDaemon(false)
1.6 线程池原理与实战
Java 线程的创建非常昂贵,需要 JVM 和 OS 配合完成大量工作:
- 为线程堆栈分配大量内存快,包括至少 1M 的栈内存
- 需要进行系统调用,以便在OS中创建和注册本地线程
而线程池的出现主要解决了以下问题:
提升性能:最大限度地复用已经创建的线程,避免创建和销毁,提升性能
线程管理:线程池可以保持对线程的统计信息,例如完成任务数量、空闲时间等,能对异步任务进行高效调度(我个人觉得还有可能根据cpu的核心数之类的确定线程数量)。
1.6.1 JUC 的线程池架构
JUC 是 java.util.concrrent 工具包的简称,是用于完成高并发、处理多线程的一个工具包。其中的线程池类与接口的架构图大致如下:
1.6.2 Executors 的4种快捷创建线程池的方法
Java 通过 Excutors 工厂类提供了4种快捷创建线程池的方法,如下表所示:
方法名 | 功能简介 |
---|---|
newSingleThreadExcutor | 创建只有一个线程的线程池 |
newFixedThreadPool | 创建固定大小的线程池 |
newCachedThreadPool | 创建一个不限制线程数量的线程池,任何提交的任务都立即执行,但是空闲线程会得到及时回收 |
newScheduledThreadPool | 创建一个可定期或者延时执行的任务的线程池 |
总的来说,newSingleThreadExcutor 线程池有以下特点:
线程池中的任务是按照提交的顺序执行的
池中的唯一线程的存活时间是无限的
当池中唯一线程繁忙时,新提交的任务会进入内部的阻塞队列,这个队列是无界队列
线程池使用完后,要调用 shutDown 来关闭线程池,此时线程池将变为 SHUTDOWN 状态,将拒绝新任务,添加新任务会抛出 RejectedExecutionException 异常,此时,线程池不会立刻退出,直到线程池中的任务都执行完成。还有另一个方法 shutdownNow ,执行之后线程状态会立刻变成 STOP ,并且不再处理了还在阻塞队列中等待的任务,会返回哪些未执行的任务。
newFixedThreadPool用于创建一个“固定数量的线程池”,它的特点如下:
- 提交任务时,如果显成熟没有达到固定数量,线程池内就会创建一个新线程,直到达到固定数量
- 提交新任务时,如果所有的线程都在繁忙,则新任务会进入阻塞队列,该队列是无界的阻塞队列
“固定数量的线程池”适合场景: 需要任务长期执行的场景,比如处理CPU密集型任务。线程数能够比较稳定地维持在某个数量,避免频繁回收和创建线程。
newCachedThreadPool 创建“可缓存线程池”,如果线程池内某些线程成为空闲线程,“可缓存线程池”能灵活回收这些线程。它的特点大致如下:
- 接收新任务时,如果池内所有线程繁忙,则添加新线程来处理
- 对线程池大小没有限制,完全依赖于JVM能够创建的最大线程大小
- 如果线程空闲(60s不执行任务),就会回收
它的适合场景:需要快速处理突发性强,耗时短的任务场景,如 Netty 的 NIO 处理场景、REST API 接口瞬时削峰场景。
newScheduledThreadPool 创建“可调度线程池”,它提供一个“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。使用方法如下:
1 | ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2); |
“可调度线程池”的适用场景:周期性地执行任务的场景。
1.6.3 线程池的标准创建方式
大部分企业的开发规范会禁止时候用上述Java 提供的4种创建线程池的方式。,要求通过标准创建方式自行创建。标准创建的一个构造方法如下:
1 | // 使用标准构造器构造一个普通的线程池 |
corePoolSize 与 maximumPoolSize
corePoolSize 与 maximumPoolSize 自动维护线程池中的工作线程,规则如下:
- 收到新任务,并且当前线程数少于 corePoolSize,即使其他线程处于空闲状态,也会创建新线程来处理该请求
- 核心线程不会被回收,但是如果设置了 allowCoreThreadTimeOut 的话,则会例外,这时候是可以被回收的
- 如果当前线程数大于 corePoolSize ,但是小于 maximumPoolSize ,那么仅当任务排队队列已满才会创建新线程。
- 当 maximumPoolSize 被设置为无界值(如Integer.MAX_VALUE,因为这是int的最大值了)时,线程池可以接受任意数量的并发任务
- corePoolSize 与 maximumPoolSize 的值可以动态更改
根据上述规则可知,给 corePoolSize 和 maximumPoolSize 设置相同的值可以创建一个固定大小的线程池。
BlockingQueue
BlockingQueue 也就是阻塞队列,如果线程池的核心线程都在忙,则收到的任务都缓存在阻塞队列中
keepAliveTime
用于设置线程的最大 Idle 市场,如果非 Core 线程(默认情况下只针对非Core线程,但如果allowCoreThreadTimeOut 设置为true,则也会应用于Core线程)空闲超过这个时常,就会被回收。如果要防止 Idle 线程被回收,可以将 keepAliveTime 设置为 Long.MAX_VALUE。当然,这个值也是能动态调整的。
1.6.4 向线程池提交任务的2种方式
有 submit 和 execute 两种方式,这二者的区别:
- submit 有返回值 Future ,execute 没有
- 由于 submit 有返回值Future ,所以可以方便对当前任务处理 Exception
- 入参不同,submit 可以接收 Callable、Runnable 两种,execute 只能接收 Runnable
1 | //submit 结果获取和异常处理 |
1.6.5 线程池任务调度流程
任务调度流程大致如下:
- 接收任务时,如果当前线程数量小于核心线程数,则创建线程(哪怕其他线程空闲),然后执行任务
- 如果线程池中的线程大于核心线程数量,新任务将被加入阻塞队列,直到阻塞队列满了
- 完成一个任务后,优先从阻塞队列中获取下一个任务,直到阻塞队列为空
- 当核心线程繁忙,并且阻塞队列也已经满了,如果再接收到新任务,将会为新任务创建一个(非核心)线程,并立即开始执行新任务。
- 当核心线程繁忙,阻塞队列满的情况下,来新任务会一直创建线程,当线程总数超过 maximumPoolSize 时就会执行拒绝策略。
1.6.6 ThreadFactory (线程工厂)
略
1.6.7 BlockingQueue(任务阻塞队列)
阻塞队列与普通队列相比有一个重要的特点: 一个线程从一个空的阻塞队列获取元素时会被阻塞(怎么实现的?),直到队列中有了元素;当队列中有了元素,被阻塞的线程会被自动唤醒。比较常用的实现类有以下几种:
- ArrayBlockingQueue:数组实现的有界阻塞队列,队列中元素按照 FIFO 排序。
- LinkedBlockingQueue:基于链表实现的阻塞队列,按照 FIFO 排序,可以设置容量(有界队列),不设置容量则默认使用 Integer.Max_VALUE 作为容量(即无界队列)。
- PriorityBlockingQueue: 具有优先级的无界队列
- DelayQueue:无界阻塞延迟队列,底层基于 PriorityBlockingQueue,队列中每个元素都有过期时间,当从队列获取元素时,只有已经过期的元素才会出队,队列头部是做早过期的元素
- SynchronousQueue :同步队列,不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入一直处于阻塞状态。
Excutors.newScheduledThreadPool所创建的线程池就是使用 DelayQueue
而,Executors.newCachedThreadPool 所创建的线程池使用的是 SynchronousQueue
1.6.8 调度器钩子方法
- beforeExecute: 执行目标实例前在工作线程异步执行该方法
- afterExecute: 执行目标实例后在工作线程异步执行该方法
- terminated: 线程池终止时的钩子方法
1.6.9 线程池的拒绝策略
当出现如下情况时,新提交的任务会被拒绝:
- 线程池已经被关闭
- 所有线程繁忙,并且线程数已经达到了 maximunPoolSize
当拒绝的时候,会调用 RejectedExecutionHandler 实例的 rejectedExecution 方法,JUC 提供了以下几种实现:
- AbortPolicy : 新任务丢掉同时抛出 RejectedExecutionException 异常,这是线程池默认策略
- DiscardPolicy: 新任务直接丢掉,并且不会抛出异常
- DiscardOldestPolicy: 抛弃最早的任务,然后让新任务入队
- CallerRunsPolicy: 调用者执行策略,提交任务线程会自己执行该任务
- 自定义策略
1.6.10 线程池的优雅关闭
优雅地关闭线程池主要涉及的方法有 3 个:
- shutDown: 这是有序关闭线程池的方法,调用之后当前线程会立即返回,不会等待线程池关闭完成。会等待当前工作队列中剩余的任务全部执行完毕,才关闭线程池。只要调用了这个方法,线程池会转为 SHUTDOWN 状态,不会再接收任务
- shutDownNow :立即关闭线程池,调用之后当前线程会立即返回,不会等待线程池关闭完成。
- awaitTermination: shutDown与shutDownNow调用后会立即返回,不管线程池的关闭。而这个方法就是用于等待线程池完成关闭。
优雅关闭线程池的方法:
- 调用 shutDown 方法,拒绝新任务提交
- 调用 awaitTermination(long timeOut) 指定超时时间,判断是否已经关闭
- 如果 awaitTermination 超时,就可以进入循环关闭,循环一定次数(比如1000次),不断关闭线程池,直到关闭或者结束
- 如果 awaitTermination 返回false 或者 被中断,调用 shutDownNow 立即关闭
1.6.11 Executors 快捷创建线程池的潜在问题(系统默认提供的4种方法)
- newFixedThreadPool创建的固定大小线程池:它的阻塞队列是 LinkedBlockingQueue 类型的无界队列,可能导致大量的任务等待,队列太大还可能导致 OOM
- 单线程的线程池也是无界队列,可能导致OOM
- newCachedThreadPool 创建可缓存线程池:因为核心线程数 0 ,最大线程数为 Integer.MAX_VALUE,阻塞队列为 SynchronousQueue
同步队列,不缓存任务,所以理论上任务数量不受限制,相当于来一个任务首先寻找可用线程,没有的话,就创建一个新的。会导致创建的线程过多,可能造成 OOM ,甚至把 CPU资源耗尽 - newScheduledThreadPool 创建可调度线程池,其最大问题是线程数量不设上限,可能导致CPU资源耗尽
1.7 确定线程池的线程数
1.7.1 按照任务类型分类
- IO 密集型: IO任务操作时间长,导致 CPU 利用率不高,常处于空闲状态。线程数确定:设置allowCoreThreadTimeOut为true、使用有界队列、corePollSize 和 maximunPoolSize 保持一致,使得接收新任务而没有空闲线程时,直接创建新线程执行,而不是在阻塞队列中等待。
- CPU 密集型: 主要是计算任务,CPU一直在运行,利用率很高。线程数确定:CPU密集型任务虽然可以并行完成,但是并行任务越多,花在任务切换的时间也越多,效率也就越低。CPU密集型任务并行执行的数量应当等于CPU的核心数。
- 混合性任务:既要逻辑计算,又要IO。线程数确定:业界有一个公式
1.8 ThreadLocal 原理与实战
ThreadLoacal 的应用场景大致有 2 类:
- 线程隔离
- 跨函数传递数据
原理:每一个线程在获取本地值时,都会将 ThreadLocal 实例作为 Key 从自己拥有的 ThreadLocalMap 中获取值,别的线程无法访问自己的 ThreadLocalMap ,自己也无法访问别人的。并且,由于 ThreadLocalMap 是线程私有的,当线程销毁时 ThreadLocalMap 也随之销毁,在一定程度上减少内存的消耗。
1.8.8 ThreadLocal 综合使用案例
ThreadLocal 使用不当会造成严重的内存泄露,为了避免这种情况,使用时应遵守以下原则:
- 尽量使用 private static final 修饰ThreadLocal ,final为了避免他人修改、变更 ThreadLocal 变量的引用,使用static 为了确保全局唯一
- ThreadLocal 使用完后务必调用 remove 方法,这是简单有效避免引发内存泄露问题的方法