Java 基础八股文

目录

基础

普通类和抽象类有哪些区别?

接口和抽象类有什么区别?

BIO、NIO、AIO 有什么区别?

什么是反射?

什么是 java 序列化?什么情况下需要序列化?

final、finally、finalize 有什么区别?

在 Java 中,为什么不允许从静态方法中访问非静态变量?

什么情况下会更倾向于使用抽象类而不是接口?

高级

集合

基础概念

说说有哪些常见集合?

底层原理

ArrayList的扩容机制?

HashMap的数据结构

你对红黑树了解多少?为什么不用二叉树/平衡树呢?

HashMap的put流程

HashMap怎么查找元素的呢?

区别

ArrayList和LinkedList有什么区别?

并发中如何处理

能具体说一下ConcurrentHashmap的实现吗?

Java中代理有几种实现方式

Java新特性

解决hash冲突的方式有哪些?

单例模式有哪些实现方式,有什么优缺点

并发

线程的创建方式

为什么要使用线程池

线程的状态转换有什么(生命周期)

线程的sleep、wait、join、yield如何使用

sleep和wait区别   

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

volatile 关键字的作用

在Java中,最常用的锁

多线程中 synchronized 锁升级的原理是什么?

synchronized 和 ReentrantLock 区别是什么?

如何预防死锁?

死锁发生的四个必要条件:

死锁预防,那么就是需要破坏这四个必要条件

什么是守护线程

ThreadLocal 

概念

日常使用

实际开发中的用法

使用时需要注意的问题

参考:

Condition 类和Object 类锁方法区别

如何避免ABA问题:

JVM

JVM内存分哪几个区,每个区的作用是什么

什么是类加载器,类加载器有哪些?

说一下类加载的执行过程?

JVM的类加载机制是什么?

什么是双亲委派模型?

怎么判断对象是否可以被回收?

 jvm 有哪些垃圾回收算法?

JVM栈堆概念,何时销毁对象

什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查    

引发 StackOverFlowError 的常见原因有以下几种

引发 OutOfMemoryError的常见原因有以下几种


基础

普通类和抽象类有哪些区别?


抽象类不能被实例化但抽象类有构造函数可以供子类使用;
抽象类不一定有抽象方法,有抽象方法的类一定是抽象类;
抽象类的子类必须实现抽象类中的所有抽象方法,否则子类仍然是抽象类;
抽象方法不能声明为静态、不能被static、final修饰。

接口和抽象类有什么区别?


(1)接口

接口使用interface修饰;
接口不能实例化;
类可以实现多个接口;

①java8之前,接口中的方法都是抽象方法,省略了public abstract。②java8之后;接口中可以定义静态方法,静态方法必须有方法体,普通方法没有方法体,需要被实现;

(2)抽象类

抽象类使用abstract修饰;
抽象类不能被实例化;
抽象类只能单继承;
抽象类中可以包含抽象方法和非抽象方法,非抽象方法需要有方法体;
如果一个类继承了抽象类,①如果实现了所有的抽象方法,子类可以不是抽象类;②如果没有实现所有的抽象方法,子类仍然是抽象类。

区别:

1、抽象类是半抽象的,有构造方法、静态代码块,抽象类可以包含抽象方法和非抽象方法,也可以有成员变量(不仅仅是常量);接口是完全抽象的,没有构造方法,在Java 8及之后,接口可以包含默认方法和静态方法,默认方法因为不是abstract的,所以可重写,也可以不重写。

2、类和类之间只能单继承,一个抽象类只能继承一个类(单继承);接口和接口之间支持多继承

3、当一个类,既继承了一个父类,又实现多个接口,父类中的成员方法与接口中的默认方法重名,子类就近选择执行父类的成员方法;当一个类实现多个接口,其中接口的默认方法重名需要重写这个方法

BIO、NIO、AIO 有什么区别?


(1)同步阻塞BIO

一个连接一个线程。

JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。

(2)同步非阻塞NIO

一个请求一个线程。

NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。

JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。

(3)异步非阻塞AIO

