创建线程的方式
- 继承
Thread
类 - 实现
Runnable
接口 - 实现
Callable<T>
接口, 泛型类型与重写的call()
方法返回值类型相同 - 线程池创建线程
Runnable
和Callable
不同:
Runnable
的run
方法没有返回值,Callable
的call
方法有返回值,搭配FutureTask
可以获取异步执行的结果Callable
的call
方法可以抛出异常,Runnable
的run
方法不能向上抛异常
wait 和 sleep 异同
同: 让当前线程暂时放弃 CPU 使用权, 进入阻塞状态
异:
-
方法归属不同
sleep(long)
是Thread
的静态方法wait()
,wait(long)
是Object
的成员方法, 每个对象都有
-
醒来时机不同
sleep(long)
和wait(long)
都在等待对应毫秒后醒来wait(long)
和wait()
可以被notify()
唤醒,wait()
不被唤醒就一直等下去- 都可以被打断唤醒
-
锁特性不同
wait()
方法调用必须先获取 wait 对象的锁,sleep()
没有这个限制wait
方法执行后会释放对象锁, 允许其他线程获取该对象锁sleep
如果在synchronized
代码块中执行则不会释放对象锁
打断正在运行的线程
- 设置标志,
while(flag){...}
,flag=false
时退出 - 调用线程的
stop()
方法 (强行打断, 不推荐) - 使用
interrupt()
方法, 阻塞线程被打断会抛异常, 线程打断会改变线程打断状态.isInterrupted()
被打断后为true
, 正常线程可以通过判断这个来决定是否退出.
synchronized
底层由 monitor 实现, 线程获得锁需要使用对象关联 monitor, monitor 有三个属性
- owner: 关联的获得锁的线程, 且只能关联一个线程
- entrylist: 关联处于阻塞的线程, 当 owner 为空时去关联 owner, 没有先后顺序(非公平锁)
- waitset: 关联调用了
wait()
方法(处于 waiting 状态)的线程
monitor 由 jvm 提供,涉及到用户态和内核态的切换、进程的上下文切换,是重量级锁,成本较高,性能较低。
HotSpot 虚拟机中,对象在内存中存储的布局由三部分:
- 对象头:
- MarkWord:对象头
- Klass Word: 描述对象实例的具体类型
- 实例数据:成员变量
- 对齐填充:如果(对象头 + 实例数据)不是 8 的整数倍,则通过对齐填充补齐
对象上重量级锁后,对象头的 Mark Word 中就被设置指向 monitor 对象的指针
轻量锁
在很多的情况下,在 Java 程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此 JVM 引入了轻量级锁的概念。
加锁过程
- 在线程栈中创建一个 Lock Record,将其 obj 字段指向锁对象。
- 通过 CAS 指令将 Lock Record 的地址存储在对象头的 Mark Word 中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置 Lock Record 第一部分为 null,起到了一个重入计数器的作用。
- 如果 CAS 修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
- 遍历线程栈,找到所有 obj 字段等于当前锁对象的 Lock Record。
- 如果 Lock Record 的 Mark Word 为 null,代表这是一次重入,将 obj 设置为 null 后 continue。
- 如果 Lock Record 的 Mark Word 不为 null,则利用 CAS 指令将对象头的 mark word 恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁1
偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 中,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
加锁过程
- 查看 Mark Word 中偏向锁的标识以及锁标志位,若是否偏向锁为 1 且锁标志位为 01,则该锁为可偏向状态。
- 若为可偏向状态,则测试 Mark Word 中的线程 ID 是否与当前线程相同,若相同,则直接执行同步代码,否则进入下一步。
- 当前线程通过 CAS 操作竞争锁,若竞争成功,则将 Mark Word 中线程 ID 设置为当前线程 ID,然后执行同步代码,若竞争失败,进入下一步。
- 当前线程通过 CAS 竞争锁失败的情况下,说明有竞争。当到达全局安全点时之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
解锁过程
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销需要等待全局安全点(即没有字节码正在执行),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。
JMM
java 内存模型,定义共享内存中多线程程序读写操作的行为规范。内存分为两块,私有线程的工作区域(工作内存)和所有线程的共享区域(主内存)。线程之间相互隔离,交互需要通过主内存。
CAS
CAS: 乐观锁思想,在无锁的情况下保证线程操作共享数据的原子性。
底层依赖Unsafe
类直接调用操作系统底层 CAS 指令
volatile
保证线程之间的可见性: 防止编译器优化(JIT), 让一个线程对共享变量的修改对另一个线程可见
禁止指令重排序: 在读写共享变量时加入不同的屏障, 阻止其他读写操作越过屏障, 从而阻止重排序.
-
写变量让
volatile
修饰的变量在代码最后位置 -
读变量让
volatile
修饰的变量在代码最前位置
AQS
AQS: AbstractQueuedSynchronizer, 是构建锁或者其他同步组件的基础框架
synchronized | AQS |
---|---|
关键字, c++实现 | java 实现 |
悲观锁, 自动释放锁 | 悲观锁, 手动开启和关闭 |
锁竞争激烈时是重量级锁, 性能差 | 锁竞争激烈时有多种解决方案 😅 |
常见实现类:
- ReentrantLock
- Semaphore
- CountDownLatch
非公平锁: 新的线程和队列中的线程共同抢占资源
公平锁: 新线程到队列中等待, 只让队列的 head 线程获取锁
ReentrantLock
使用 CAS+AQS 实现, 对比synchronized
的特点:
- 可中断, 指获取到锁的过程中可以被打断,
lockInterruptibly()
方法是中断锁 - 可以设置超时时间,
tryLock()
可以设置超时时间 - 可以公平和非公平
- 支持多个条件变量
死锁
死锁条件:
- 互斥
- 不可剥夺
- 循环等待
- 请求和保持
ConcurrentHashMap
1.7: 分段的数组+链表, segment 分段锁使用 ReentrantLock, HashEntry 使用 CAS
1.8: 数组+红黑树+链表, CAS+synchronized 保证并发安全
并发安全
并发特性:
- 原子性: synchronized, lock
- 内存可见性: volatile, synchronized, lock
- 有序性: volatile
线程池
线程池参数
- corePoolSize: 核心线程数
- maximumPoolSize: 最大线程数 = 核心线程数 + 非核心线程数
- keepAliveTime: 非核心线程生存时间, 期间没有新任务则释放线程资源
- unit: 生存时间单位
- workQueue: 阻塞队列,没有空闲核心线程时, 新任务加入到队列中, 队列满则会创建非核心线程执行任务,非核心线程执行完新任务然后才执行阻塞队列中的任务
- threadFactory: 线程工厂, 定制线程对象的创建(设置线程名字,是否为守护线程)
- handler: 拒绝策略,所有线程都在繁忙,workQueue 也放满时触发拒绝策略
拒绝策略
- AbortPolicy: 直接抛出异常,默认策略
- CallerRunsPolicy: 用调用者线程执行任务
- DiscardOldestPolicy: 丢弃阻塞队列中最靠前的任务,并执行当前任务
- DiscardPolicy: 直接抛弃任务
阻塞队列
- ArrayBlockingQueue: 基于数组结构的有界阻塞队列,FIFO
- LinkedBlockingQueue: 基于链表结构的有界阻塞队列,FIFO
- DelayedWorkQueue: 优先级队列,保证每次出队的任务都是当前队列中执行时间最靠前的
- SynchronousQueue: 不存储元素的阻塞队列,每次插入都必须等待一个移出操作
核心线程数
假设:服务器 CPU 核心数为 $N$
IO 密集型: $2N+1$
CPU 密集型: $N+1$
线程池种类
FixedThreadPool: 固定线程数线程池. 核心线程和最大线程数相同, 阻塞队列为 LinkedBlockingQueue, 最大容量为Integer.MAX_VALUE
SingleThreadPool: 单线程化线程池, 使用唯一线程执行任务, 保证所有任务按 FIFO 执行. 核心线程数和最大线程数都为 1, 阻塞队列为 LinkedBlockingQueue, 最大容量为Integer.MAX_VALUE
CachedThreadPool: 可缓存线程池. 核心线程数为 0, 最大线程数为Integer.MAX_VALUE
, 阻塞队列为 SynchronousQueue.
ScheduledThreadPool: 可以执行延迟任务的线程池, 支持定时和周期性任务执行. 最大线程数为Integer.MAX_VALUE
, 存活时间为 0, 阻塞队列为 DelayedWorkQueue.
FixedThreadPool 和 SingleThreadExecutor 因为阻塞队列没有限制, 可能会导致 OOM; CachedThreadPool 因为最大线程数没有限制, 也可能导致 OOM.
所以推荐使用 ThreadPoolExecutor 自己填各个参数来创建线程池.
使用场景
- 数据导入, 多个线程分部分导入大量数据
- 结果汇总, 多个线程同时请求计算结果, 最后合并结果
- 异步调用, 使用
@Async
异步调用, 可以自定义线程池
JVM 组成
程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址。
java 堆:线程共享区域,保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则 OOM。
栈内存存储局部变量和方法调用,线程私有
堆内存存储 java 对象和数组,线程共有
方法区(元空间)存储类的信息和运行时常量池,是各个线程共享的内存区域
直接内存
不属于 JVM 中的内存结构,不由 JVM 管理,是虚拟机的系统内存。常见于 NIO 操作,用于数据缓冲区。分配回收成本较高,但读写性能高。
类加载器
类加载器:将字节码加载到 JVM 中
双亲委派机制
双亲委派机制:加载一个类时,先委托上一级加载,若上级加载器也有上级,则继续向上委托,如果该类委托上级没有被加载,则子加载器尝试加载该类。
双亲委派机制好处:
- 避免一个类被重复加载,父类加载后则无需重复加载,保证唯一性
- 保证类库 API 不会被修改
类加载
垃圾回收
引用计数法:每个对象都有一个引用计数器,当对象被引用时,计数器加一,当引用失效时,计数器减一,当计数器为 0 时,对象被回收。循环引用无法解决。
可达性分析:从 GC Roots 出发,向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
GC Roots 包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 Native 方法引用的对象
垃圾回收算法
标记清除方法
- 根据可达性分析算法得出的垃圾进行标记
- 对这些标记为可回收的内容进行垃圾回收
优点:标记和清除速度较快
缺点:碎片化较为严重,内存不连贯
标记整理算法
先标记,将存活对象都向内存的另一端移动,然后清理边界外的垃圾。
优点:无内存碎片
缺点:需要移动对象,效率低
老年代使用
复制算法
复制一块相同的区域,在一个区域进行标记后,将存货的对象复制到另一块区域,然后原区域全部清空。
优点:垃圾对象较多时效率较高,清理后内存无碎片
缺点:分配两块内存空间,同一时刻只能用一块,内存使用率较低
新生代使用
分代回收
在堆中:新生代 : 老年代 = 1 : 2
在新生代中:Eden : from : to = 8 : 1 : 1
工作机制:
- 新建对象分配到 Eden 区
- Eden 内存不足时,标记 Eden 和 from 的存活对象,使用复制算法复制到 to 中,然后清空 Eden 和 from 区
- 一段时间后,Eden 区内存再次不足时,标记 Eden 和 to 中存货对象复制到 from 中,清空 Eden 和 to 区
- 在幸存区(from 和 to)对象经历了多次回收(最多 15 次)后,晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
- MinorGC: 新生代的垃圾回收,暂停时间短(STW)
- MixedGC: 新生代+老年代部分区域的垃圾回收,G1 收集器特有
- FullGC: 新生代+老年代完整垃圾回收,暂停时间长(STW),尽量避免
STW: stop the world,暂停所有线程,等待垃圾回收完成。
垃圾回收器
串行垃圾收集器
- Serial 用于新生代,复制算法
- Serial Old 用于老年代,标记整理算法
使用单线程进行垃圾回收,垃圾回收时只有一个线程工作,java 应用中所有线程都要暂停(STW),堆内存较小,适合个人电脑。
并行垃圾收集器
- Parallel New 用于新生代,复制算法
- Parallel Old 用于老年代,标记整理算法
JDK8 默认,垃圾回收时多个线程工作,java 应用中所有线程都要暂停(STW)
CMS 垃圾收集器
CMS(Concurrent Mark Sweep)并发的,用于老年代,标记清除算法,进行垃圾回收时应用仍然能正常运行
- 初始标记:只标记和 GC Roots 直接关联的对象,STW 时间短
- 并发标记:继续标记
- 重新标记:再次检查,防止有变化
G1 垃圾收集器
G1(Garbage First)用于新生代和老年代,复制算法,JDK9 后默认
划分为多个区域,每个区域都可以充当为 eden, survivor, old, humongous(大对象)
并发失败(回收速度赶不上创建新对象速度)时触发 Full GC
-
新生代回收
-
并发标记
-
混合收集
复制完成后进入下一轮回收
引用类型
强引用:只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收
软引用:第一次垃圾回收后内存依旧不足,触发第二次垃圾回收时会被回收
弱引用:垃圾回收时无论是否内存充足都会就被回收
虚引用:必须配合引用队列使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用相关方法释放直接内存