第一章 快速认识线程
1.3 线程的生命周期
NEW、RUNNABLE、RUNNING、BLOCKED、TERMINATED
1.5 Runnable
Runanble的好处
Runnable负责逻辑执行单元的部分,将可执行的逻辑单元和线程控制分离开来,让多个线程引用同一数据资源
第二章 深入Thread构造函数
2.3 ThreadGroup
构造Thread时可以指定其ThreadGroup,若没有指定,则会将当前线程(父线程)的ThreadGroup作为其ThreadGroup
2.6 守护线程
当JVM中没有一个非守护线程,程序会退出,守护线程一般用于处理一些后台工作,例如垃圾回收线程
如何使用
setDaemon()可以设置为守护线程,
应用场景
希望程序退出时,一些线程能够关闭,例如实时同步数据的线程等等
第三章 Thread API
3.7 interrupt
interrupt flag
中断标识,表示当前线程被interrupt,会被设置interrupt flag
如果当前线程正在执行可中断方法时(例如wait sleep),会抛出InterruptException,并将interrupt flag清除
interrupt
打断当前线程的阻塞状态,继续执行之后的代码
isInterrupted
判断当前线程是否被中断,此方法不会影响interrupt flag
interrupted
静态方法,判断当前线程是否被中断,并擦除interrupt flag
3.8 join
当前线程等待此线程至生命周期结束或者指定的时间
Thread.currentThread().join()
可以一直挂着当前线程,永远不会结束(当前线程一直在等待当前线程结束,所以当前线程永远不会结束)
应用场景
例如一个收集服务器信息的多线程程序,每一个线程同步负责一个服务器的采集工作,当所有的服务器采集结束后,计算结束时间并做一些数据逻辑、发送消息等操作,可将所有的采集线程join到父线程,最后在父线程计算结束时间
关闭线程
1. 捕获中断信号
使用isInterrupted()方法,判断当前线程被中断,停止程序继续执行
2.使用volatile修饰的开关标识flag
在当前线程中增加一个volatile修饰的closed标识,增加一个close方法
public void close() {
this.closed = true;
this.interrupt();
}
3. 强制关闭
1. 使用场景
有时候当前线程在做一些非常耗时的工作,无法做一些中断信号的判断
2. 如何关闭当前线程?
在执行线程A中设置一个守护线程B,在B中做真正的逻辑操作,当需要关闭线程A时,将线程A打断,守护线程B会被JVM强制结束
第四章 线程安全与数据同步
4.2 初识synchronized
对代码块进行修饰,保证不同的线程对共享变量的单线程执行、互斥访问,从而防止数据不一致的问题出现
synchronized 包括 monitor enter 和 monitor exit 两个JVM指令
等待的线程将会进入BLOCKED状态
4.3 深入synchronized
monitor
每个对象都与一个monitor相关联,一个monitor的lock锁在同一时间只能被一个线程获得
monitor的所有权争夺过程
1. monitor初始计数器为0,意味着该monitor的lock还没有被获得,A线程获得之后立即对该计数器加1,表示A线程为该monitor的所有者
2. A线程重入,计数器再次累加
3. B线程尝试获取该monitor的所有权,陷入阻塞(BLOCKED)状态直至monitor的计数器变为0,才能再次尝试获取
monitor exit
释放对monitor的所有权,其计数器减1,如果计数器结果为0,表示该线程不再拥有monitor的所有权
4.4 this monitor 和 class monitor
this monitor
对当前类的实例的monitor进行加锁
在非静态方法上修饰 synchronized 和对代码块修饰 synchronized(this) 具有相同的效果
class monitor
对当前类的class的monitor进行加锁
在静态方法上修饰 synchronized 和对代码块修饰 synchronized(xxx.class) 具有相同的效果
4.5 线程死锁
原因
- 交叉锁(哲学家吃面)
- 内存不足,线程彼此在等待资源释放
- 一问一答的数据交换,由于某种原因导致服务端错失了客户端的请求,服务端和客户端都在等待彼此的数据交换
- 数据库锁
- 文件锁
- 死循环引起死锁,CPU占用居高不下(鄙人碰到过,游戏中活动开始执行任务,执行结束后需要开启下一次活动,下次活动的时间设置错误导致立即执行,于是活动再次开始,无限循环,最后通过日志发现活动在无限开启才得以解决)
诊断
jps查看进程PID后,通过jstack PID获取DeadLock信息
第五章 线程间通信
5.2 单线程间通信
wait 和 notify、notifyAll
wait会导致当前线程进入阻塞,直到其他线程调用了同一资源的notify或者notifyAll方法
wait方法必须必须拥有该对象的monitor所有权,也就是必须在在同步方法中使用
当前线程执行了该对象的wait后会放弃该对线的monitor所有权,在被唤醒后将进入runnable等待争夺该对象的monitor锁
wait和sleep的区别
- wait是Object的方法,sleep是Thread特有的方法
- wait方法必须在同步方法中执行,且必须拥有相同对象的monitor所有权,sleep则不需要
- 线程在同步方法中执行sleep,并不会释放monitor所有权,wait会释放
- wait的线程被唤醒后,需要再次抢锁,但不会重复执行wait之前的操作
5.3 多线程通信
notifyAll
wait set
线程休息室,wait住的线程会进入到此数据结构中,notify会从里面弹出一个线程(JVM自定义规则),notifyAll会将所有线程弹出
5.4 自定义显式锁
synchronized的缺陷
- 阻塞时长无法控制
- 阻塞状态无法中断,interrupt后无法捕捉中断信号
第六章 Thread Group
API介绍
见书籍110页或者视频P34
第七章
7.1 获取线程运行时异常
介绍
Thread的run方法中不允许抛出Exception,需要使用以下方式处理运行时异常
// 1.设置具体某个线程的指定异常处理
new Thread().setUNcaughtExceptionHandler((thread, e) -> {
})
// 2.设置全局的异常处理
Thread.setDefaultUncaughtExcptionHandler((thread, e) -> {
})
7.2 Hook程序注入
介绍
当JVM进程中没有活跃的非守护线程,或者收到了系统中断信号时,JVM在退出之前会执行Hook线程
示例
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// TODO 捕获到RuntimeException,做资源释放等操作
}));
实战
为防止某程序重复启动
1. 在启动时检查是否存在 .lock 文件,
2. 注入Hook程序,在程序退出时删除 .lock 文件
获取程序调用方法栈
Thread.currentThread().getStackTrace()
第八章 线程池原理
8.1 线程池原理
一个完整的线程池需要具备以下要素:
- 任务等待队列
- 线程数量管理,包括初始数量init(min),活跃数量active,最大数量max
- 任务拒绝策略,若线程数量已达到上限且等待队列已满,需要有相应的拒绝策略
- 线程工厂,用于个性化定制线程
- QueueSize,任务队列的长度,任务队列主要存放提交的Runnable,为了防止内存溢出
- KeepedAlive时间,主要决定线程各个重要参数自动维护的时间间隔
第九章 类的加载过程
9.1 类的加载过程简介
三大阶段
加载阶段:主要负责查找类的class文件
-
连接阶段:
- 验证:确保类的正确性,比如class的版本,魔数
- 准备:为类的静态变量分配内存,并初始化为默认值
- 解析:把类中的符号引用转换为直接引用
初始化阶段:为类的静态变量赋予正确的初始值
类的初始化时间
当一个类在首次使用的时候才会进行初始化,是一个延迟lazy的机制
9.2 类的主动使用和被动使用
6种主动使用
- new关键字
- 访问类的静态变量
- 访问类的静态方法
- 对类进行反射操作
- 初始化其子类
- 启动类,也就是执行main函数所在的类
其他称作被动使用
例如:
- 构造某个类的数组
- 引用类的静态常量
9.3 类的加载过程详解
9.3.1 类的加载阶段
类加载的最终产物是堆内存中的class对象,同一个ClassLoader,对同一个类,始终对应堆内存中的一个class对象。
加载类的方式:
- 本地磁盘直接加载
- 内存中直接加载
- 网络加载
- zip,jar文件中加载
- 数据库中的blob字段提取二进制数据加载
- 动态编译加载
9.3.2 类的连接阶段
-
验证:
-
验证文件格式
- 魔数
- 主次版本号
- MD5验证class文件的字节流是否有残缺或其他附加信息
- 常量池中的常量是否有不支持的类型
- etc
-
-
元数据的验证
- 检查父类信息
- 父类是否可继承
- 是否实现了抽象方法
- 方法重载的合法性
- etc
-
字节码验证
- 保证类型转换合法
- etc
-
符号引用验证
- 调用了一个不存在的方法
- 符号引用中的类、字段、方法,是否对当前类可见
- etc
准备:为类的静态变量分配内存,并初始化为默认值
-
解析:为类的静态变量赋予正确的初始值
- 类接口解析
- 字段的解析
- 类方法的解析
- 接口方法的解析
9.3.3 类的初始化
最主要的事情就是执行<clinit>()方法,即 class initialize
- <clinit>()方法在编译阶段生成
- JVM保证<clinit>()方法的线程安全性
- 静态语句块中,只能访问到定义在静态语句块之前的变量,对于定义在它之后的变量,只能赋值,不能访问
- <clinit>()方法不需要显式地调用父类的构造函数,JVM会保证父类的<clinit>()方法先执行,因此Object的<clinit>()方法会最先执行
- 由于父类的<clinit>()方法先执行,所以定义在父类中的静态语句块,要优先于子类
第十章 JVM类加载器
10.1 JVM内置三大类加载器
-
跟(Bootstrap)类加载器
由C++编写,是最顶层的加载器
可通过 -Xbootclasspath 来指定跟加载器的路径
其所加载的类库可通过系统属性 "sun.boot.class.path" 获得
-
扩展类加载器
由纯JAVA编写,主要是用于加载JAVA_HOME下的 jre\lb\ext 目录里的类库
完整类名是 "sun.misc.Launcher$ExtClassLoader"
其所加载的类库可通过系统属性 "java.ext.dirs" 获得
-
系统类加载器
负责加载classpath下的类库
加载路径一般通过 -classpath 或者 -cp 指定
其所加载的类库可通过系统属性 "java.class.path" 获得
10.2 自定义类加载器
所有的自定义类加载器都是java.lang.ClassLoader的子类,它是一个抽象类,虽然没有抽象方法,但子类必须实现findClass方法,否则会抛出ClassNotFoundException
10.2.1 自定义类加载器
findClass的实现示例:
10.2.2 父委托机制(双亲委托)
当一个类加载器被调用了 loadClass 之后,它会递归地交给它的父加载器尝试加载,如果找不到,再逐层往下交给子加载器加载
10.2.3 破坏双亲委托机制
重写 loadClass 方法
详见P92 或者 书籍168页
TIP 面试题
如果自定义一个String类,其包名也是java.lang,你调的是哪一个呢?
- 因为ClassLoader的双亲委托机制,调的是JDK中的String
有没有方法调到自己写的String类呢?
- 可以自定义自己的ClassLoader,重写其中的loadClass方法,破坏其双亲委托机制,理论上是可行的,但会抛出一个SecurityException,因为JVM对 java.lang 的包名做了安全检查
10.2.4 类加载器的命名空间、运行时包、类的卸载
加载器的命名空间:命名空间是由该加载器及其所有父加载器所构成,使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个class,则会在堆内存和方法区产生多个class的对线
运行时包:运行时包是由类加载器的命名空间和类的全限定名称共同组成
初始类加载器:每个类加载器有一个维护列表,在类的加载过程中,所有参与的类加载器,即使没有亲自加载,也会将此class类添加进维护列表中,所有这些类加载器,称为该类的初始类加载器
类的卸载:通过 -verbose:class 可查看JVM在运行期间到底加载了多少class,类的卸载需要满足以下三个条件,才会被GC回收
该类所有的实例都已经被GC
加载该类的ClassLoader实例已经被GC
该类的class实例没有在其他地方被引用
第十一章 线程上下文类加载器
11.1 为什么需要线程上下文类加载器
例如JDBC,提供了高度抽象,我们只需要面向接口编程,但java.lang.sql中的所有接口都是由跟加载器加载,而第三方提供的具体实现是由系统类加载器加载,由于双亲委托机制,无法访问到各大厂商的具体实现
11.2 数据库驱动的初始化源码解析
- Class.forName("com.mysql.jdbc.Driver") 用当前线程的类加载器主动访问该类
- Thread.currrentThread().getContextClassLoader() 获取当前线程的上下文类加载器,即步骤1中的加载器,通常是系统类加载器
- 使用当前线程的上下文类加载器去加载mysql的所有driver驱动类,调用具体的mysql的connect实例方法,返回真正的connection实例
第十二章 volatile关键字
12.1 初识 volatile
保证一个共享变量的可见性
12.2 CPU缓存一致性问题
- CPU有自己的缓存,其目的是为了提高运算效率,CPU在计算前会将主内存中的数据读到CPU缓存中再进行计算,最后再刷到主内存中去
- 每个线程都有自己的工作内存(本地内存,对应CPU中的Cache),共享变量会在每个线程的本地内存中存在一个副本
- 在一个线程中,如果没有对一个共享变量进行写操作,JVM将其优化认为其不需要从主内存中读取数据,所以每次都是读取的自己在CPU缓存中的副本
第十三章 深入volatile
13.1 并发变成的三个重要特性
- 原子性:在一次的操作或多次操作中,要么所有操作全部得到了执行,要么所有操作都不执行
- 可见性:当一个线程对一个共享变量进行了修改,另外的线程可以立即看到修改后的最新值
- 有序性:程序代码再执行过程中的先后顺序性
13.2 JMM如何保证三大特性
- volatile关键字不保证原子性
- volatile关键字保证可见性
- volatile关键字保证有序性
13.3 volatile深入解析
-
如何保证可见性
- 当一个线程修改volatile修饰的变量,会立即将其刷新到主内存中去
- 对于变量的读操作,会直接在主内存中进行(当然也会缓存到工作内存中,只是其被修改后,会被标记为失效)
- 对一个变量的写操作要早于对这个变量的读操作
-
如何保证顺序性
- 禁止对其修饰的变量进行重排序
volatile 和 synchronized 的区别
- 使用方式不同,valatile是修饰实例变量或者类变量;synchronized用于修饰方法或者语句块;volatile修饰的变量可以为null;synchronized锁住的monitor对象不能为null
- volatile无法保证原子性,而synchronized可以
- 对可见性的保证机制不同,volatile是使用机器指令 "lock;" 的方式迫使其他线程工作内存中的数据失效,不得不从主内存中再次加载;而synchronized借助于JVM指令 monitor enter 和 monitor exit 这种排他的方式阻塞其他线程,并在 monitor exit 的时候将所有共享资源刷新到主内存中去
- volatile保证有序性;而synchronized也是通过排他的形式,在synchronized修饰的代码块中指令也会重排序,但因为阻塞了其他线程,保证了最终结果的一致性
- volatile不会使线程进入阻塞,而synchronized会
实战 线程池相关API Executors
ThreadPoolExecutor
构造方法
参数详解:
- corePoolSize:线程池的核心数量
- maximumPoolSize:允许的最大线程数
- keepAliveTime:当线程数量大于核心数量时,多余的空闲线程在回收前等待新任务的最大时间
- unit:keepAliveTime的单位
- workQueue:任务在执行前放入的队列
- threadFactory:创建线程的方式,对线程可自定义化,比如设置为守护线程
- handler:线程因为到达上限且队列已满,从而阻塞时的策略(拒绝策略等)
shutdown()
当前任务未开始的直接退出(interrupt),开始了的等待结束,最后销毁线程池
shutdownNow()
当前所有任务直接打断(interrupt),且返回未开始的任务列表
如果打断时,已开始的任务里正在执行阻塞方法(sleep、wait等待),会导致已开始的任务抛出异常,中断执行
如果打断时,已开始的任务没有执行阻塞方法,则会继续执行至任务结束,效果同shutdown方法
awaitTermination(time, timeUnit)
当前线程阻塞,等待线程池中的任务结束
Executors
ThreadPool的工厂类
newCachedThreadPool()
初始0个核心线程,最大可支持 Integer.MAX_VALUE 个线程
空闲线程自动释放时间为60s
特点:
可无限扩大线程数,只要有任务进入,就会立即找到一条空闲线程处理,如果没有空闲线程则会立即创建一个新线程
与直接创建线程的好处是在60s内可以重复利用线程
任务处理完会自动结束,不需要显式调用shutdown
适合处理执行时间较短,数量较大(数量不固定)的任务
newFixedThreadPool(size)
初始核心线程和最大线程数都是size个
空闲线程不会自动释放
队列最大支持Integer.MAX_VALUE个
特点
当前可执行的线程数永久固定,不会增长
任务处理完不会自动结束,需要显式调用shutdown
适合处理数量固定的任务
newSingleThreadExecutor()
初始和最大线程数都是1个
空闲线程不会自动释放
队列最大支持Integer.MAX_VALUE个
特点
用FinalizableDelegatedExecutorService封装,屏蔽了ThreadPoolExecutor的大多数方法,不对外显示
任务处理结束后,当前线程池依然存活,可复用
空闲线程自动释放时间为0
需要显式调用shutdown
适合处理一个接一个的任务,保证所有任务的执行顺序按照任务的提交顺序执行
newWorkStealingPool()
JDK1.8新增,不是ThreadPoolExcutor的扩展,而是 ForkJoinPool的扩展
引用了Future和Callable的设计思想
根据CPU的核心数,对应地创建线程
特点:
同时处理的线程数等于CPU的核心数
任务处理完会自动结束,不需要显式调用shutdown
适合处理很耗时的任务