一个有效请求一个线程。

JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。

参考:一文搞懂NIO、AIO、BIO的核心区别(建议收藏)_ITPUB博客

BIO,NIO,AIO区别_bio,nio,aio的区别-CSDN博客

什么是反射?


所谓反射,是java在运行时进行自我观察的能力,通过class、constructor、field、method四个方法获取一个类的各个组成部分。

在Java运行时环境中,对任意一个类,可以知道类有哪些属性和方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于反射机制。

什么是 java 序列化?什么情况下需要序列化?

序列化就是一种用来处理对象流的机制。将对象的内容流化,将流化后的对象传输于网络之间。

序列化是通过实现serializable接口,该接口没有需要实现的方法,implement Serializable只是为了标注该对象是可被序列化的,使用一个输出流(FileOutputStream)来构造一个ObjectOutputStream对象,接着使用ObjectOutputStream对象的writeObejct(Object object)方法就可以将参数的obj对象到磁盘,需要恢复的时候使用输入流。

序列化是将对象转换为容易传输的格式的过程。

例如,可以序列化一个对象,然后通过HTTP通过Internet在客户端和服务器之间传输该对象。在另一端,反序列化将从流中心构造成对象。

一般程序在运行时,产生对象,这些对象随着程序的停止而消失,但我们想将某些对象保存下来,这时,我们就可以通过序列化将对象保存在磁盘,需要使用的时候通过反序列化获取到。

对象序列化的最主要目的就是传递和保存对象,保存对象的完整性和可传递性。

譬如通过网络传输或者把一个对象保存成本地一个文件的时候,需要使用序列化。

final、finally、finalize 有什么区别?


final可以修饰类,变量,方法,修饰的类不能被继承,修饰的变量不能重新赋值,修饰的方法不能被重写

finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。

finalize方法用于垃圾回收。

一般情况下不需要我们实现finalize,当对象被回收的时候需要释放一些资源,比如socket链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize方法,关闭这个链接。

但是当调用finalize方法后,并不意味着gc会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题。所以,不推荐使用finalize方法。

在 Java 中,为什么不允许从静态方法中访问非静态变量?


静态变量属于类本身,在类加载的时候就会分配内存,可以通过类名直接访问;
非静态变量属于类的对象,只有在类的对象产生时,才会分配内存,通过类的实例去访问;
静态方法也属于类本身,但是此时没有类的实例,内存中没有非静态变量,所以无法调用。

什么情况下会更倾向于使用抽象类而不是接口?

  1.  

    类型与子类之间存在 is-a 关系‌:当抽象类和子类之间存在一种基本类型与具体实现的关系时,使用抽象类更为合适。例如,如果有一个Animal抽象类,而CatDog是Animal的子类,表示具体的动物类型,这种情况下使用抽象类能够清晰地表达这种is-a关系‌。

  2.  

    需要为子类提供默认实现‌:如果需要为子类提供一些方法的默认实现,而不是强制所有实现接口的类都必须实现这些方法,那么使用抽象类更为合适。抽象类可以包含部分实现的方法,而接口则要求所有实现它的类都必须提供方法的实现‌。

  3.  

    需要访问非 public 的方法或变量‌:由于抽象类是类,因此可以包含成员的变量和方法,而接口只能定义public的方法。如果需要访问非public的方法或变量,使用抽象类比使用接口更加方便。抽象类还可以将一些公共逻辑和数据放在一起,简化编程过程‌。

  4.  

    性能考虑‌:在一些对时间要求比较高的应用中,抽象类可能会比接口稍快一点。这可能是因为接口需要动态调度,而抽象类则可能提供一定的性能优势‌。

  5.  

    编码和组织‌:如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码和组织,那么抽象类是一个更好的选择。接口和抽象类可以一起使用,其中接口中定义函数,而在抽象类中定义默认的实现‌。

高级

集合

基础概念

说说有哪些常见集合?

其中 Collection 是集合 ListSet 的父接口,它主要有两个子接口:

  • List:存储的元素有序,可重复。
  • Set:存储的元素无序,不可重复。

