第二章-Java 高级篇
1、HashMap 底层源码 难度系数:⭐⭐⭐
HashMap 的底层结构在 jdk1.7 中由数组+链表实现,在 jdk1.8 中由数组+链表+红黑树实现,以数组+链表的结构为例。


JDK1.8 之前 Put 方法:

JDK1.8 之后 Put 方法:



HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null。此外,HashMap 中的映射不是有序的。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。
补充:将链表转换成红黑树前会判断,即使阈值大于 8,但是数组长度小于 64,此时并不会将链表变为红黑树。而是选择进行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于 64 时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于 8 并且数组长度大于 64 时,链表才转换为红黑树。具体可以参考 treeifyBin 方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于 8 并且数组长度大于 64 时,链表转换为红黑树时,效率也变的更高效。
注意:可以结合百度 hashmap 源码解析进行更深入的了解。
https://blog.csdn.net/v123411739/article/details/78996181
2、 难度系数:⭐⭐


java 虚拟机主要分为以下几个区
方法区
- 有时候也成为永久代,在该区内很少发生垃圾回收,但是并不代表不发生 GC,在这里进行的 GC 主要是对方法区里的常量池和对类型的卸载
- 方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。
- 该区域是被线程共享的。
- 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
虚拟机栈
- 虚拟机栈也就是我们平常所称的栈内存,它为 java 方法服务,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
- 虚拟机栈是线程私有的,它的生命周期与线程相同。
- 局部变量表里存储的是基本数据类型、returnAddress 类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译器间确定
- 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
本地方法栈
- 本地方法栈和虚拟机栈类似,只不过本地方法栈为 Native 方法服务。
堆
- java 堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
程序计数器:
- 内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个 java 虚拟机规范没有规定任何 OOM 情况的区域。
https://openjdk.org/jeps/122 介绍 静态变量、字符串常量从永久代移动到堆中
3、 难度系数:⭐
采用分区分代回收思想:
- 复制算法 中使用的是 Minor GC,这种 GC 算法采用的是复制算法(Copying)
- 率高,缺点:需要内存容量大,比较耗内存
- 使用在占空间比较小、刷新次数多的新生区
- 标记-清除 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 效率比较低,会差生碎片。
- 标记-整理 老年代一般是由标记清除或者是标记清除与标记整理的混合实现
- 效率低速度慢,需要移动对象,但不会产生碎片。
4、 (或者 GC 对象的判定方法) 难度系数:⭐
引用计数法
- 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
- ,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
- 该算法的基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
- 在 java 中可以作为 GC Roots 的对象有以下几种:
5、 怎么排查 难度系数:⭐⭐
- 引发 StackOverFlowError 的常见原因有以下几种
- 无限递归循环调用(最常见)
- 执行了大量方法,导致线程栈空间耗尽
- 方法内声明了海量的局部变量
- native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64 位 Linux)
- 引发 OutOfMemoryError 的常见原因有以下几种
- 集合类中有对对象的引用,使用完后未清空,使得 JVM 不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 启动参数内存值设定的过小
- 栈溢出、堆溢出案例演示

public class StackOverFlowTest { private static int count = 1; public static void main(String[] args) { //模拟栈溢出 //getDieCircle();
//模拟堆溢出
getOutOfMem();
}
public static void getDieCircle(){ System.out.println(count++); getDieCircle(); }
public static void getOutOfMem(){ while (true) { Object o = new Object(); System.out.println(o); } } }

6、什么是线程池,线程池有哪些(创建) 难度系数:⭐
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率
//在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
//然后调用他们的 execute 方法即可。这 4 种线程池底层 全部是 ThreadPoolExecutor 对象的实现,
下面不重要
newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为 Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为 1 分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用 CachedThreadPool 时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
newFixedThreadPool 创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool 是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
newSingleThreadExecutor 创建一个单线程化的 Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
newScheduleThreadPool 创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟 3 秒执行。
7、 难度系数:⭐
- 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
- 主要特点:。
8、 难度系数:⭐

- 第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建 corePoolSize 个线程
- 第二步:调用 execute()提交一个任务时,如果当前的工作线程数< corePoolSize,直接创建新的线程执行这个任务
- 第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
- 第四步:如果队列已满,并且线程池中工作线程的数量< maximumPoolSize,还是会创建线程执行这个任务
- 第五步:如果队列已满,并且线程池中的线程已达到 maximumPoolSize,这个时候会执行拒绝策略,JAVA 线程池默认的策略是 AbortPolicy,即抛出 RejectedExecutionException 异常

