创建线程的方式

  • 继承Thread
  • 实现Runnable接口
  • 实现Callable<T>接口, 泛型类型与重写的call()方法返回值类型相同
  • 线程池创建线程

RunnableCallable不同:

  • Runnablerun方法没有返回值,Callablecall方法有返回值,搭配FutureTask可以获取异步执行的结果
  • Callablecall方法可以抛出异常,Runnablerun方法不能向上抛异常

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 引入了轻量级锁的概念。

加锁过程

  1. 在线程栈中创建一个 Lock Record,将其 obj 字段指向锁对象。
  2. 通过 CAS 指令将 Lock Record 的地址存储在对象头的 Mark Word 中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
  3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置 Lock Record 第一部分为 null,起到了一个重入计数器的作用。
  4. 如果 CAS 修改失败,说明发生了竞争,需要膨胀为重量级锁。

解锁过程

  1. 遍历线程栈,找到所有 obj 字段等于当前锁对象的 Lock Record。
  2. 如果 Lock Record 的 Mark Word 为 null,代表这是一次重入,将 obj 设置为 null 后 continue。
  3. 如果 Lock Record 的 Mark Word 不为 null,则利用 CAS 指令将对象头的 mark word 恢复成为无锁状态。如果失败则膨胀为重量级锁。

偏向锁1

偏向锁:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 中,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

加锁过程

  1. 查看 Mark Word 中偏向锁的标识以及锁标志位,若是否偏向锁为 1 且锁标志位为 01,则该锁为可偏向状态。
  2. 若为可偏向状态,则测试 Mark Word 中的线程 ID 是否与当前线程相同,若相同,则直接执行同步代码,否则进入下一步。
  3. 当前线程通过 CAS 操作竞争锁,若竞争成功,则将 Mark Word 中线程 ID 设置为当前线程 ID,然后执行同步代码,若竞争失败,进入下一步。
  4. 当前线程通过 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 不会被修改

类加载

见:JavaGuide 类加载过程详解

垃圾回收

引用计数法:每个对象都有一个引用计数器,当对象被引用时,计数器加一,当引用失效时,计数器减一,当计数器为 0 时,对象被回收。循环引用无法解决。

可达性分析:从 GC Roots 出发,向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。

GC Roots 包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 Native 方法引用的对象

垃圾回收算法

标记清除方法

  1. 根据可达性分析算法得出的垃圾进行标记
  2. 对这些标记为可回收的内容进行垃圾回收

优点:标记和清除速度较快

缺点:碎片化较为严重,内存不连贯

标记整理算法

先标记,将存活对象都向内存的另一端移动,然后清理边界外的垃圾。

优点:无内存碎片

缺点:需要移动对象,效率低

老年代使用

复制算法

复制一块相同的区域,在一个区域进行标记后,将存货的对象复制到另一块区域,然后原区域全部清空。

优点:垃圾对象较多时效率较高,清理后内存无碎片

缺点:分配两块内存空间,同一时刻只能用一块,内存使用率较低

新生代使用

分代回收

在堆中:新生代 : 老年代 = 1 : 2

在新生代中:Eden : from : to = 8 : 1 : 1

工作机制:

  1. 新建对象分配到 Eden 区
  2. Eden 内存不足时,标记 Eden 和 from 的存活对象,使用复制算法复制到 to 中,然后清空 Eden 和 from 区
  3. 一段时间后,Eden 区内存再次不足时,标记 Eden 和 to 中存货对象复制到 from 中,清空 Eden 和 to 区
  4. 在幸存区(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

  1. 新生代回收

  2. 并发标记

  3. 混合收集

    复制完成后进入下一轮回收

引用类型

强引用:只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收

软引用:第一次垃圾回收后内存依旧不足,触发第二次垃圾回收时会被回收

弱引用:垃圾回收时无论是否内存充足都会就被回收

虚引用:必须配合引用队列使用,被引用对象回收时会将虚引用入队,由 Reference Handler 线程调用相关方法释放直接内存