Map是另外的接口,是键值对映射结构的集合。

底层原理

ArrayList的扩容机制?


ArrayList是基于数组的集合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1超过数组长度,就会进行扩容。

ArrayList的扩容是创建一个1.5倍的新数组,然后把原数组的值拷贝过去。

HashMap的数据结构

JDK1.8的数据结构是**数组+链表+红黑树

通过计算的Hash值找到数组上的位置,如果出现Hash相同的情况,依然会出现Hash碰撞,如果数组位置上没有元素,则直接将这个元素放到数组位置上,如果有元素则会向下延伸出链表,不过新的元素会放到链表的尾部,即尾插法。当链表的长度超过8并且数组长度大于64时,为了避免查找搜索性能下降,该链表会转换成一个红黑树。

HashMap的数据结构_hashmap数据结构-CSDN博客

你对红黑树了解多少?为什么不用二叉树/平衡树呢?


红黑树本质上是一种二叉查找树,为了保持平衡,它又在二叉查找树的基础上增加了一些规则:

1、每个节点要么是红色,要么是黑色;
2、根节点永远是黑色的;
3、所有的叶子节点都是是黑色的(注意这里说叶子节点其实是图中的NULL节点);
4、每个红色节点的两个子节点一定都是黑色;
5、从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

之所以不用二叉树:

红黑树是一种平衡的二叉树,插入、删除、查找的最坏时间复杂度都为O(logn),避免了二叉树最坏情况下的O(n)时间复杂度。

之所以不用平衡二叉树:

 平衡二叉树是比红黑树更严格的平衡树,为了保持保持平衡,需要旋转的次数更多,也就是说平衡二叉树保持平衡的效率更低,所以平衡二叉树插入和删除的效率比红黑树要低。

HashMap的put流程

首先进行哈希值的扰动,获取一个新的哈希值。(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

判断tab是否位空或者长度为0,如果是则进行扩容操作.

if((tab = table) == null || (n = tab.length) == 0)
    n =  (tab = resize()).length;


根据哈希值计算下标,如果对应下标正好没有存放数据,则直接插入即可否则需要覆盖。tab[i=(n-1) & hash])

判断tab[i]是否为树节点,否 则向链表中插入数据,是 则向树中插入节点.

如果链表中插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树。treeifyBin(tab, hash);

最后所有元素处理完成后,判断是否超过阈值;threshold,超过则扩容。

HashMap怎么查找元素的呢?

区别

ArrayList和LinkedList有什么区别?

(1)数据结构不同

  • ArrayList基于数组实现
  • LinkedList基于双向链表实现

(2)多数情况下,ArrayList更利于查找,LinkedList更利于增删

(3)是否支持随机访问

ArrayList基于数组,所以它可以根据下标查找,支持随机访问,当然,它也实现了RandmoAccess接口,这个接口只是用来标识是否支持随机访问。
LinkedList基于链表,所以它没法根据序号直接获取元素,它没有实现RandmoAccess接口,标记不支持随机访问。

(4)内存占用,ArrayList基于数组,是一块连续的内存空间,LinkedList基于链表,内存空间不连续,它们在空间占用上都有一些额外的消耗

并发中如何处理

能具体说一下ConcurrentHashmap的实现吗?

ConcurrentHashmap线程安全在jdk1.7版本是基于分段锁实现,在jdk1.8是基于CAs+synchronized实现。

CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。

CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。

jdk1.8实现线程安全不是在数据结构上下功夫,它的数据结构和HashMap是一样的,数组+链表+红黑树。它实现线程安全的关键点在于put流程。 

put流程

首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化
如果当前数组位置是空则直接通过CAS自旋写入数据
如果hash==MOVED,说明需要扩容,执行扩容
如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树
get查询

get很简单,和HashMap基本相同,通过key计算位置,table该位置key相同就返回,如果是红黑树按照红黑树获取,否则就遍历链表获取。

参考:

【Java集合框架面试题(30道)】_java集合面试题-CSDN博客