- RUNNING(运行中):线程池处于正常运行状态,可以接受新任务并处理已提交的任务。
- SHUTDOWN(关闭中):线程池不再接受新任务,但会继续处理已提交的任务,直到任务队列为空。
shutdown()方法用于将线程池状态切换为 SHUTDOWN。 - STOP(停止中):线程池不再接受新任务,并且会尝试终止正在执行的任务。已提交但未执行的任务会从队列中移除。
shutdownNow()方法用于将线程池状态切换为 STOP。 - TIDYING(整理中):线程池在 SHUTDOWN 或 STOP 状态下,当所有任务都已经终止,工作线程数为 0 时,会将线程池状态切换为 TIDYING,表示线程池正在进行一些清理工作。
- TERMINATED(终止):线程池的终止状态,表示线程池已经完全终止,不再处理任务。线程池状态会在 TIDYING 状态结束后切换到 TERMINATED。
9、 难度系数:⭐⭐⭐⭐⭐
参数与作用:共 7 个参数
- corePoolSize:核心线程数,
- 在 ThreadPoolExecutor 中有一个与它相关的配置:allowCoreThreadTimeOut(默认为 false),当 allowCoreThreadTimeOut 为 false 时,核心线程会一直存活,哪怕是一直空闲着。而当 allowCoreThreadTimeOut 为 true 时核心线程空闲时间超过 keepAliveTime 时会被回收。
- maximumPoolSize:最大线程数
- 线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为 LinkedBlockingDeque 时,这个值将无效。
- keepAliveTime:存活时间,
- 当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受 allowCoreThreadTimeOut 影响。
- unit:keepAliveTime 的单位。
- workQueue:任务队列
- 常用有三种队列,即 SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
- threadFactory:线程工厂,
- ThreadFactory 是一个接口,用来创建 worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
- RejectedExecutionHandler:拒绝策略
- 也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用 RejectedExecutionHandler 的 rejectedExecution 法。默认是抛出一个运行时异常。
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现
CPU 密集型:N+1
IO 密集型:2N
12

12 1000

24

48

96

192

384

768

500

300

350

- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
CPU 密集型
- 主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu 的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
IO 密集型
- 主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner 测试大量运行次数求出平均值)
- AbortPolicy:直接抛出异常,默认策略;
- CallerRunsPolicy:
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
- DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
10、 难度系数:⭐
CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap
CopyOnWriteArrayList、CopyOnWriteArraySet 采用写时复制实现线程安全
ConcurrentHashMap JDK1.7 采用分段锁的方式实现线程安全
ConcurrentHashMap JDK1.8 采用 SYNC + CAS + 自旋CopyOnWriteArrayList 的 add 源码

11、Atomic 原子类了解多少 原理是什么 难度系数:⭐
Java 的原子类都存放在并发包 java.util.concurrent.atomic 下,如下图:

基本类型
● 使用原子的方式更新基本类型
● AtomicInteger:整型原子类
● AtomicLong:长整型原子类
● AtomicBoolean:布尔型原子类
数组类型
● 使用原子的方式更新数组里的某个元素
● AtomicIntegerArray:整形数组原子类
● AtomicLongArray:长整形数组原子类
● AtomicReferenceArray:引用类型数组原子类
引用类型
● AtomicReference:引用类型原子类
● AtomicStampedReference:原子更新引用类型里的字段原子类
● AtomicMarkableReference :原子更新带有标记位的引用类型
● AtomicIntegerFieldUpdater:原子更新整形字段的更新器
● AtomicLongFieldUpdater:原子更新长整形字段的更新器
● AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,以及解决使用 CAS 进行原子更新时可能出现的 ABA 问题
- AtomicInteger 类利用 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
- CAS 的原理,。UnSafe 类的 objectFieldOffset() 方法是个本地方法,这个方法是用来拿“原值”的内存地址,返回值是 valueOffset;另外,value 是一个 volatile 变量,因此 JVM 总是可以保证任意时刻的任何线程总能拿到该变量的最新值。
12、 难度系数:⭐⭐⭐
Synchronized 原理:
的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM 可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放 monitor。
的同步是利用 monitorenter 和 monitorexit 这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当 jvm 执行到 monitorenter 指令时,当前线程试图获取 monitor 对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行 monitorexit 指令时,锁计数器-1;当锁计数器为 0 时,该锁就被释放了。如果获取 monitor 对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
参考:https://blog.csdn.net/ben040661/article/details/125697819


