Java 笔记

Table of Contents

资料

读书笔记

源码分析

Java Memory Model

分为 主内存工作内存(线程)
不同线程拥有自己的工作内存, 同一个变量在主内存只有一份, 当线程用到时, 会从主内存中拷贝一个副本, 线程执行完的时候, 再将变量同步到主内存.

内存之间交互

1. lock(主) 把一个变量标记为一条线程独占的状态
2. unlock(主) 把一个处于锁定状态的变量释放出来, 释放后的变量才可以被其他线程锁定
3. read(主), load(工作) 把一个变量值从主内存传输到线程的工作内存中, 然后工作内存载入
4. use(工作) 把工作内存中的一个变量值传递给执行引擎
5. assign(工作) 把一个从执行引擎接收到的值赋给工作内存的变量
6. store(工作), write(主) 把工作内存中的一个变量的值传送到主内存中, 然后主内存写入

并发特性

原子性
一个操作或者多个操作, 要么全部执行并且执行的过程不回被任何因素打断, 要么就都不执行.
可见性
当多个线程访问同一个变量时, 一个线程修改了这个变量的值, 其他线程能够立即看得到修改的值.
有序性
程序执行的顺序按照代码的先后顺序执行(禁止指令重排序).

Happens-Before

  • 程序顺序规则
  • 监视器锁规则
  • volatile 变量规则
  • 线程启动规则
  • 线程结束规则
  • 中断规则
  • 终结器规则
  • 传递性

同步工具类

CountDownLatch - 闭锁 (底层: AQS)
初始化一个值, countdown(), countdown() … await()
Semaphore - 信号量 (底层: AQS)
初始化一个值, 用于控制最大并发量
CyclicBarrier - 栅栏 (底层: ReentrantLock)
所有线程都抵达栅栏后, 才开始执行后面的代码 (可重用)
Exchanger - 交换
交换两个线程的变量

引用类型

强引用 平常使用的都是, 无法被 GC 回收
软引用 (SoftReference) 当空间不足的时候, 才会被 GC 回收
弱引用 (WeakReference) 下一次 GC 的时候回收
虚引用 (PhantomReference) GC 回收后可见

Java 主流锁

  • 线程要不要锁住同步资源?
    • 锁住: 悲观锁
    • 不锁住: 乐观锁
  • 锁住同步资源失败, 线程要不要阻塞?
    • 阻塞
    • 不阻塞: 自旋锁 / 适应性自旋锁
  • 多个线程竞争同步资源的流程细节有没有区别?
    • 不锁住资源, 多个线程中只有一个能修改资源成功, 其它线程会重试: 无锁
    • 同一个线程执行同步资源时自动获取资源: 偏向锁
    • 多个线程竞争同步资源时, 没有获取资源的线程自旋等待锁释放: 轻量级锁
    • 多个线程竞争同步资源时, 没有获取资源的线程阻塞等待唤醒: 重量级锁
  • 多个线程竞争锁时要不要排队?
    • 排队: 公平锁
    • 先尝试插队, 插队失败再排队: 非公平锁
  • 一个线程中的多个线程能不能获得同一把锁?
    • 能: 可重入锁
    • 不能: 不可重入锁
  • 多个线程能不能共享一把锁?
    • 能: 共享锁
    • 不能: 互斥锁(排他锁)

悲观锁 vs 乐观锁

  • 悲观锁: 使用前加锁, 使用后释放锁. 例如: synchronized 与 Lock
  • 乐观锁: 在更新数据前判断有没有别的线程更新了这个值. 例如: Atomic 相关类, 底层使用 volatile 和 CAS 算法.

自旋锁 vs 适应性自旋锁

  • 自旋锁: 不放弃 CPU 时间片, 通过 for(;;) {…} 等待锁释放.
  • 适应性自旋锁: 在自旋锁的基础上, 如果超出失败次数上限(失败次数上限不固定, 由 JVM 调整), 线程将会阻塞.

无锁 vs 偏向锁 vs 轻量级锁 vs 重量级锁

Header Word (Mark Word)
默认存储对象的 HashCode, 分代年龄和锁标志位信息.
这些信息都是与对象自身定义无关的数据, 它会根据对象的锁状态复用自己的存储空间.

Klass Point
对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例.

每一个 Java 对象都有一把看不见的锁, 称为内部锁或 Monitor 锁.
Monitor 是线程私有的数据结构, 每一个线程都有一个可用的 Monitor.

