多线程

  1. 进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
  2. 线程:线程与进程相似,但线程是一个比进程更小的执行单位。一个 Java 程序的运行是 main 线程和多个其他线程同时运行
  3. 线程的组成:程序计数器,虚拟机栈,本地方法栈
  4. 进程与线程的区别:线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
  5. 堆和方法区:堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  6. 线程的生命周期:
    1. NEW: 新创建
    2. RUNNABLE: 运行状态,运行时又分为两种情况,(准备)READY和(运行中)RUNNING
    3. BLOCKED:阻塞
    4. WAITING:等待
    5. TIME_WAITING:超时
    6. TERMINATED:终止
      Java线程生命周期
  7. 上下文切换: 线程执行过程中会有自己的运行条件和状态(上下文),当出现一些情况时,就会让出CPU,从占用状态退出,进行线程的切换。线程切换意味着需要保存当前上下文状态,为了以后恢复到原状态,并且加载CPU占用线程上下文,这个过程就是上下文切换。
  8. 线程死锁: 多个线程被阻塞,并都在等待某个锁释放,从而出现无限等待的情况
  9. 产生死锁的原因:
    1. 互斥条件: 一个资源只有一个线程持有
    2. 请求与保持条件: 获取某个资源的锁之后,又进行了锁竞争,而且没有释放已有资源的锁
    3. 不剥夺条件: 自己获得的资源,只有自己用,自己释放,未使用完之前不释放
    4. 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源的关系
  10. 预防&避免死锁:
    1. 申请全部资源
    2. 在多重锁竞争时,及时释放已拥有的锁
    3. 破坏循环等待条件
  11. Sleep()Wait()
    1. sleep()方法是作用于当前线程的
    2. wait()方法是作用于对象锁的线程,每个对象都拥有
    3. 调用wait()方法,那么会让出当前对象锁线程,进入等待状态,要恢复需要别的线程调用同一个对象的notify()方法
  12. Thread的run()方法和start()方法
    1. start() 方法是用于启动线程,并让线程进入就绪状态
    2. run() 方法是在start()方法启动之后执行,如果单纯只执行run()方法,那么将毫无意义,只是作为一个普通方法
  13. 乐观锁和悲观锁
    1. 悲观锁: 对一切更新保持悲观,每次都会加锁,直到更新结束才释放当前锁,把资源转移给其它线程
    2. 乐观锁: 对一起更新保持乐观,共享资源被访问不会出现问题,不加锁,只有在提交更新时去检测资源更新版本号是否有被更新到
    3. 应用场景:
      1. 悲观锁一般用在写操作比较多的场景中,避免频繁失败和重试。比如ReentrantLocksynchronized等独占锁
      2. 乐观锁一般用在写操作比较少的场景中,避免频繁加锁带来的性能损耗,提升系统吞吐量。CAS算法和版本号机制实现的一般都是乐观锁
  14. CAS算法(Compare And Swap): 经典乐观锁实现算法。原理就是,在更新之前,把预期值与要更新的值进行比较,如果相同,那么就可以更新,如果不同就驳回更新操作,之后可以重试或者中断。
    1. CAS算法包含三个关键值: E(预期值),V(要更新的值),N(新值)。所以,如果E=V就表示可以更新N
    2. 例子: 更新一个i值,V原值为1,N值为6,在更新前就会对比,E的预期值也应该是1,那么结果如下
      1. i为1,那么就正常更新,将i更新为6
      2. i不为1,被其它线程修改过,放弃更新
    3. CAS 相关的实现是通过 C++ 内联汇编的形式实现的
  15. 乐观锁ABA问题
    1. 问题所在: 单纯判断预期值与更新值相同是片面的,因为,更新值可以先变化为另一个,然后在变回去原值,这发生了变化,但是CAS算法并不能判断,就出现了ABA问题
    2. 解决: 可以除了判断预期值与更新值,再增加时间戳或者版本作为额外判断,那么就可以保证是同一时刻/版本的值,就不会有问题
  16. volatile关键字: 带有该关键字的变量,说明它为共享且不稳定的,每次使用需要到内存中获取。但它只能保证可见性,不能保证原子性。在禁止指令重排时,也需要加入这个关键字
  17. synchronized: 用于多线程下资源的同步,保证资源的线程安全
    1. 可以修改静态方法,实例方法,代码块
    2. JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销
  18. ReentrantLock: 并非底层而是通过API实现,可以通过调用lock()unlock()来进行加锁,释放锁。
  19. ReentrantLock对比synchronized来说,在功能上更加完善,可以中断当前线程,也可以有选择性的进行线程通知。
  20. ThreadLocal: 存放每个线程自己变量的空间
  21. AQS: 抽象队列同步器。原理就是对公共资源进行访问时,如果资源未锁定,那么获取资源的使用权限,并锁定。如果已锁定,那就需要有个能给阻塞等待以及唤醒是进行锁分配的机制,现在实现这一机制的方法是通过一个虚拟双向队列,称为CLH队列(FIFO)。该队列每个节点(Node)代表一个阻塞的线程,其中每个节点还包含该线程的状态,以及上下流的节点
  22. 线程池ThreadPoolExecutor
    1. 核心配置:
      1. corePoolSize: 核心线程数。io密集型为2n,cpu密集型为n+1
      2. maximumPoolSize: 最大线程数
      3. workQueue: 队列
    2. 拒绝策略:
      1. AbortPolicy: 抛出RejectedExecutionException来拒绝新任务的处理,默认为这个策略
      2. CallerRunsPolicy: 调用执行自己的线程运行任务
      3. DiscardPolicy: 不处理新任务,直接丢弃掉
      4. DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求
  23. 线程池的核心配置参数:
    1. corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量
    2. maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数
    3. workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中
  24. 动态线程池配置方案: 《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》
  25. 线程池的状态:
    • Running: 运行状态,能够接受处理任务
    • ShutDown: 停止状态,不能接受但能处理已接受任务
    • Stop: 停止状态,不能接受同时中断正在处理的任务
    • Tidying: 所有任务已终止,有效线程数为0,进入该状态之后会调用terminate()方法
    • Terminated: 在执行完terminate()方法之后进入该状态。
  26. ThreadLocal: 每个线程自己的数据集装箱,私有数据
  27. jdk的线程池和tomcat线程池的区别
    • jdk: 先使用核心线程数配置,接着使用队列长度,最后再使用最大线程配置
    • tomcat: 先使用核心线程数配置,再使用最大线程配置,最后才使用队列长度