Java中代理有几种实现方式

JDK动态代理:

1、定义一个接口,作为代理类和目标类的共同接口。
2、创建一个实现InvocationHandler接口的代理类。
3、通过Proxy.newProxyInstance()方法创建代理对象,传入目标类的类加载器、目标类实现的接口和代理类的实例。
4、在代理类的invoke()方法中,可以在调用目标方法之前和之后执行额外的逻辑。
5、使用代理对象来调用方法,实际上是通过代理类的invoke()方法间接调用目标类的方法。
CGLIB动态代理:

引入CGLIB库的依赖。
1、创建一个实现MethodInterceptor接口的代理类。
2、通过Enhancer类创建代理对象,设置目标类作为父类、代理类作为回调对象。
3、在代理类的intercept()方法中,可以在调用目标方法之前和之后执行额外的逻辑。
4、使用代理对象来调用方法,实际上是通过代理类的intercept()方法间接调用目标类的方法。


这些代理方式各有特点,可以根据具体需求选择适合的方式。静态代理相对简单,但需要为每个目标类编写一个代理类;JDK动态代理适用于基于接口的代理;CGLIB动态代理适用于没有实现接口的类的代理。

Java新特性

  1. Lambda表达式‌:Lambda表达式允许用户定义匿名函数,即没有名称的函数。它们可以看作是一段可以传递的代码,实现了函数式编程的某些特性,使得函数作为参数传递成为可能。Lambda表达式的引入简化了代码,提高了代码的可读性和维护性‌。

  2.  

    函数式接口‌:函数式接口是只有一个抽象方法的接口,Lambda表达式与函数式接口一起使用,使得代码更加简洁。Java 8引入了多个函数式接口,如RunnableCallable等,这些接口为Lambda表达式的使用提供了基础‌。

  3.  

    方法引用‌:方法引用是Lambda表达式的补充,它允许开发者直接引用已存在的方法来代替Lambda表达式,进一步简化了代码编写‌。

  4.  

    接口的默认方法‌:Java 8允许在接口中定义默认方法,这样实现该接口的类可以选择是否覆盖这些方法。这一特性增强了接口的灵活性,使得接口可以包含一些默认的实现,而不需要强制要求实现类提供具体的实现‌。

  5.  

    Stream API‌:Stream API是Java 8中处理集合数据的一种新方式,它允许开发者以函数式编程的方式处理数据集合,提供了丰富的操作集合元素的接口和方法,使得对集合的操作更加简洁和高效‌。

  6.  

    Optional类‌:Optional类用于解决空指针异常的问题,它提供了一个容器对象可以包含也可能不包含非null的对象。通过Optional,可以更优雅地处理可能为null的情况,避免NullPointerException‌。

解决hash冲突的方式有哪些?

  1. 开放定址法(Open Addressing):当发生冲突时,通过一个小的算法计算新的哈希值,并在新的位置插入元素。

  2. 再哈希法(Rehashing):使用另一个哈ash函数,来减少冲突。

  3. 链地址法(Separate Chaining):将所有哈希值相同的元素保存在一个链表中。

  4. 建立公共溢出区域(Combine Overflow Areas):为所有的哈希值分配一个公共的溢出区域。

单例模式有哪些实现方式,有什么优缺点

1、饿汉式单例模式

优点:

  • 线程安全,因为实例在类加载的时候就已经创建好了。
  • 简单易用,没有线程同步等复杂问题。
  • 线程访问单例实例的速度比懒汉式单例模式更快。

缺点:

  • 类加载时就创建实例,有可能会浪费资源。
  • 如果单例类依赖于其他类,这些依赖的类在类加载时也会被创建,

2、懒汉式单例模式

优点:

  • 延迟对象的创建时间,减少内存占用。
  • 简单易懂,易于实现。

缺点:

  • 非线程安全,需要考虑并发情况。
  • 多线程环境下,需要使用同步锁或者双重校验锁来保证线程安全,可能会影响性能。

3、双重校验锁单例模式