- 无锁状态:
- 无锁状态是对象头中的锁标志位为 01 的状态,表示没有任何线程持有锁或竞争锁。
- 当一个线程尝试获取一个无锁状态的对象时,它会尝试使用 CAS(Compare-And-Swap)操作来更新对象头中的锁标志位,将其设置为 1。如果 CAS 操作成功,线程成功获取锁;否则,它将升级为偏向锁或轻量级锁。
- 偏向锁状态:
- 偏向锁状态是在无锁状态的基础上发展而来的,它通过偏向线程 ID 来表示一个线程持有锁。
- 当一个线程第一次尝试获取一个对象的锁时,如果对象是无锁状态,JVM 会尝试将对象头中的偏向线程 ID 设置为当前线程的 ID,并将锁标志位设置为 1,表示对象已经偏向于当前线程。
- 如果其他线程尝试获取同一个锁,JVM 会检查对象头中的偏向线程 ID,如果与当前线程 ID 一致,就可以获取锁,无需竞争。
- 如果有其他线程尝试获取锁,偏向锁就会升级为轻量级锁。
- 轻量级锁状态:
- 轻量级锁是一种用于解决低竞争情况下的锁协议,它采用 CAS 操作来尝试获取锁。
- 当一个线程尝试获取一个偏向锁对象时,它会使用 CAS 操作来尝试获取锁。如果 CAS 操作成功,线程获得了轻量级锁,可以进入临界区;否则,它将升级为重量级锁。
- 如果有多个线程尝试获取同一个轻量级锁,它们会使用 CAS 操作来竞争锁,避免了传统的阻塞和线程切换。
- 重量级锁状态:
- 重量级锁是传统的互斥锁,它使用操作系统的底层同步机制来确保线程安全。
- 当多个线程竞争一个重量级锁对象时,它们会被阻塞,并进入等待队列。操作系统会负责管理线程的阻塞和唤醒,以及线程的切换。
- 这种状态涉及到较高的线程开销和系统资源消耗,适用于高竞争情况下的锁协议。
Lock 原理:
- Lock 的存储结构:(用于锁的状态变更),)
- Lock 获取锁的过程:本质上是通过s,如果当场没获取到,会将该线程放在线程等待链表中。
- Lock 释放锁的过程:修改状态值,调整等待链表。
- Lock 大量使用 CAS+自旋。因此根据 CAS 特性,lock 建议使用在低锁冲突的情况下。
Lock 与 synchronized 的区别:
- Lock 的加锁和解锁都是由 java 代码配合 native 方法(调用操作系统的相关方法)实现的,而 synchronize 的加锁和解锁的过程是由 JVM 管理的
- 当一个线程使用 synchronize 获取锁时,若锁被其他线程占用着,那么当前只能被阻塞,直到成功获取锁。而 Lock 则提供超时锁和可中断等更加灵活的方式,在未能获取锁的 条件下提供一种退出的机制。
- 一个锁内部可以有多个 Condition 实例,即有多路条件队列,而 synchronize 只有一路条件队列;同样 Condition 也提供灵活的阻塞方式,在未获得通知之前可以通过中断线程以 及设置等待时限等方式退出条件队列。
- synchronize 对线程的同步仅提供独占模式,而 Lock 即可以提供独占模式,也可以提供共享模式
| synchronized | Lock |
|---|---|
| 关键字 | 类 |
| 自动加锁和释放锁 | 需要手动调用 unlock 方法释放锁 |
| jvm 层面的锁 | API 层面的锁 |
| 非公平锁 | 可以选择公平或者非公平锁 |
| 锁是一个对象,并且锁的信息保存在了对象中 | 代码中通过 int 类型的 state 标识 |
| 有一个锁升级的过程 | 无 |
13、了解 ConcurrentHashMap 吗 为什么性能比 HashTable 高,说下原理 难度系数:⭐⭐
ConcurrentHashMap 是线程安全的 Map 容器,JDK8 之前,ConcurrentHashMap 使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即 segment 类,这个类继承 ReentrantLock 来保证线程安全,JKD8 的版本取消 Segment 这个分段锁数据结构,底层也是使用 ,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。
hashtable 类基本上所有的方法都是采用 synchronized 进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap 是采用了分段锁的思想提高性能,锁粒度更细化
14、ConcurrentHashMap 底层原理 难度系数:⭐⭐⭐
。结构 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f = 目标位置元素
Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
if (tab == null || (n = tab.length) == 0)
// 数组桶为空,初始化数组桶(自旋+CAS)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 使用 synchronized 加锁加入节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// 说明是链表
if (fh >= 0) {
binCount = 1;
// 循环加入新的或者覆盖节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
// 红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}15、了解 volatile 关键字不 难度系数:⭐
项目中有没有用过?,工具类单例模式 DCL
- volatile 是 Java 提供的最轻量级的同步机制,保证了共享变量的可见性,被 volatile 关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。
- volatile 禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以 JVM 相关的优化没了,效率会偏弱
16、 难度系数:⭐⭐
- volatile 本质是告诉 JVM 当前变量在的,需要从主存中读取,synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被 。
- volatile 仅能用在变量级别,而 synchronized 可以使用在变量、方法、类级别。
- volatile 仅能实现变量的修改可见性,;而 synchronized 则可以保证变量的修改 。
- volatile 不会造成线程阻塞,synchronized 可能会造成线程阻塞。
- volatile 标记的变量不会被编译器优化,synchronized 标记的变量可以被编译器优化。
17、Java 类加载过程 难度系数:⭐
- 加载 加载时类加载的第一个过程,在这个阶段,将完成一下三件事情:
通过一个类的全限定名获取该类的二进制流。
将该二进制流中的静态存储结构转化为方法去运行时数据结构。
在内存中生成该类的 Class 对象,作为该类的数据访问入口。
- 验证 验证的目的是为了确保 Class 文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:
文件格式验证:验证字节流是否符合 Class 文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是>否正确等。
符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
- 准备
- 准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
- 解析
- 该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
- 初始化
- 初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。
18、什么是类加载器,类加载器有哪些 难度系数:⭐

类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。
主要有以下四种类加载器
- 用来加载 java 核心类库,无法被 java 程序直接引用
- :它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
- 也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它
- ,通过继承 java.lang.ClassLoader 类的方式实现
什么时候会使用到加载器?java 中的加载器是按需加载,什么时候用到,什么时候加载
● new 对象的时候
● 访问某个类或者接口的静态变量,或者对该静态变量赋值时
● 调用类的静态方法时
● 反射
● 初始化一个类的子类时,其父类首先会被加载
● JVM 启动时标明的启动类,也就是文件名和类名相同的那个类
19、 难度系数:⭐⭐
内存分配
栈区:栈分为 java 虚拟机栈和本地方法栈
堆区:,在虚拟机启动时创建,唯一目的存放对象实例。堆区是 gc 的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为 Eden 区,主要放新创建对象,From survivor 和 To survivor 保存 gc 后幸存下的对象,默认情况下各自占比 8:1:1。
方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被 Java 虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。
回收策略以及 Minor GC 和 Major GC
● 对象优先在堆的 Eden 区分配
● 大对象直接进入老年代
● 长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,.Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 GC 的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。
20、如何查看 java 死锁 难度系数:⭐
//演示死锁
package com.ssg.mst;
public class 死锁 {
private static final String lock1 = "lock1";
private static final String lock2 = "lock2";
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (true) {
synchronized (lock1) {
try {
System.out.println(Thread.currentThread().getName() + lock1);
Thread.sleep(1000);
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + lock2);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread thread2 = new Thread(() -> {
while (true) {
synchronized (lock2) {
try {
System.out.println(Thread.currentThread().getName() + lock2);
Thread.sleep(1000);
synchronized (lock1){
System.out.println(Thread.currentThread().getName() + lock1);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread1.start();
thread2.start();
}
}- 程序运行,进程没有停止。

- 通过 jps 查看 java 进程,找到没有停止的进程

- 通过 jstack 9060 查看进程具体执行信息

21、 难度系数:⭐
造成死锁的几个原因
这是造成死锁必 ,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前 3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第 4 个条件,不出现循环等待锁的关系。
- ,保证每个线程按同样的顺序进行加锁
- ,可以针对锁设置一个超时时间
- ,确保在第一时间发现死锁并进行解决
22、说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图,监控工具。
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等
23、常用的 JVM 调优的参数都有哪些?
常用的 JVM 调优参数有很多,以下是一些常见的参数:
- -Xms :设置 JVM 的初始堆大小。
- -Xmx :设置 JVM 的最大堆大小。
- -Xss :设置线程的栈大小。
- -XX:NewRatio :设置新生代和老年代的比例。
- -XX:SurvivorRatio :设置 Eden 区和 Survivor 区的比例。
- -XX:MaxPermSize (在 JDK 8 之前)或 -XX:MaxMetaspaceSize (在 JDK 8 及以后):设置永久代(或元空间)的最大大小。
- -XX:ParallelGCThreads :设置并行垃圾收集器的线程数。
- -XX:+UseConcMarkSweepGC :启用并发标记清除垃圾收集器。
- -XX:+UseG1GC :启用 G1 垃圾收集器。
- -XX:+UseSerialGC :启用串行垃圾收集器。