Java 进阶知识点

java进阶知识点

线程

线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运作者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等
Java的线程实现与虚拟机相关,Java1.2之后用户调度线程一对一映射到操作系统内核线程

Java5后线程状态被明确定义在java.lang.Thread.State枚举类中,对应状态图如下:

Java 进阶知识点_第1张图片

  • 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内存区域

Java 进阶知识点_第2张图片

  • 程序计数器(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等功能需要运行在本地线程之中,类似部分都需要占用内存空间

堆内存划分

堆内存结构:

Java 进阶知识点_第3张图片

  • 新生代:大部分对象创建和销毁的区域,内部分为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里分配一块

Java 进阶知识点_第4张图片

G1 GC内存结构:

Java 进阶知识点_第5张图片

垃圾收集器

  • 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也成立

参考

Java核心技术面试精讲

你可能感兴趣的:(Java,面试相关)