Java高频面试题合集——【Java基础、IO流、Java异常、Java集合、Java并发】高频面试题汇总

❤ 作者主页:欢迎来到我的技术博客😎
❀ 个人介绍:大家好,本人热衷于Java后端开发,欢迎来交流学习哦!( ̄▽ ̄)~*
🍊 如果文章对您有帮助,记得关注点赞收藏评论⭐️⭐️⭐️
📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉

文章目录

1、JDK、JRE和JVM三者之间的关系?

JDK(Java Development Kit): 是 Java 开发工具包,是整个 Java 的核心。

JRE( Java Runtime Environment): 是 Java 的运行环境。

JVM(Java Virtual Machine): 是 Java 虚拟机,是整个 Java 实现跨平台的最核心的部分,能够运行以 Java 语言编写的软件程序。所有的 Java 程序会首先被编译为 .class 的类文件,这种类文件可以在虚拟机上执行。


2、Java中创建对象有几种方式?

  1. 使用 new 关键字

  2. 通过反射机制创建,该方法调用无参的构造器创建对象(反射):Class.forName.newInstance()

  3. 使用 clone() 方法

  4. 反序列化,比如调用 ObjectInputStream 类的 readObject() 方法。


3、==和equals()有什么区别?

==运算符:

  • 作用于基本数据类型时,是比较两个数值是否相等;

  • 作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;

equals()方法:

  • 没有重写时,Object默认以 == 来实现,即比较两个对象的内存地址是否相同;

  • 进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。


4、重写和重载的区别?

重载 发生在同一个类中,若多个方法之间 方法名相同、参数列表不同,则它们构成重载的关系。重载与方法的返回值以及访问修饰符无关,即重载的方法不能根据返回类型进行区分。

重写 发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的 方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。还有,若父类方法的访问修饰符为private,则子类不能对其重写。


5、构造方法有哪些特性?

  1. 名字与类名相同;
  2. 没有返回值,但不能用 void 声明构造函数;
  3. 成类的对象时自动执行,无需调用。

6、自动装箱和自动拆箱的区别?

  • 自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;

  • 自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;

通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。


7、Integer 和 int 的区别?

  • int是基本数据类型,Integer是int的包装类;
  • int型变量的默认值是0,Integer变量的默认值是null,这一点说明Integer可以区分出未赋值和值为0的变量;
  • Integer变量必须实例后才可以使用,而int不需要。

8、说一说hashCode()和equals()的关系?

hashCode()用于获取哈希值,eauqls()用于比较两个对象是否相等,它们应遵守如下规定:

  • 如果两个对象相等,则它们必须有相同的哈希值。

  • 如果两个对象有相同的哈希码,则它们未必相等。


9、为什么重写 equals() 时必须重写 hashCode() 方法?

Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。

如果重写 equals() 时没有重写 hashCode() 方法的话,就可能会导致 equals() 方法判断是相等的两个对象,哈希值却不相等。


10、String、StringBuilder、StringBuffer 的区别?

  • String 中的对象是不可变的,也就可以理解为常量,线程安全。

  • StringBuilder与 StringBuffer类似,都是字符串缓冲区,但线程不安全;

  • StringBuffer 对方法加了同步锁,因此线程安全。

执行效率:StringBuilder > StringBuffer > String


11、浅拷贝和深拷贝的区别?

  • 浅拷贝:拷贝对象与原始对象是同一个对象,浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值也会随之变化,这就是浅拷贝;
  • 深拷贝:拷贝对象与原始对象是同两个对象,深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝。

深拷贝的实现就是在引用类型所在的类实现 Cloneable 接口,并使用 public 访问修饰符 重写 clone 方法

在这里插入图片描述


12、抽象类和接口的区别?

  • 抽象类中可以定义构造函数,接口不能定义构造函数;

  • 抽象类中可以有抽象方法和具体方法,而接口中只能有抽象方法;

  • 抽象类中的成员权限可以是public、默认、protected,而接口中的成员权限只能是public;

  • 抽象类中可以包含静态方法,而接口中不可以包含静态方法。


13、静态变量和实例变量的区别?

  • 静态变量: 是被static修饰的变量,它属于类,因此不管创建多少个对象,静态变量在内存中有且只有一个拷贝,静态变量可以实现让多个对象共享内存;
  • 实例变量:属于某一实例,需要先创建对象,然后通过对象才能访问到它。