JVM

  1. 运行时JVM分区:
    1. 线程私有
      1. 程序计数器: 线程指示器,负责线程的执行顺序,流程控制(如:顺序执行,选择,循环,异常处理),在线程恢复之后,能给准确找到线程之前的执行位置。它随着线程创建而创建,随着线程回收而销毁,不会发生OOM
      2. 本地方法栈: 和Java虚拟机栈成互补关系,为虚拟机使用到Native方法而存在。其构成和生命周期与Java虚拟机类似。
      3. Java虚拟机栈(简称栈): 生命周期和线程相同。方法的调用都需要通过栈。
      • 栈是由一个个的栈帧组成,每个栈帧的构成如下:
        1. 局部变量表: 存放一些编译期可知的各种数据类型和引用类型
        2. 操作数栈: 存放方法的计算中间计算结果,作为方法的中转站
        3. 动态链接: 编译为字节码文件时,所有的变量和方法引用都会转化为符号引用保存在Class文件的常量池中。动态链接就是为了将符号引用转换为调用方法的直接引用。
        4. 方法返回地址: 方法的返回值
      • 栈帧是由方法调用而创建,调用结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
      • 运行栈会出现的两种错误:
        1. StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误
        2. OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
    2. 共有
      1. 堆: 存放对象实例,所有的对象实例以及数据都在这里分配内存。在JDK1.8之后的内存分区:
        1. 新生代: 最初创建的对象都在新生代中,经过GC多次回收而存留下来的,就会进入老年代。判断一个对象的是否进入的标准是根据算法计算动态年龄,比这个年龄大就进入老年代
        2. 老年代: 多次GC之后遗留下来的对象,并且根据计算动态年龄,大于这个年龄的就会进入老年代
        3. 元空间: 使用直接内存(计算机内存)
        4. 字符串常量池: 为了减少内存消耗和提升性能而开辟的空间,主要是为了避免字符串重复创建。它是由c++实现,本质就是一共HashSet数据结构的集合,存放字符串对象的引用,并指向堆中的字符串对象
      2. 方法区: 只有规定,实现是根据各个虚拟机自己的标准。在调用类时,会将其解析,并存储加载的类信息到方法区,其中类信息比如:属性,类信息,方法,静态变量,常量,即时编译器编译后的代码缓存等数据。方法区是堆中元空间的抽象
        1. 元空间使用的是直接内存,由计算机内存大小控制上限,理论上不会超内存。可以使用-XX:MaxMetaspaceSize来固定大小上限
        2. 元空间存放的元数据,不由配置项MaxPermSize控制,由系统实际空间控制,能给加载更多的类
        3. JDK8,不需要额外设置永久代,转为元空间
        4. 运行时常量池: 存放编译期生成的各种字面量和符号引用。会在类加载之后存放于方法区的运行时常量池中,内存收到方法区内存大小限制。
      3. 直接内存: 计算机的内存
  2. HotSpot虚拟机
    1. 创建对象的步骤
      1. 类加载检查: 检查常量池,确认是否已经加载过、解析和初始化,没有就继续执行后续步骤
      2. 分配内存: 分配的方式如下
        1. 空闲列表: 内存规整的情况下,只需要将指针移动对象的内存大小位置,就完成了分配。这种情况下,使用过的内存和未使用的内存都是泾渭分明的,所以,这种GC方法都是堆内存规整的。
        2. 指针碰撞: 通过列表记录内存的使用情况,把空闲的内存块分配给新的对象,更新记录就完成了。这种情况下,内存是不规整的。
      3. 初始化零值
      4. 设置对象头: 当初始化完成时,需要对其进行一些设置,例如这个对象的类信息,对象哈希码,对象的GC分代年龄等信息,并将这下信息存放在对象头中
      5. 执行init方法: 上述步骤执行完,就已经在内存中存在一个新的对象了,这时候就需要根据程序员的意愿进行对象的初始化操作了,这样,一个真正的对象才算创建
    2. 内存分配的线程安全问题: 对象创建是很频繁的,那么创建是也需要注意线程安全问题,有如下两个解决方案:
      • CAS+失败重试: CAS就是一种乐观锁的实现方式,然后配合失败重试
      • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配
  3. HotSpot虚拟机对象的内存布局
    1. 对象头: 包含两个部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,用于得知对象是哪个类的实例
    2. 实例数据: 对象存储的有效信息,各类字段的内容
    3. 对其填充: 不是必然存在,没有特别含义,起到占位作用
  4. HotSpot虚拟机对象的访问定位: 创建对象就是为了使用对象,那么如何进行访问?有如下两种方式
    1. 句柄: 堆中会开辟一块句柄空间存放这个信息,栈中引用存放的句柄的地址,句柄存放对象实例数据指针与对象类型数据指针
      Java线程生命周期
    2. 指针: 不访问句柄,那么引用中存储的就是对象的地址,直接访问指针获取对应对象及其类信息

两种方式各有优劣,句柄稳定,指针快


参考原文链接:https://javaguide.cn/java/concurrent/java-concurrent-questions-01.html