无锁 0 01 对象的hashCode, 对象分代年龄、是否是偏向锁(0)
偏向锁 1 01 偏向线程ID, 偏向时间戳, 对象分代年龄, 是否是偏向锁(1)
轻量级锁  00 指向栈中锁记录的指针
重量级锁  10 指向互斥量(重量级锁)的指针

简单总结:
偏向锁通过对比 Mark Word 解决加锁问题, 避免执行CAS操作.
轻量级锁是通过 用CAS 操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能.
重量级锁是将除了拥有锁的线程以外的线程都阻塞.

官方文档: OpenJDK - Synchronization and Object Locking

1AXmgTt5hp3QdJO.gif

插图的右侧是标准的加锁过程, 只要一个对象没有无锁, 标志位一定是 01.
当 synchronized 一个对象时, Header Word 和指向对象的指针会存储在当前栈帧的锁记录中. 然后 VM 尝试通过 CAS 操作将 指向锁记录的指针 设置到 Header Word 里.
如果成功, 当前的线程就会拥有锁, 标志位变成 00, 表示这个对象被锁住了.
如果该对象已经被锁, 而导致 CAS 操作失败, VM 会先判断 Header Word 是否指向当前线程的方法栈. 如果是的话, 表示当前线程已经持有锁, 可以进入同步块继续执行.
否则, 该线程通过自旋来获得锁(初始次数为0). 但是当自旋超过一定的次数, 或者一个线程在持有锁, 一个在自旋, 又有第三个来访时, 那么轻量级锁一定会升级成重量级锁来管理等待的线程, 标志位变成 10.
在大多数情况下, 锁总是由同一线程多次获得, 不存在多线程竞争. 所以在 1.6 中引入了偏向锁的技术, 第一个获得该对象锁的的线程 ID 会设置到 Header Word 中. 表示该对象偏向于这个线程.
偏向锁只有遇到其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁, 线程不会主动释放偏向锁.

公平锁 vs 非公平锁

  • 公平锁: 线程会严格按照获取锁的顺序排队.
  • 非公平锁: 线程优先插队, 插队失败才会排队. (RenentrantLock等同步类的默认选择, 性能更优)

垃圾回收

垃圾回收算法

  • 复制算法 (用于新生代)
    将内存分成两半, 一半用完后, 将存活的数据移动到另一半
  • 标记-清除 (Mark Sweep) (用于老年代)
    效率低, 同时 GC 后会产生大量不连续的碎片
    8cynrZCMV4GUpPI.png
  • 标记-清除-整理 (Mark Sweep Compact) (用于老年代)
    将存活的全部一定到一端, 然后清除另一端的空间
    Y8VjuIcmvMoWJt2.png
  • 分代收集(商业)
    bResqE1cwmBvSOj.png
    Java8 之后: PermGen 变成了 Metaspace
    失效: -XX:PermSize 和 -XX:MaxPermGen
    生效: -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize

垃圾回收器

JAixmhlzv1Mqe36.jpg

  • 串行(Serial): 单线程, 简单、易实现、效率高.
  • 并行(ParNew): Serial 的多线程版, 可以充分利用 CPU 资源, 减少回收的时间.
  • 吞吐量优先(Parallel Scavenge): 侧重于吞吐量的控制.
  • 并发标记清除(CMS, Concurrent Mark Sweep): 以获取最短回收停顿时间为目标.
  • G1(Garbage First): 堆内存分成多个固定大小的块, 应用在多处理器和大容量内存环境中, 在实现高吞吐量的同时, 尽可能的满足垃圾收集暂停时间的要求.

参数基本策略

活跃数据: 应用程序稳定运行时长期存活对象在堆中占用的空间大小, 也就是Full GC后堆中老年代占用空间的大小.

总大小 3 - 4 倍活跃数据的大小
新生代 1 - 1.5 倍活跃数据的大小
老年代 2 - 3 倍活跃数据的大小
永久代 1.2 - 1.5 倍 Full GC 后的永久代空间占用

如果应用存在大量的短期对象, 应该选择较大的新生代.
如果应用存在大量的持久对象, 应该选择较大的老年代.

HotSpot JVM 手册

Java%208%20-%20GC%20cheatsheet.png

Author: Saul Lawliet

Created: 2022-03-21 Mon 15:34