14、short s1 = 1;s1 = s1 + 1;有什么错?那么 short s1 = 1; s1 += 1;呢?有没有错误?

  • 对于 short s1 = 1; s1 = s1 + 1; 来说,在 s1 + 1 运算时会自动提升表达式的类型为 int ,那么将 int 型值赋值给 short 型变量,s1 会出现类型转换错误。

  • 对于 short s1 = 1; s1 += 1; 来说,+= 是 Java 语言规定的运算符,Java 编译器会对它进行特殊处理,因此可以正确编译。


15、final、finally和finalize 有什么区别?

  • final 和 finally 是Java中的关键字,而 finalize 是一个方法;
  • final:用于修饰类、变量和方法,表示这个类、变量和方法不能被继承或重写;
  • finally: 是在 try-catch 语句中使用的关键字,表示不管是否发生异常,finally中的代码都会执行;
  • finalize: 是Object() 类中的一个方法,是一个在垃圾回收器将对象从内存中清除之前调用的方法。

16、什么是Java反射机制?Java反射有哪些应用场景?

Java反射指的是在Java程序运行状态中,对于任何一个类,都可以获得这个类的所有属性和方法;对于给定的一个对象,都能够调用它的任意一个属性和方法。这种动态获取类的内容以及动态调用对象的方法称为反射机制。

Java的反射机制常见的应用场景有:

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;

  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;

  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。


17、介绍一下Java中的IO流?

  • 按照数据流向,可以将流分为 输入流输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。

  • 按照数据类型,可以将流分为 字节流字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。

  • 按照处理功能,可以将流分为 节点流处理流,其中节点流可以直接从/向一个特定的IO设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流。


18、介绍一下Java的序列化和反序列化?

序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。

若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java的很多类已经实现了Serializable接口,如包装类、String、Date等。

若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。


19、什么情况下需要序列化?

  • 永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
  • 通过序列化以字节流的形式使对象在网络中进行传递和接受;
  • 通过序列化在进程中传递对象。

20、常见的异常类有哪些?

  • NullPointerException:当应用程序试图访问空对象时,则抛出该异常。
  • SQLException:提供关于数据库访问错误或其他错误信息的异常。
  • IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出。
  • FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
  • IOException:当发生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成的异常的通用类。
  • ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。
  • IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数。

21、Exception和Error的区别?

  • Error 类和 Exception 类的父类都是 Throwable 类。
  • Exception: 程序本身可以处理的异常,可以通过 try 来进行捕获。又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • Error: Error 属于程序无法处理的错误。例如 Java 虚拟机运行错误、虚拟机内存不够等错误 。这些异常发生时,Java 虚拟机一般会选择线程终止。

22、try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

会。程序在执行到 return 时会首先将返回值存储在一个指定的位置,其次去执行 finally 块,最后再返回。因此,对基本数据类型,在 finally 块中改变 return 的值没有任何影响,直接覆盖掉;而对引用类型是有影响的,返回的是在 finally 对 前面 return 语句返回对象的修改值。


23、finally是不是一定会被执行到?

不一定,下面列举两种执行不到的情况:

  • 当程序进入 try 块之前就出现异常时,会直接结束,不会执行 finally 块中的代码;
  • 当程序在 try 块中强制退出时也不会去执行 finally 块中的代码,比如在 try 块中执行 exit 方法。

24、throw 和 throws 的区别?

  • throw:在方法体内部,表示抛出异常,由方法体内部的语句处理;throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例;
  • throws:在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。

25、运行时异常和受检异常有何异同?

  • 运行时异常:如:空指针异常、指定的类找不到、数组越界、方法传递参数错误、数据类型转换错误。可以编译通过,但是一运行就停止了,程序不会自己处理;
  • 受检查异常:要么用 try … catch… 捕获,要么用 throws 声明抛出,交给父类处理。

26、什么是注解?注解的解析方法有哪几种?

Annotation (注解) : 可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用 @Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。

27、Java中常用的容器有哪些?

Java中的集合类主要由 CollectionMap 这两个接口派生而出,其中Collection接口又派生出三个子接口,分别是Set、List、Queue。所有的Java集合类,都是Set、List、Queue、Map这四个接口的实现类,这四个接口将集合分成了四大类,其中

  • Set代表无序的,元素不可重复的集合;

  • List代表有序的,元素可以重复的集合;

  • Queue代表先进先出(FIFO)的队列;

  • Map代表具有映射关系(key-value)的集合。

