java进阶知识点
线程
线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运作者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等
Java的线程实现与虚拟机相关,Java1.2之后用户调度线程一对一映射到操作系统内核线程
Java5后线程状态被明确定义在java.lang.Thread.State枚举类中,对应状态图如下:
- NEW:表示线程被创建出来还没真正启动的状态,只有在该状态才能调用start
- RUNNABLE:表示该线程已经在JVM中执行,可能是正在运行,也可能还在等待系统分配给它CPU片段,在就绪队列里面排队
- BLOCKED:表示线程在等待Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态
- WAITING:表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式。join也会令线程进入等待状态
- TIMED_WAIT:其进入条件和等待状态类似,但是存在超时条件,比如wait或join等方法的指定超时版本,sleep方法,并发库中的相关工具等
- TERMINATED:表示线程终止状态,不管是正常执行结束还是意外退出
Thread无法复用(非NEW状态无法调用start),可以将要复用的逻辑放在Runnable中,利用Thread或线程池执行来实现复用
Thread.onSpinWait(),是Java9中引入的特性,利用CAS进行短期等待优化,JVM可能会利用CPU的pause指令来提高性能
线程死锁
死锁原因:
- 多个线程之间的互斥锁有嵌套循环依赖关系
检测死锁:
- jstack:通过jstack pid获取线程栈,定位相互之间的依赖关系
- ThreadMXBean:在定时线程中使用ThreadMXBean的findDeadlockedThreads定位死锁
避免死锁:
- 尽量避免多个锁之间嵌套使用
- 如多个锁是必要的,则尽量设计好锁的获取顺序
- 使用带超时的获取锁方法:
lock.tryLock() || lock.tryLock(timeout, unit)
- 通过静态代码分析(如FindBugs)提前定位可能的死锁或竞争情况
除了代码逻辑上出现的死锁还会有类加载过程发生的死锁,如第三方框架大量使用自定义类加载,针对特定情况官方文档提供了响应JVM参数和基本原则
线程安全概念
线程安全就是保证多线程环境下共享的、可修改的状态或数据的正确性
线程安全特性:
- 原子性:相关操作不会中途被其他线程干扰,一般通过同步机制实现
- 可见性:一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,就是被修改的状态能够立刻反映在内存中,通过volatile实现
- 有序性:是保证线程内串行语义,避免指令重排等
synchronized与ReentrantLock区别
- synchronized:Java内建的同步机制(Intrinsic Lock),提供了互斥的语义和可见性
- 早期比较低效,Java6后性能有较多改进,低竞争场景表现好
- 可用于修饰方法、代码块等,以对象(this)或类(xxx.class)作为同步单位
- 自动获取和释放锁,即使在同步代码块中抛出异常也同样自动释放
- 无法设置公平性,依赖操作系统线程调度
- ReentrantLock:再入锁,Lock的实现类,提供更精细的同步操作
- Java5开始提供,高竞争场景表现好
- 只适用于代码块,以线程作为同步单位,当一个线程试图获取一个它已经获取的锁时,能够自动获取成功
- 手动获取锁,不建议在try内获取锁,避免在获取锁时抛出异常导致其他已获得锁的线程无故被释放锁
- 手动释放锁,在finally中释放锁,避免在获取锁后的同步代码中抛出异常导致死锁
- 可以设置公平性,设置后会倾向于将锁赋予等待时间最久的线程,减少线程饥渴,但会引入额外开销导致一定的吞吐量下降,建议只有当程序确实有公平性需要时来指定
- 具备尝试非阻塞地获取锁,且可选超时
- 可以判断是否有线程或某个特定线程,在排队等待获取锁
- 可以响应中断请求,获取到锁的线程能够响应中断
- 提供条件变量(Condition)来控制线程,通过它的signal/await方法实现线程唤醒与等待
并发工具类
同步锁:
- ReentrantLock:再入锁,Lock的实现类,提供更精细的同步操作
- ReentrantReadWriteLock:分离读锁和写锁,当写锁被某个线程持有时,读锁将等待直到写锁被释放,适合读多写少的场景,ReadWriteLock接口的实现
- StampedLock:在提供读写锁的同时,支持优化读模式(先尝试读,然后通过validate校验读数据时是否在写模式,在写模式则尝试获取读锁,不在写模式则避免了开销),不支持再入性,也就是不以持有锁的线程为单位
辅助类:
- CountDownLatch:线程调用await阻塞,直到其他线程调用countDown足够的次数,才会释放堵塞,不可重置计数器
- CyclicBarrier:多个线程调用await阻塞,直到堵塞线程数量到达屏障点,才会释放堵塞,并自动重置计数器
- Semaphore:计数信号量,线程通过acquire/release获取或释放信号量许可,当信号量中有可用的许可时,线程能获取该许可
- Phaser:功能与CountDownLatch接近,但允许线程动态地注册到Phaser上,实现多个线程类似步骤、阶段场景的协调,线程注册等待屏障条件触发,进而协调彼此间行动
同步容器:
- List
- CopyOnWriteArrayList:ArrayList的CopyOnWrite线程安全实现,读操作不加锁,修改操作加锁后先拷贝原数组,修改后替换原来的数组,适合读多写少场景
- Map
- ConcurrentHashMap:基于分段锁的线程安全HashMap,不保证有序,添加获取速度快
- ConcurrentSkipListMap:基于SkipList结构,支持自然顺序,增删元素速度快
- Set
- CopyOnWriteArraySet:通过包装CopyOnWriteArrayList为Set容器实现
- ConcurrentSkipListSet:通过包装ConcurrentSkipListMap为Set容器实现
- Queue
- ConcurrentLinkedQeque:基于lock-free的线程安全单向链表队列,遍历一致性低于基于锁的队列
- ConcurrentLinkedDeque:ConcurrentLinkedQeque的双向链表队列实现
- ArrayBlockingQueue:基于锁和数组的线程安全有界队列,初始化时要指定容量
- LinkedBlockingQueue:基于锁和单向链表的线程安全有界队列,若初始化时不指定容量则默认设为Integer.MAX_VALUE,相当于无界队列
- SynchronousQueue:每个插入操作都要等待一个移除操作的线程安全队列(反之亦然),看上去容量为1,实际上内部容量为0
- PriorityBlockingQueue:支持优先级排列的线程安全无界队列
- DelayedQueue:用来处理延时任务的线程安全无界队列
- LinkedTransferQueue:结合LinkedBlockingQueue与SynchronousQueue两者优点的线程安全无界队列,当有消费者等待数据时,生产者通过transfer将数据直接交给消费者,减少了锁的获取与释放
类加载过程
- 加载(Loading):将字节码数据从不同数据源(jar包、class文件、网络数据源)读取到JVM中,并映射为JVM认可的数据结构(Class对象)
- 链接(Linking):把原始的类定义信息平滑地转化入JVM运行的过程中
- 验证(Verification):JVM核验字节信息是否符合Java虚拟机规范
- 准备(Preparation):创建类或接口中的静态变量,分配静态变量所需内存空间并设置初始值(原始类型常量会直接赋原值)
- 解析(Resolution):将常量池中的符号引用(symbolic reference)替换为直接引用
- 初始化(Initialization):执行类初始化代码逻辑,包括静态字段赋值(使用原值覆盖准备阶段的初始值),类静态初始化块逻辑,父类初始化优先于子类
双亲委派模型:当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载Java类型
动态加载类方法:
- findClass:表示根据类名查找类对象
- loadClass:表示根据类名进行双亲委托模型进行类加载并返回类对象
- defineClass:表示根据类的字节码转换为类对象,字节码可以是byte数组的形式
JVM内存区域
- 程序计数器(PC,Program Counter Register):每个线程有独立的程序计数器,用于存储当前线程正在执行的Java方法的JVM指令地址;如果执行native方法,则是未指定值(undefined)
- Java虚拟机栈(Java Virtual Machine Stack):每个线程有独立的虚拟机栈,其内部保存一个个栈帧(Stack Frame),每次调用Java方法时都会创建栈帧后入栈,方法执行完毕则出栈,栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等
- 堆(Heap):所有线程共享,用来放置Java对象实例,堆内空间会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分
- 方法区(Method Area):所有线程共享,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等,方法区是JVM定义的规范,早期方法区的实现是永久代(Permanent Generation),Java8后使用元数据区(Metaspace)实现
- 运行时常量池(Run-Time Constant Pool):方法区的一部分,可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛
- 本地方法栈(Native Method Stack):每个线程有独立的本地方法栈,与Java虚拟机栈非常相似,支持对本地方法的调用
不在JVM规范内:
- 直接内存区(Direct Memory):Direct Buffer所直接分配的内存
- Code Cache等其他内存:JIT Compiler在运行时对热点方法进行编译后生成的方法存储在Code Cache内;GC等功能需要运行在本地线程之中,类似部分都需要占用内存空间
堆内存划分
堆内存结构:
- 新生代:大部分对象创建和销毁的区域,内部分为Eden区和两个Survivor区
- Eden区:对象初始分配的区域,内部分为多个TLAB区
- Survivor区:放置从Minor GC中保留下来的对象,也叫from、to区域
- 老年代:放置长生命周期的对象,通常是从Survivor区域拷贝过来的对象,如果对象太大,新生代没有对应的连续空间则直接分配到老年代
- 永久代:早期Java方法区实现,储存Java类元数据、常量池、Intern字符串缓存,Java8之后就不存在永久代了
- Virtual:堆内各区域暂未扩展到其上限时的暂不可用(uncommitted)空间
TLAB(Thread Local Allocation Buffer)内存结构:JVM为每个线程分配的一个私有缓存区域,start、end是起始地址,top(指针)表示已分配大小,当top与end相遇表示该缓存已满,JVM会视图再从Eden里分配一块
G1 GC内存结构:
垃圾收集器
- Serial GC:单线程运行,兼顾新生代与老年代,新生代使用复制算法,老年代使用标记-整理算法,Client默认选择
- ParNew GC:Serial GC的多线程版本,新生代GC实现,使用复制算法,常用于配合老年代的CMS GC使用
- CMS GC:设计目的是尽量减少停顿时间,老年代GC实现,使用标记-清除算法,适合Web应用,Java9中被标记为废弃
- Parallel GC:吞吐量优先的多线程GC,兼顾新生代与老年代,新生代使用复制算法,老年代使用标记-整理算法,Java8早期Server默认选择
- G1 GC:兼顾吞吐量和停顿时间的多线程GC,兼顾新生代与老年代,内存结构使用region划分,region之间是复制算法,实际整体上可看做标记-整理算法,Java9以后Server默认选择
垃圾回收算法
垃圾回收算法基本思路可分为三种,每种都有相应的优缺点
- 复制(Copying)算法:新生代GC基本都基于复制算法,将Minor GC后存活的对象顺序复制到to区域,避免内存碎片化,缺点是内存占用大
- 标记-清除(Mark-Sweep)算法:首先标记出所有要回收的对象,然后进行清除,缺点是效率低和有内存碎片化问题
- 标记-整理(Mark-Compact)算法:首先标记出所有要回收的对象,然后在清理过程中将存活对象移动到连续的内存空间中,缺点是效率低
Java使用分代收集算法来针对不同场景使用最适当的算法
通常将新生代中GC叫做Minor GC,老年代中GC叫做Major GC,对整个堆进行GC叫做Full GC
G1 GC使用了新的GC方式,新生代中仍旧存在Minor GC,但会涉及处理Remembered Set(用于记录和维护region之间对象的引用关系),而老年代大部分情况下进行并发标记,并发标记周期结束后Minor GC会切换为Mixed GC同时清理新生代与老年代GC,回收足够数量的老年代区域后Mined GC会切换回Minor GC,直到下一个并发标记周期结束
JMM
JMM(Java Memory Model):Java虚拟机中定义的Java内存模型规范,通过该规范屏蔽了不同硬件平台和操作系统的内存访问差异,实现各种平台都能达到一致的内存访问效果
JMM规定所有的变量都在主存中,每个线程有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能直接访问其他线程的工作内存
JMM通常依赖于所谓的内存屏障来解决可见性等问题,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种happen-before规则
happen-before:是Java内存模型中保证多线程操作可见性的机制,也是对早期语言规范中含糊的可见性概念的一个精确定义
- 程序次序规则:一个线程内执行的每个操作,保证先行发生于后面的操作
- 锁定规则:对一个锁的解锁操作,保证先行发生于后面对该锁的加锁操作
- volatile变量规则:对该变量的写操作,保证先行发生于后面对该变量的读操作
- 线程启动规则:Thread对象的start方法,保证先行发生于此线程的每一个动作
- 线程中断规则:线程interrupt方法的调用,保证先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join方法结束、Thread.isAlive的返回值手段检测到线程已经终止执行
- 对象终结规则:对象的初始化,保证先行发生于它finalize方法的开始
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则操作A先行发生于操作C也成立