是懒汉式的一种优化,判断为空,进去带锁的代码块中,在其中在判断是否为空,如果为空在创建

优点:

  • 延迟加载,只有当需要用到实例时才会创建,节省了系统资源。
  • 线程安全,适合高并发场景。

缺点:

  • 实现复杂,容易出现问题。
  • 由于 Java 内存模型的限制,可能会出现指令重排的问题,需要使用 volatile 关键字来解决。

参考:Java中的各种单例模式优缺点解析_python_脚本之家 (jb51.net)

并发

线程的创建方式

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程   有返回值
  4. 使用线程池创建线程

为什么要使用线程池

Java 高并发应用频繁创建和销毁线程的操作是非常低效的,而且是不被编程规范所允许的。线程池主要解决了以下两个问题:
1 提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己 创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度 地对已经创建的线程进行复用,使得性能提升明显。
2 线程管理:每个 Java 线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。

线程的状态转换有什么(生命周期)

新建状态(New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
1、等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
2、同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
3、其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的sleepwaitjoinyield如何使用

1、sleep 的作用是让目前正在执行的线程休眠,让 CPU 去执行其他的任务。从线程状态来说,就是从执行状态变成限时阻塞状态。
2、 wait ( 必须先获得对应的锁才能调用 ): 让线程进入等待状态 , 释放当前线程持有的锁资源线程只有在 notify 或者 notifyAll 方法调用后才会被唤醒 , 然后去争夺锁 .
3、 join : 线程之间协同方式 , 使用场景 : 线程 C 必须等待线程 B 运行完毕后才可以执行 , 那么就可以在线程 B 的代 码中加入 thread_c.join();
4、 yield : 让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因 此,使用 yield() 的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证 yield() 达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

sleep和wait区别   


sleep方法
属于Thread类中的方法

释放cpu给其它线程 不释放锁资源

sleep(1000) 等待超过1s被唤醒

wait方法
属于Object类中的方法

释放cpu给其它线程,同时释放锁资源

wait(1000) 等待超过1s被唤醒

wait() 一直等待需要通过notify或者notifyAll进行唤醒

wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException异常

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

volatile 关键字的作用

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

在Java中,最常用的锁

最常用的锁包括synchronized关键字锁、ReentrantLock锁、ReadWriteLock读写锁等。

  1.  

    synchronized关键字锁‌:这是Java中最基本的锁机制之一,它可以用来修饰方法或代码块。使用synchronized关键字修饰的方法或代码块在同一时间只能被一个线程执行,其他线程需要等待。这种锁机制基于对象的内置监视器(或称为锁)来实现线程同步,是一种独占锁,同一时刻只允许一个线程获取锁,其他线程将被阻塞等待‌。

  2.  

    ReentrantLock‌:这是Java.util.concurrent包提供的另一种锁机制,提供了与synchronized关键字类似的功能,但具备更高的灵活性。ReentrantLock提供了可重入的特性,即同一个线程可以多次获得同一个锁。这种锁允许线程重复获取同一把锁而不会造成死锁,提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法‌。

  3.  

    ReadWriteLock读写锁‌:这是一种特殊的锁机制,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。ReadWriteLock分为读锁和写锁两种,读锁是共享锁,写锁是独占锁。这种锁机制适用于读多写少的场景,可以提高并发性能‌。

这些锁机制在Java中广泛应用,用于解决多线程环境下的线程安全问题,确保数据的完整性和一致性。选择哪种锁取决于具体的应用场景和需求。

多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

synchronized 和 ReentrantLock 区别是什么?


synchronized 是关键字,ReentrantLock 是类,这是二者的本质区别。

synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。

相同点:两者都是可重入锁

主要区别如下:

1、ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
2、ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
3、二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word;Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象

如何预防死锁?

死锁发生的四个必要条件:

1. 互斥条件 同一时间只能有一个线程获取资源。
2. 不可剥夺条件 一个线程已经占有的资源,在释放之前不会被其它线程抢占
3. 请求和保持条件 线程等待过程中不会释放已占有的资源
4. 循环等待条件 多个线程互相等待对方释放资源

死锁预防,那么就是需要破坏这四个必要条件

1. 由于资源互斥是资源使用的固有特性,无法改变,不讨论
2. 破坏不可剥夺条件
一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐
式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重
新获得自己原有的资源以及新申请的资源才可以重新启动,执行
3. 破坏请求与保持条件
第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源
第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源
4. 破坏循环等待条件
采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用
较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进
程才能申请较大编号的进程。

什么是守护线程

Java 中有两类线程: User Thread( 用户线程 ) Daemon Thread( 守护线程 )
任何一个守护线程都是整个 JVM 中所有非守护线程的保姆:
只要当前 JVM 实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守 护线程结束时,守护线程随着JVM 一同结束工作。 Daemon 的作用是为其他线程的运行提供便利服务, 守护线程最典型的应用就是 GC ( 垃圾回收器 ) ,它就是一个很称职的守护者。
User Daemon 两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread 已经全部 退出运行了,只剩下Daemon Thread 存在了,虚拟机也就退出了。 因为没有了被守护者, Daemon 也 就没有工作可做了,也就没有继续运行程序的必要了。

ThreadLocal 

概念

ThreadLocal 是Java并发包(java.util.concurrent)中提供的一个类,它的主要作用是在多线程环境下为每个线程提供一个独立的变量副本,使得每个线程在访问 ThreadLocal 时获取到的都是自己的私有变量,而不是共享的同一个变量。换句话说,ThreadLocal 能够隔离线程间的数据共享,提供线程级别的数据存储。

日常使用

1、线程上下文信息传递:例如在web应用中,服务器接收到请求后,需要在不同的过滤器、处理器链路中传递用户会话信息,此时可以将这些信息存放在 ThreadLocal 中,因为在Servlet容器中,每个HTTP请求都会被分配到一个单独的线程中处理。

2、避免同步开销:对于那些只需要在单个线程内保持状态,不需要线程间共享的数据,使用 ThreadLocal 可以避免使用锁带来的性能损耗。

3、数据库连接、事务管理:在多线程环境下,每个线程有自己的数据库连接,可以使用 ThreadLocal 存储当前线程的数据库连接对象,以确保线程安全。

实际开发中的用法

1、初始化/设置值:ThreadLocal.set(T value) 方法用来设置当前线程的变量副本值。
2、获取值:T get() 方法用来获取当前线程所对应的变量副本的值,如果此线程从未设置过值,那么返回 null 或者初始值(如果有的话)。
3、移除值:remove() 方法用于删除当前线程保存的变量副本,如果不主动清理,可能会造成内存泄露。

使用时需要注意的问题

1、内存泄露:当线程结束生命周期后,如果没有显式调用 remove() 方法,存储在线程本地变量表中的 ThreadLocal 变量副本不会自动删除,这可能导致它们无法被垃圾回收,尤其是在线程池场景中,如果线程会被复用,这个问题更为突出。

2、线程安全的误解:虽然 ThreadLocal 保证了每个线程只能访问自己的变量副本,但是它并不能保证变量副本本身的线程安全性,即如果存放在 ThreadLocal 中的对象不是线程安全的,多个线程通过各自的 ThreadLocal 访问相同的非线程安全对象时,还需要采取额外的同步措施。

3、过度使用:不恰当的使用 ThreadLocal 可能导致代码逻辑变得复杂,增加维护难度,尤其是当线程间本来就需要共享数据时,不应该滥用 ThreadLocal 避免数据交换。

参考:

ThreadLocal详解及用法示例_threadlocal的用途和用法-CSDN博客

Condition 类和Object 类锁方法区别

  1. 使用方式与功能特性‌:

  2.  

    配合使用的同步机制‌:

    • Object类的wait(), notify(), notifyAll()方法是与synchronized关键字一起使用的,这意味着它们只能在synchronized方法或块中使用,且synchronized关键字隐式地获取了对象的锁。
    • Condition类的方法则是与Lock接口一起使用的,这提供了更大的灵活性,包括支持响应中断、可轮询锁请求、定时锁等特性。Condition类允许更精细的控制,例如可以唤醒满足特定条件的线程,这是Object类无法实现的。
  3.  

    线程控制‌:

    • Object类的notify()和notifyAll()方法是随机的,唤醒等待的线程时没有特定的顺序。
    • Condition类提供了更多的控制选项,例如可以唤醒特定条件的线程,这使得在复杂的线程同步场景中更加有用。

综上所述,虽然Object类和Condition类都提供了等待/通知机制,但它们在使用方式、功能特性以及配合使用的同步机制上存在显著差异。Condition类提供了更多的控制和灵活性,尤其是在需要更复杂的线程同步场景中,如需要响应中断、轮询锁请求或定时锁等情况下,是Object类无法比拟的‌。

如何避免ABA问题:

可以通过加版本号或者加时间戳解决

JVM

JVM内存分哪几个区,每个区的作用是什么

  1. 方法区‌(有时也称为永久代):这个区域主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。它是线程共享的,并且进行的主要垃圾回收是对方法区里的常量池和对类型的卸载。运行时常量池位于方法区内,用于存放静态编译产生的字面量和符号引用,具有动态性,即常量并不一定是编译时确定,运行时生成的常量也会存在于这个常量池中。

  2.  

    虚拟机栈‌:为Java方法服务,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。虚拟机栈是线程私有的,其生命周期与线程相同。局部变量表存储基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用等。

  3.  

    本地方法栈‌:与虚拟机栈类似,但为Native方法服务。执行每个本地方法时,也会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  4.  

    ‌:这是所有线程共享的一块内存区域,几乎所有的对象实例都在这里创建。因此,该区域经常发生垃圾回收操作。

  5.  

    程序计数器‌:这是一个较小的内存空间,字节码解释器通过改变这个计数值来选取下一条需要执行的字节码指令。它完成了分支、循环、跳转、异常处理和线程恢复等功能,是Java虚拟机规范中唯一一个没有规定任何OOM情况的区域。

什么是类加载器,类加载器有哪些?

1、什么是类加载器?

类加载器负责加载所有的类,其为所有被载入内存的类生成一个java.lang.Class实例对象。

2、类加载器有哪些?

JVM有三种类加载器:

(1)启动类加载器

该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自java.lang.classLoader。

(2)扩展类加载器

它的父类为启动类加载器,扩展类加载器是纯java类,是ClassLoader类的子类,负责加载JRE的扩展目录。

(3)应用程序类加载器

它的父类为扩展类加载器,它从环境变量classpath或者系统属性java.lang.path所指定的目录中加载类,它是自定义的类加载器的父加载器。

说一下类加载的执行过程?


当程序主动使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接、初始化3个步骤对该类进行类加载。

1、加载

加载指的是将类的class文件读入到内存中,并为之创建一个java.lang.Class对象。

类的加载由类加载器完成,类加载器由JVM提供,开发者也可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器可以从不同来源加载类的二进制数据,通常有如下几种来源:

从本地文件系统加载
从jar包加载
通过网络加载
把一个Java源文件动态编译,并执行加载
2、连接

当类被加载之后,系统为之生成一个对应的Class对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到JRE中。

类连接又可分为三个阶段:

(1)验证

文件格式验证
元数据验证
字节码验证
符号引用验证

(2)准备

为类的静态变量分配内存,并设置默认初始值。

(3)解析

将类的二进制数据中的符号引用替换成直接引用。

3、初始化

为类的静态变量赋予初始值。

JVM的类加载机制是什么?


JVM类加载机制主要有三种:

1、全盘负责

类加载器加载某个class时,该class所依赖的和引用其它的class也由该类加载器载入。

2、双亲委派

先让父加载器加载该class,父加载器无法加载时才考虑自己加载。

3、缓存机制

缓存机制保证所有加载过的class都会被缓存,当程序中需要某个class时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成class对象,存入缓存区中。

这就是为什么修改了class后,必须重启JVM,程序所做的修改才会生效的原因。

什么是双亲委派模型?

如果一个类收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器执行,如果父加载器还存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器,如果父类加载器可以完成父加载任务,就成功返回,如果父加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型。

双亲委派模式的优势:

避免重复加载;
考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委派模式传递到启动加载器,而启动加载器在核心Java API中发现同名的类,发现该类已经被加载,就不会重新加载网络传递的Integer类,而直接返回已加载过的Integer.class,这样可以防止核心API库被随意篡改。

怎么判断对象是否可以被回收?


1、引用计数算法

(1)判断对象的引用数量

通过判断对象的引用数量来决定对象是否可以被回收;
每个对象实例都有一个引用计数器,被引用+1,完成引用-1;
任何引用计数为0的对象实例可以被当做垃圾回收;

(2)优缺点

优点:执行效率高,程序受影响较小;
缺点:无法检测出循环引用的情况,导致内存泄漏;

2、可达性分析算法

通过判断对象的引用链是否可达来决定对象是否可以被回收。

如果程序无法再引用该对象,那么这个对象肯定可以被回收,这个状态称为不可达。

那么不可达状态如何判断呢?

答案是GC roots,也就是根对象,如果一个对象无法到达根对象的路径,或者说从根对象无法引用到该对象,该对象就是不可达的。

以下三种对象在JVM中被称为GC roots,来判断一个对象是否可以被回收。

(1)虚拟机栈的栈帧

每个方法在执行的时候,JVM都会创建一个相应的栈帧(操作数栈、局部变量表、运行时常量池的引用),当方法执行完,该栈帧就从栈中弹出,这样一来,方法中临时创建的独享就不存在了,或者说没有任何GC roots指向这些临时对象,这些对象在下一次GC的时候便会被回收。

(2)方法区中的静态属性

静态属性数据类属性,不属于任何实例,因此该属性自然会作为GC roots。这要这个class在,该引用指向的对象就一直存在,class也由被回收的时候。

class何时会被回收?

堆中不存在该类的任何实例
加载该类的classLoader已经被回收
该类的java.lang.class对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息
(3)本地方法栈引用的对象

 jvm 有哪些垃圾回收算法?

(1)标记清除算法

如果对象被标记后进行清除,会带来一个新的问题--内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

(2)复制算法(Java堆中新生代的垃圾回收算法)

先标记待回收内存和不用回收内存;
将不用回收的内存复制到新的内存区域;
就的内存区域就可以被全部回收了,而新的内存区域也是连续的;
缺点是损失部分系统内存,因为腾出部分内存进行复制。

(3)标记压缩算法(Java堆中老年代的垃圾回收算法)

对于新生代,大部分对象都不会存活,所以复制算法较高效,但对于老年代,大部分对象可能要继续存活,如果此时使用复制算法,效率会降低。

标记压缩算法首先还是标记,将不用回收的内存对象压缩到内存一端,此时即可清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。

老年代的垃圾回收算法称为“Major GC”。

JVM栈堆概念,何时销毁对象


类在程序运行的时候就会被加载,方法是在执行的时候才会被加载,如果没有任何引用了,Java自动垃圾回收,也可以用System.gc()开启回收器,但是回收器不一定会马上回收。
静态变量在类装载的时候进行创建,在整个程序结束时按序销毁;
实例变量在类实例化对象时创建,在对象销毁的时候销毁;
局部变量在局部范围内使用时创建,跳出局部范围时销毁;

什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查    


引发 StackOverFlowError 的常见原因有以下几种

1、无限递归循环调用(最常见)
2、执行了大量方法,导致线程栈空间耗尽
3、方法内声明了海量的局部变量
4、native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。


引发 OutOfMemoryError的常见原因有以下几种


1、内存中加载的数据量过于庞大,如一次从数据库取出过多数据
2、集合类中有对对象的引用,使用完后未清空,使得JVM不能回收
3、代码中存在死循环或循环产生过多重复的对象实体
4、启动参数内存值设定的过小

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值