在这里插入图片描述


28、ArrayList 和 LinkedList有什么区别?

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

  • 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N);

  • 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引;

  • LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。


29、说一说HashSet 和 TreeSet的区别?

HashSet、TreeSet中的元素都是不能重复的,并且它们都是 线程不安全 的,二者的区别是:

  • HashSet底层是采用 哈希表 实现的,而TreeSet底层是采用 红黑树 实现的。

  • HashSet中的元素可以是null,但TreeSet中的元素不能是null;

  • HashSet不能保证元素的排列顺序,而TreeSet支持自然排序、定制排序两种排序的方式;


30、介绍一下HashMap 底层的实现原理?

JDK7中的HashMap,是基于 数组+链表 来实现的,它的底层维护一个 Entry数组

JDK7中HashMap的实现方案有一个明显的缺点,即当Hash冲突严重时,在桶上形成的链表会变得越来越长,这样在查询时的效率就会越来越低,其时间复杂度为O(N)。

JDK8中的HashMap,是基于 数组+链表+红黑树 来实现的,它的底层维护一个 Node数组。当链表的存储的数据个数大于等于 8 的时候,不再采用链表存储,而采用了红黑树存储结构。这么做主要是在查询的时间复杂度上进行优化,链表为O(N),而红黑树一直是O(logN),可以大大的提高查找性能。


31、介绍一下HashMap的扩容机制?

  1. 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。

  2. 数组是否需要扩充是通过 负载因子 判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。

  3. 为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。

  4. 对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。


32、HashMap 的长度为什么是 2 的幂次方?

  1. 提高计算效率,更快算出元素的位置。 对于机器而言,位运算永远比取余运算快得多,在length为2的整数次方的情况下,hash(key) % length 能被替换成高效的 hash(key) &(length - 1),两者的结果是相等的。

  2. 减少哈希碰撞,使得元素分布均匀。因此,数组长度是一个2的整数次方时,哈希碰撞的概率起码能下降一半,而且所有元素也能均匀地分布在数组上。


33、HashMap 的加载因子为何默认是0.75f?

  • 在空间占用与查询时间之间取得较好的权衡;
  • 大于这个值,空间节省了,但链表就会比较长影响性能;
  • 小于这个值,冲突减少了,但扩容就会更频繁,空间占用多。

34、HashMap 的 get 方法能否判断某个元素是否在 map 中?

HashMap 的 get 函数的返回值不能判断一个 key 是否包含在 map 中,因为 get 返回 null 有可能是不包含该 key,也有可能该 key 对应的 value 为 null。因为 HashMap 中允许 key 为 null,也允许 value 为 null。


35、介绍一下HashSet 的实现原理?

HashSet 的实现是依赖于 HashMap 的,HashSet 的值都是存储在 HashMap 中的。在 HashSet 的构造法中会初始化一个 HashMap 对象,HashSet 不允许值重复。因此,HashSet 的值是作为 HashMap 的 key 存储在 HashMap 中的,当存储的值已经存在时返回 false。


36、 Collection 和 Collections 有什么区别?

  • Collection:是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素。它的直接继承接口有 List,Set 和 Queue。

  • Collections:是不属于 Java 的集合框架的,它是集合类的一个工具类/帮助类。此类不能被实例化, 服务于 Java 的 Collection 框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。


37、创建线程的几种方式?

  1. 继承 Thread 类创建线程;
  2. 实现 Runnable 接口创建线程;
  3. 通过 Callable 和 Future 创建线程;
  4. 通过线程池创建线程。

38、进程和线程的区别?

进程: 是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。

线程: 是进程的一个实体,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。


39、线程的run()和start()有什么区别?

run()方法被称为 线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来 启动线程

调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理。但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。


40、Runnable 和 Callable 有什么区别?

  • Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是纯粹地去执行 run() 方法中的代码而已;

  • Callable 接口中的 call() 方法是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。


41、说一说sleep()和wait()的区别?

  1. sleep() 是Thread 类中的 静态方法,而 wait() 是 Object 类中的 成员方法
  2. sleep() 可以在任何地方使用, 而 wait() 只能在同步方法或同步代码块中使用;
  3. sleep() 不会释放锁, 而 wait() 会释放锁,并需要通过 notify() / notifyAll() 重新获取锁。

42、说一说notify()、notifyAll()的区别?

  • notify(): 用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

  • notifyAll(): 用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。


43、介绍一下线程的生命周期?

在线程的生命周期中,它要经过新建(New)、就绪(Ready)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、就绪之间切换。

在这里插入图片描述


44、说一说synchronized与Lock的区别?

  1. synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。

  2. synchronized可以用在代码块上、方法上;Lock只能写在代码里。

  3. synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,需要在finally中显示释放锁。

  4. synchronized会导致线程拿不到锁一直等待;Lock可以设置获取锁失败的超时时间。

  5. synchronized无法得知是否获取锁成功;Lock则可以通过tryLock得知加锁是否成功。

  6. synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。


45、synchronized是什么?

synchronized 是 Java中一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或代码块在任意时刻只能有一个线程执行。


46、如何使用synchronized?

synchronized 关键字的使用方式主要有以下的3种:

  1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
  2. 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁;
  3. 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获取给定对象的锁。

47、构造方法可以用 synchronized 修饰吗?

构造方法不能使用 synchronized 关键字修饰,因为构造方法本身属于线程安全的,不存在同步的构造方法一说。


48、说一说 synchronized 的底层实现原理?

  1. synchronized 作用在 代码块 时,它的底层是通过 monitorentermonitorexit 指令来实现的,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

在这里插入图片描述

  • 当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 (monitor) 的持有权,如果锁的计数器为0,则表示锁可以获取,获取后将计数器设为1,也就是加1;

在这里插入图片描述

  • 对象锁的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放,其他线程可以尝试获取锁;

在这里插入图片描述

  • 如果获取对象锁失败,那当前线程就要阻塞等待,知道锁被另外一个线程释放为止。
  1. synchronized 修饰 方法,并没有通过 monitorenter 和 monitorexit 指令来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:
  • 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
    在这里插入图片描述

不过两者的本质都是对对象监视器 monitor 的获取。


49、JDK1.6 之后的 synchronized 底层做了哪些优化?

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

在这里插入图片描述


50、synchronized 和 volatile 有什么区别?

  • synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在;
  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能比 synchronized 关键字要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块;
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都可以保证;
  • volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

51、volatile 关键字有什么用?

  1. 保证可见性: 如果我们将变量声明为 volatile,这就指示JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
  2. 禁止指令重排: 如果我们将变量声明为 volatile,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

52、谈谈volatile的实现原理?

volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用 “内存屏障” 来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个 lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。


53、synchronized 和 ReentrantLock 区别是什么?

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

  • synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的API接口;

  • synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块;

  • synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁;

  • synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁;

  • ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断;

  • synchronized 不能响应中断 ,而ReentrantLock 可以响应中断,解决死锁的问题。


54、公平锁和非公平锁有什么区别?

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁 :锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

55、谈一下对悲观锁和乐观锁的理解?

  • 悲观锁: 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。
  • 乐观锁:乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了。

使用场景:

  • 悲观锁通常多用于写多比较多的情况下(多写场景),避免频繁失败和重试影响性能。
  • 乐观锁通常多于写比较少的情况下(多读场景),避免频繁加锁影响性能,大大提升了系统的吞吐量。

56、如何实现乐观锁?

乐观锁一般会使用 版本号机制CAS算法 实现,CAS算法相对来说更多一些,这里需要格外注意。

  • 版本号机制
    一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

  • CAS算法

    CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

    CAS 涉及到三个操作数:

    • V :要更新的变量值(Var)
    • E :预期值(Expected)
    • N :拟写入的新值(New)

    当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。


57、乐观锁存在哪些问题?

  • ABA问题
    如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 ABA问题

    ABA 问题的解决思路是在变量前面追加上 版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  • 循环时间长开销大
    CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

    如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
    1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
    2. 可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。

  • 只能保证一个共享变量的原子操作

    CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用AtomicReference 类把多个共享变量合并成一个共享变量来操作。


58、AQS是什么?

AQS 的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。

AQS就是一个抽象类,主要用来构造锁和同步锁。

AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。


59、AQS的原理是什么?

AQS 核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

在这里插入图片描述

CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。


60、什么是线程池?

线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。


61、为什么要用线程池?

线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

 
创作不易,如果有收获!!!别忘记点个赞,让更多人看到!!!


关注博主不迷路,内容持续更新中!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java技术一点通

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值