Java面试知识点

我在知乎上看了很多份Java面试题,其实有一些比较好的,就拿出来分享给大家,根据自己实际情况可以查漏补缺。

目录

1.你觉得java好在哪? 

2.你如何设计一个HashMap

3.并发类库提供的线程池有哪些?

4. GC 如何调优?

5.动态代理是什么

6.JDK 动态代理与 CGLIB 区别?

7.注解是什么原理?

8.反射用过吗?

9.类加载的过程

10、双亲委派机制

11.JDK和JRE的区别

12.用过哪些 JDK 提供的工具?

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

14.什么是序列化( Serializable )?什么是反序列化?

15.什么是不可变类?

16.Java 按值传递还是按引用传递?

17.泛型有什么用?泛型擦除是什么?

18.说说强、软、弱、虚引用?

19.Integer 缓存池知道吗?

20.Exception 和 Error 的区别知道吗?

21.深拷贝和浅拷贝?

22.面向对象编程和面向过程编程的区别?

23.重载与重写的区别?

24.什么是内部类,有什么用?

25.说说 Java 的集合类吧?

26.说说 ThreadLocal ?

27.同步、异步、阻塞、非阻塞 IO 的区别?

28.BIO、NIO、AIO?

29.JDK8 有哪些新特性?

30.你都用过哪些 Java 并发工具类?

Semaphore

CyclicBarrier

CountDownLatch

StampedLock

32.Java 中的阻塞队列用过哪些?

33.用过Java 中哪些原子类?

基本类型

数组类型

引用类型

属性更新类型

累加器

34.Synchronized 和 ReentrantLock 区别?

35.Synchronized 原理知道不?

36.ReentrantLock 的原理?

37.说说 AQS 吧?

38.读写锁知道不?

39.CAS 知道不?

40.说说线程的生命周期?

41.什么是 JMM ?

42.说说原子性、可见性、有序性?

43.说说 Java 常见的垃圾收集器?

44.垃圾回收,如何判断对象是否是垃圾?

45.你知道有哪些垃圾回收算法?

标记-清除

复制算法

标记-整理算法

46.String,Stringbuffer,StringBuilder的区别?

47.happens-before 听过吗?

48.什么是锁的自适应自旋?

49.JVM 内存区域划分

50. 指令重排知道吗?

51.final 和可以保证可见性吗?

52.锁如何优化?


1.你觉得java好在哪?

  • java拥有跨平台性,跨平台是指一次编写到处运行

    • 执行流程是java代码-->编译成字节码-->通过jvm编译成机器码(二级制)

  • java拥有垃圾自动回收机制,将内存的管理交给GC来做,可以提高开发效率

  • java的生态非常好,java拥有丰富的第三方类库,网上全面的资料,企业级框架(spring,springmvc,mybatis,springboot,springcloud),还有各种中间件

2.你如何设计一个HashMap

HashMap是一个键值格式的集合,通过key可以快速找到对应的value,HashMap处理冲突可以采用拉链法来处理冲突,为了避免恶意的hash攻击,当拉链法超过一定长度后就可以转为红黑树结构(java8)

HashMap扩容可以考虑和redis那样两个table延迟移动

3.并发类库提供的线程池有哪些?

首先线程池有几个关键的配置:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。

  1. 默认情况下线程不会预创建,所以是来任务之后才会创建线程(设置prestartAllCoreThreads可以预创建核心线程)。

  2. 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。

  3. 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。

  4. 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。

  5. 如果线程空闲时间超过空闲存活时间,并且线程线程数是大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置allowCoreThreadTimeOut 可以回收核心线程)。

我们再回到面试题来,这个实现指的就是 Executors 的 5 个静态工厂方法:

  • newFixedThreadPool

  • newWorkStealingPool

  • newSingleThreadExecutor

  • newCachedThreadPool

  • newScheduledThreadPool

4. GC 如何调优?

GC 调优的核心思路就是尽可能的使对象在年轻代被回收,减少对象进入老年代。

具体调优还是得看场景根据 GC 日志具体分析,常见的需要关注的指标是 Young GC 和 Full GC 触发频率、原因、晋升的速率 、老年代内存占用量等等。

比如发现频繁会产生 Full GC,分析日志之后发现没有内存泄漏,只是 Young GC 之后会有大量的对象进入老年代,然后最终触发 Ful GC。所以就能得知是 Survivor 空间设置太小,导致对象过早进入老年代,因此调大 Survivor 。

或者是晋升年龄设置的太小,也有可能分析日志之后发现是内存泄漏、或者有第三方类库调用了 System.gc等等。

反正具体场景具体分析,核心思想就是尽量在新生代把对象给回收了。

5.动态代理是什么

动态代理就是一个代理机制,动态是相对于静态来说的。

代理可以看作是调用目标的一个包装,通常用来在调用真实的目标之前进行一些逻辑处理,消除一些重复的代码。

静态代理指的是我们预先编码好一个代理类(手写),而动态代理指的是运行时生成代理类(生成)。

动态更加方便,可以指定一系列目标来动态生成代理类(AOP),而不像静态代理需要为每个目标类写对应的代理类。

代理也是一种解耦,目标类和调用者之间的解耦,因为多了代理类这一层。

常见的动态代理有 JDK 动态代理 和 CGLIB。

6.JDK 动态代理与 CGLIB 区别?

JDK 动态代理是基于接口的,所以要求代理类一定是有定义接口的

CGLIB 基于ASM字节码生成工具,它是通过继承的方式来实现代理类,所以要注意 final 方法

基本上这样答差不多了,我们再看看 JDK 动态代理实现原理:

  1. 首先通过实现 InvocationHandler 接口得到一个切面类。

  2. 然后利用 Proxy 根据目标类的类加载器、接口和切面类得到一个代理类。

  3. 代理类的逻辑就是把所有接口方法的调用转发到切面类的 invoke() 方法上,然后根据反射调用目标类的方法。

7.注解是什么原理?

注解其实就是一个标记,可以标记在类上、方法上、属性上等,标记自身也可以设置一些值。

有了标记之后,我们就可以在解析的时候得到这个标记,然后做一些特别的处理,这就是注解的用处。

@Target 等等

8.反射用过吗?

反射其实就是Java提供的能在运行期可以得到对象信息的能力,包括属性、方法、注解等,也可以调用其方法。

一般的编码不会用到反射,在框架上用的较多,因为很多场景需要很灵活,所以不确定目标对象的类型,届时只能通过反射动态获取对象信息。

9.类加载的过程

类加载顾名思义就是把类加载到 JVM 中,而输入一段二进制流到内存,之后经过一番解析、处理转化成可用的 class 类,这就是类加载要做的事情。

二进制流可以来源于 class 文件,或者通过字节码工具生成的字节码或者来自于网络都行,只要符合格式的二进制流,JVM 来者不拒。

类加载流程分为加载、连接、初始化三个阶段,连接还能拆分为:验证、准备、解析三个阶段。

所以总的来看可以分为 5 个阶段:

  • 加载:将二进制流搞到内存中来,生成一个 Class 类。

  • 验证:主要是验证加载进来的二进制流是否符合一定格式,是否规范,是否符合当前 JVM 版本等等之类的验证。

  • 准备:为静态变量(类变量)赋初始值,也即为它们在方法区划分内存空间。这里注意是静态变量,并且是初始值,比如 int 的初始值是 0。

  • 解析:将常量池的符号引用转化成直接引用。符号引用可以理解为只是个替代的标签,比如你此时要做一个计划,暂时还没有人选,你设定了个 A 去做这个事。然后等计划真的要落地的时候肯定要找到确定的人选,到时候就是小明去做一件事。 解析就是把 A(符号引用) 替换成小明(直接引用)。符号引用就是一个字面量,没有什么实质性的意义,只是一个代表。直接引用指的是一个真实引用,在内存中可以通过这个引用查找到目标。

  • 初始化:这时候就执行一些静态代码块,为静态变量赋值,这里的赋值才是代码里面的赋值,准备阶段只是设置初始值占个坑。

10、双亲委派机制

双亲委派的意思是

如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此。

一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。

这里的双亲其实就指的是父类,没有mother。

父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。

Java 自身提供了 3 种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用 C++ 实现的,主要负责加载\lib目录中或被-Xbootclasspath指定的路径中的并且文件名是被虚拟机识别的文件。它是所有类加载器的爸爸。

  2. 扩展类加载器(Extension ClassLoader),它是Java实现的,独立于虚拟机,主要负责加载\lib\ext目录中或被java.ext.dirs系统变量所指定的路径的类库。

  3. 应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这玩意就是我们程序中的默认加载器。

所以一般情况类加载会从应用程序类加载器委托给扩展类再委托给启动类,启动类找不到然后扩展类找,扩展类加载器找不到再应用程序类加载器找。(先到启动类找(bootstrap),再到扩展类找(extension),再到应用程序类(application)找)

双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式

11.JDK和JRE的区别

JRE(Java Runtime Environment)指的是 Java 运行环境包含了 JVM 和 Java 类库等

JDK(Java Development Kit) 是java开发者工具,还提供了一些工具比如各种诊断工具:jstack,jmap,jstat 等。

JDK包括JRE和JVM

12.用过哪些 JDK 提供的工具?

  • jps:虚拟机进程状况工具

  • jstat:虚拟机统计信息监视工具

  • jmap:Java内存映像工具

  • jhat:虚拟机堆转储快照分析工具

  • jstack:Java堆栈跟踪工具

  • jinfo:Java配置信息工具

  • VisualVM:图形化工具,可以得到虚拟机运行时的一些信息:内存分析、CPU 分析等等,在 jdk9 开始不再默认打包进 jdk 中。

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

接口:只能包含抽象方法,不能包含成员变量,当 has a 的情况下用接口。

接口是对行为的抽象,类似于条约。在 Java 中接口可以多实现,从 has a 角度来说接口先行,也就是先约定接口,再实现。

抽象类: 可以包含成员变量和一般方法和抽象方法,当 is a 并且主要用于代码复用的场景下使用抽象类继承的方式,子类必须实现抽象类中的抽象方法。

在 Java 中只支持单继承。从 is a 角度来看一般都是先写,然后发现代码能复用,然后抽象一个抽象类。

14.什么是序列化( Serializable )?什么是反序列化?

序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。 (java代码转换成二级制代码)

反序列化就是将字节序列格式转换成对象的过程。 (二级制代码转为java代码)

15.什么是不可变类?

不可变类指的是无法修改对象的值,比如 String 就是典型的不可变类,当你创建一个 String 对象之后,这个对象就无法被修改。

不可变类的好处就是安全,因为知晓这个对象不可能会被修改,因此可以放心大胆的用,在多线程环境下也是线程安全的。

String 类用 final 修饰,表示无法被继承。

16.Java 按值传递还是按引用传递?

Java 只有按值传递,不论是基本类型还是引用类型。

JVM 内存有划分为栈和堆,局部变量和方法参数是在栈上分配的,基本类型和引用类型都占 4 个字节,当然 long 和 double 占 8 个字节。

而对象所占的空间是在堆中开辟的,引用类型的变量存储对象在堆中地址来访问对象,所以传递的时候可以理解为把变量存储的地址给传递过去,因此引用类型也是值传递。

17.泛型有什么用?泛型擦除是什么?

泛型可以把类型当作参数一样传递,使得像一些集合类可以明确存储的对象类型,不用显示地强制转化(在没泛型之前只能是Object,然后强转)。

并且在编译期能识别类型,类型错误则会提醒,增加程序的健壮性和可读性。

泛型擦除指的指参数类型其实在编译之后就被抹去了,也就是生成的 class 文件是没有泛型信息的,所以称之为擦除。

18.说说强、软、弱、虚引用?

Java 根据其生命周期的长短将引用类型又分为强引用、软引用、弱引用、幻象引用。

  • 强引用:就是我们平时 new 一个对象的引用。当 JVM 的内存空间不足时,宁愿抛出 OutOfMemoryError 使得程序异常终止,也不愿意回收具有强引用的存活着的对象。

  • 软引用:生命周期比强引用短,当 JVM 认为内存空间不足时,会试图回收软引用指向的对象,也就是说在 JVM 抛出 OutOfMemoryError 之前,会去清理软引用对象,适合用在内存敏感的场景。

  • 弱引用:比软引用还短,在 GC 的时候,不管内存空间足不足都会回收这个对象,ThreadLocal中的 key 就用到了弱引用,适合用在内存敏感的场景。 -虚引用:也称幻象引用,之所以这样叫是因为虚引用的 get 永远都是 null ,称为get 了个寂寞,所以叫虚。

虚引用的唯一作用就是配合引用队列来监控引用的对象是否被加入到引用队列中,也就是可以准确的让我们知晓对象何时被回收。

19.Integer 缓存池知道吗?

因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127,可以根据通过设置JVM-XX:AutoBoxCacheMax=来修改缓存的最大值,最小值改不了。

实现的原理是int 在自动装箱的时候会调用Integer.valueOf,进而用到了 IntegerCache。

20.Exception 和 Error 的区别知道吗?

Exception 是程序正常运行过程中可以预料到的意外情况应该被开发者捕获并且进行相应的处理

Error 是指在正常情况下不太可能出现的情况,绝大部分的 Error 都会导致程序处于不正常、不可恢复的状态,也就是挂了。

所以不便也不需被开发者捕获,因为这个情况下你捕获了也无济于事。

Exception和Error都是继承了Throwable类,在Java代码中只有继承了Throwable类的实例才可以被throw或者被catch。

尽量不要捕获类似Exception这样通用的异常,而应该捕获特定的异常。

只在需要try-catch的地方try-catch,try-catch的范围能小则小

21.深拷贝和浅拷贝?

深拷贝:完全拷贝一个对象,包括基本类型和引用类型,堆内的引用对象也会复制一份。

浅拷贝:仅拷贝基本类型和引用,堆内的引用对象和被拷贝的对象共享。

所以假如拷贝的对象成员间有一个 list,深拷贝之后堆内有 2 个 list,之间不会影响,而浅拷贝的话堆内还是只有一个 list。

因此深拷贝是安全的,浅拷贝的话如果有引用对象则原先和拷贝对象修改引用对象的值会相互影响。

22.面向对象编程和面向过程编程的区别?

面向对象编程(Object Oriented Programming,OOP)是一种编程范式或者说编程风格。

把类或对象作为基本单元来组织代码,并且运用提炼出的:封装、继承和多态来作为代码设计指导。

面向过程编程是以过程作为基本单元来组织代码的,过程其实就是动作,对应到代码中来就是函数,面向过程中函数和数据是分离的,数据其实就是成员变量。

而面向对象编程的类中数据和动作是在一起的,这也是两者的一个显著的区别。

23.重载与重写的区别?

重载:指的是方法名相同,参数类型或者顺序或个数不同,这里要注意和返回值没有关系,方法的签名是名字和参数列表,不包括返回值。

重写( @Override ,是父子关系):指的是子类重写父类的方法,方法名和参数列表都相同,也就是方法签名是一致的。重写的子类逻辑抛出的异常和父类一样或者是其父类异常的子类,并且方法的访问权限不得低于父类。

24.什么是内部类,有什么用?

内部类顾名思义就是定义在一个类的内部的类,按位置分:在成员变量的位置定义,则是成员内部类,在方法内定义,则是局部内部类。

如果用 static 修饰则为静态内部类,还有匿名内部类。

一般而言只会用成员内部类、静态内部类和匿名内部类。

成员内部类可以使用外部类的所有成员变量以及方法,包括 private 的。

静态内部类只能使用外部类的静态成员变量以及方法。

匿名类常用来作为回调,使用的时候再实现具体逻辑来执行回调。

实际上内部类是一个编译层面的概念,像一个语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和外部类没有任何区别,所以在 JVM 中是没有内部类的概念的

一般情况下非静态内部类用在内部类和其他类无任何关联,专属于这个外部类使用,并且也便于调用外部类的成员变量和方法,比较方便。

静态外部类其实就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间。

25.说说 Java 的集合类吧?

常用的集合有 List(有序有索引可重复)、Set(无序无索引不可重复)、Map、Queue 等。

List 常见实现类有 ArrayList 和 LinkedList。

  • ArrayList 基于动态数组实现,支持下标随机访问,对删除不友好。(查询快)

  • LinkedList 基于双向链表实现,不支持随机访问,只能顺序遍历,但是支持O(1)插入和删除元素。(增删快)

Set 常见实现类有:HashSet、TreeSet、LinkedHashSet。

  • HashSet 其实就是 HashMap 包了层马甲,支持 O(1)查询,无序。

  • TreeSet 基于红黑树实现,支持范围查询,不过基于红黑树的查找时间复杂度是O(lgn),有序。

  • LinkedHashSet,比 HashSet 多了个双向链表,通过链表保证有序。

Map 常见实现类有:HashMap、TreeMap、LinkedHashMap

  • HashMap:基于哈希表实现,支持 O(1) 查询,无序。

  • TreeMap:基于红黑树实现,O(lgn)查询,有序。

  • LinkedHashMap:同样也是多了双向链表,支持有序,可以很好的支持 lru 的实现。

设置有序,并且重写LinkedHashMap中的 removeEldestEntry 方法,即可实现 lru。

这里有一点要提一下,如果你对某个东西比较熟悉就要在合适的地方抛出来。比如通过 LinkedHashMap 你还能延伸到 lru ,这表明你对 LinkedHashMap 有研究并且也知晓 lru,面试官自己可能都不清楚,会觉得你有点东西。

而且面试官基本会追问 lru 然后接着延伸,比如延伸到改进的 lru ,mysql 缓存中的 lru 等等,这就是通过你的引导把问题领域迁移到你自身熟悉的地方,这岂不美哉?如果你不熟悉,那少 bb。

Queue 常见的实现类有:LinkedList、PriorityQueue。

PriorityQueue:优先队列,是基于堆实现的,底层其实就是数组。

26.说说 ThreadLocal ?

ThreadLocal 本质是通过本地化资源来避免共享,也就是每个线程都有自己的本地私有化变量,这样每个线程访问自己属性即可,避免了多线程竞争导致的锁等消耗。

ThreadLocalMap 是采用线性探测的方式来解决 hash 冲突,所以要注意 ThreadLocal 的数量,因为这种冲突解决方式比较低效。

ThreadLocal 在 Entry 中作为 key 是弱引用,所以当外部对 ThreadLocal 的强引用消失之后,只剩下弱引用的 ThreadLocal 会被 GC 清除,这时候 Entry 中的 value 还在,但是已经访问不到了,所以称之为内存泄漏。

不过当调用 get 和 set 方法时,如果直接 hash 没中,开始线性探测,那么碰到 key 为 null 的节点才会清理掉。

当然更好的方式是显示的在用完之后调用 remove,这样就能及时清理。

既然弱引用会导致内存泄漏,那为什么还要弱引用?

首先如果 key 不用弱引用,那么当外部对 ThreadLocal 的强引用消失之后,由于 ThreadLocalMap 是这个线程的成员之一,所以这个线程还在,那么 ThreadLocalMap 就在,而 ThreadLocalMap 在,那么 Entry 肯定在,而 Entry 在那么强引用的 key 和 value 就肯定在。

所以如果 key 不用弱引用,那么 key 都无法被 GC 。

所以 key 用弱引用那么至少 key 这点内存是可以被省掉的,并且线性探测还能清一些 Entry。

其实发生内存泄漏的根本不在于 key 是弱引用,因为他们都属于一个线程的属性,所以线程活着它们就不能被 GC,这一条引用链是无法更改的。

然后现在都是用线程池,所以线程有可能长时间存活,因此就会逐渐堆积,导致内存满了。

所以这点需要明确。

与之相关的还有个 InheritableThreadLocal

这玩意可以理解为就是可以把父线程的 threadlocal 传递给子线程,所以如果要这样传递就用 InheritableThreadLocal ,不要用 threadlocal。

这里要注意,只会在线程创建的时会拷贝 InheritableThreadLocal 的值,之后父线程如何更改,子线程都不会首其影响。

27.同步、异步、阻塞、非阻塞 IO 的区别?

我们的程序和硬件之间隔了个操作系统,而为了安全考虑,Linux 系统分了:用户态和内核态。

在这个前提下,我们再明确程序要从磁盘(网卡)读数据的两个步骤:

  1. 数据从存储设备拷贝到内核缓存

  2. 数据再从内核缓存拷贝到用户空间

好了,现在咱们可以看看这几个概念了。

  • 同步I/O:指的是线程需要等待 2 执行完毕。

  • 异步I/O:指的是线程不需要等待 2 执行。

  • 阻塞I/O:指的是步骤 1 会阻塞,即线程需要阻塞等待 1 执行完毕。

  • 非阻塞I/O:指的是步骤 1 不会被阻塞,不需要阻塞等待。

所以平时还会有同步阻塞I/O,或者啥同步非阻塞I/O,就是步骤 1 和 2 的组合罢了。

我们再来理解一下,毕竟咱们这面霸系列不是背诵,是理解。

同步和异步指的是:是否需要等待方法的调用执行完毕。

这两个概念主要注重的是调用方式,同步和异步调用编码方式是不同的,同步其实就是一条道写下来,异步则是需要回调、事件等方式来实现后面的逻辑。

阻塞和非阻塞:一般用在底层系统调用身上,阻塞指的是线程未满足条件会被阻塞,进入 sleep 状态,即时间片还未到就让出 CPU,非阻塞则是计算未满足条件也直接返回。

所以阻塞是真的被阻塞住了,是在等待数据,是需要让出时间片的。

而同步的线程其实还是有时间片的,所以同步一般有个超时时间,计算超时之后就会返回继续执行后面的代码。

28.BIO、NIO、AIO?

BIO 指的是同步阻塞I/O,相信看了 27 题之后对这个同步阻塞很清晰了,就是等着。

在这种模型下只能是来一个连接用一个线程,连接多并发大的话服务器顶不住这么多线程的。

NIO 指的是同步非阻塞I/O,我们熟知的 IO 多路复用就是NIO,适合用在连接多、每次传输较为短的场景。

AIO 指的是异步I/O,调用了之后就不管了,数据来了自动会执行回调方法。

异步可以有效的减少线程的等待,减少了用户线程拷贝数据的那段等待,效率更高。

29.JDK8 有哪些新特性?

JDK8 较为重要和平日里经常被问的特性如下:

  • 用元空间替代了永久代。

  • 引入了 Lambda 表达式

  • 引入了日期类、接口默认方法、静态方法。

  • 新增 Stream 流式接口

  • 匿名内部类

然后相信你们对 HashMap 和 ConcurrentHashMap 有一定的准备,所以抛出来

  • 修改了 HashMap 和 ConcurrentHashMap 的实现(等着八股文之问)

  • 新增了 CompletableFuture 、StampedLock 等并发实现类。

像一些中间件异步化代码都用了 CompletableFuture 来实现,所以还是得做一些了解的,如果不熟悉这条就不用提了。

30.你都用过哪些 Java 并发工具类?

Semaphore、CyclicBarrier、CountDownLatch 三连。

当然 JUC 下面还有挺多,反正列几个说说就行,面试的时候切忌不要一股脑儿的把知道的都扔出来,这叫留白

Semaphore

这玩意叫信号量,广泛应用于各种操作系统中,相对于平日只允许一个线程访问临界区的 lock 和 synchronized 来说,信号量允许多线程同时访问一个临界区

原理就简单的理解为初始化一个数,如果来了一个线程则把数减一,如果减一之后数的值小于 0 则阻塞当前线程,移入一个阻塞队列中,否则允许执行。

当一个线程执行完毕之后将数加一,并唤醒阻塞队列中的一个等待线程。

实际是内部有个继承自 AQS 的 Sync 类,通过依托 AQS 的封装来实现功能。

主要用于流量的控制,比如停车场只允许停一定数量的车位。

简单示例如下:

 int count;
    final Semaphore semaphore   = new Semaphore(1); // 初始化信号量
    // 用信号量保证互斥    
    void addOne() {
      try {
          semaphore.acquire();   //对应down,计数减一
          count+=1;
        } catch (InterruptedException e) {
          e.printStackTrace();
        } finally {
          semaphore.release();  //对应up,计数加一
        }
    }

CyclicBarrier

从名字分析,这是一个可循环的屏障。

屏障的意思是:让一组线程都运行到同一个屏障点之后,线程会阻塞等待所有线程都达到这个屏障点,然后所有线程才得以继续执行。

它实际上是基于 ReentrantLock 和 Condition 的封装来实现这一功能的。

原理我先口述一下,因为面试官很有可能会问原理

首先设置了达到屏障的线程数量,当线程调用 await 的时候计数器会减一,如果计数器减一不等于 0 的时候,线程会调用 condition.await 进行阻塞等待。

如果计数器减一的值等于0,说明最后一个线程也到达了屏障,于是如果有 barrierCommand 就执行 barrierCommand ,然后调用 condition.signalAll 唤醒之前等待的线程,并且重置计数器,然后开启下一代。

当规定数量的线程到达屏障之后会把计数重置回去,并且开启了下一代,所以 CyclicBarrier 是可以循环使用的。

CountDownLatch

这个锁其实和 CyclicBarrier 有点类似,都是等待一个节点的到达,但是还是不太一样的。

CyclicBarrier 是各个线程等待阻塞所有线程都达到一个节点之后,所有线程继续执行。

CountDownLatch 是一个线程阻塞着等待其他线程到达一个节点之后才能继续执行,这个过程中其他线程是不会阻塞的

实现原理:内部又一个继承自 AQS 的 Sync 类,核心其实就是围绕一个整数 state。

初始化 state 的值,当调用一次 countDown 会把 state 的值减一,当 state 的值减到 0 的时候就会唤醒之前调用 await 等待的线程。

主要是依靠 AQS 封装的好,所以代码很少,原理也很清晰简单。

StampedLock

既然30题提到了这个,之前也专门写过文章分析,那刚好拿来讲讲。

可以认为是读写锁的“改进”版本。读写锁读写是互斥的,而 StampedLock 搞了个悲观读和乐观读,悲观读和写是互斥的,乐观读则不会。

搞个官方示例看下就很清晰了:

 class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
​
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();  //获取写锁
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp); //释放写锁
     }
   }
​
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //乐观读
     double currentX = x, currentY = y;
     if (!sl.validate(stamp)) { //判断共享变量是否已经被其他线程写过
        stamp = sl.readLock();  //如果被写过则升级为悲观读锁
        try {
          currentX = x;
          currentY = y;
        } finally {
           sl.unlockRead(stamp); //释放悲观读锁
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
​
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock(); //获取读锁
     try {
       while (x == 0.0 && y == 0.0) {
         long ws = sl.tryConvertToWriteLock(stamp);  //升级为写锁
         if (ws != 0L) {
           stamp = ws;
           x = newX;
           y = newY;
           break;
         }
         else {
           sl.unlockRead(stamp);
           stamp = sl.writeLock();
         }
       }
     } finally {
       sl.unlock(stamp);
     }
   }
 }

乐观锁就是获取判断一下,如果被修改了那么就升级为悲观锁。

但是 StampedLock 是不可重入锁,而且也不支持 condition 。并且如果线程使用writeLock() 或者readLock() 获得锁之后,线程还没执行完就被 interrupt() 的话,会导致CPU飙升,需要用 readLockInterruptibly 或者 writeLockInterruptibly

32.Java 中的阻塞队列用过哪些?

阻塞队列主要用来阻塞队列的插入和获取操作,当队列满了的时候阻塞队列的插入操作,直到队列有空位。当队列为空的时候阻塞队列的获取操作,直到队列有值。

常用在实现生产者和消费者场景,在笔试题中比较常见。

常见的有 ArrayBlockingQueue 和 LinkedBlockingQueue,分别是基于数组和链表的有界阻塞队列。

两者原理都是基于 ReentrantLock 和 Condition 。

都是有界阻塞队列两者有什么区别?

ArrayBlockingQueue 基于数组,内部实现只用了一把锁,可以指定公平或者非公平锁。

LinkedBlockingQueue 基于链表,内部实现用了两把锁,take 一把、put 一把,所以入队和出队这两个操作是可以并行的,从这里看并发度应该比 ArrayBlockingQueue 高。

还有 PriorityBlockingQueue 和 DelayQueue,分别是支持优先级的无界阻塞队列和支持延时获取的无界阻塞队列,如果你看过 DelayQueue 实现就会发现内部用的是 PriorityQueue。

还有 SynchronousQueue、 LinkedBlockingDeque 和 LinkedTransferQueue。

SynchronousQueue 前面线程池分析有提到过,它是不占空间的,入队比如等待一个出队,也就是生产者必须等待消费者拿货,无法把先把货存在队列。

LinkedBlockingDeque 是双端阻塞无界队列,就是队列的头尾都能操作,头尾都能插入和移除。

LinkedTransferQueue,相对于其他阻塞队列从名字来看它有 Transfer 功能,其实也不是什么神奇功能,一般阻塞队列都是将元素入队,然后消费者从队列中获取元素。

而 LinkedTransferQueue 的 transfer 是元素入队的时候看看是否已经有消费者在等了,如果有在等了直接给消费者即可,所以就是这里少了一层,没有锁操作。

33.用过Java 中哪些原子类?

原子类是 JUC 封装的通过无锁的方式实现的一系列线程安全的原子操作类。

上面截图的原子类主要分为五大类,我画个脑图汇总一下:

原子类的核心原理就是基于 CAS(Compare And Swap)。

CAS 简单的理解为:给予一个共享变量的内存地址,然后内存中应该的值(预期值)和新值,然后通过一条 CPU 指令来比较此内存地址上的值是否等于预期值,如果是则替换内存地址上的值为新值,如果不是则不予替换且换回。

也就是说硬件层面支持一条指令来实现这么几个操作,一条指令是不会被打断的,所以保证了原子性。

基本类型

可以简单的理解为通过基本类型原子类 AtomicBoolean、AtomicInteger 和 AtomicLong 就可线程安全地、原子地更新这几个基本类型。

数组类型

AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,简单的理解为可以原子化地更新数组内的每个元素,几个的差别无非就是数组里面存储的数据是什么类型。

引用类型

AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,就是对象引用的原子化更新。

差别在于 AtomicStampedReference 和 AtomicMarkableReference 可以避免 CAS 的 ABA 问题。

AtomicStampedReference 是通过版本号 stamp 来避免, AtomicMarkableReference 是通过一个布尔值 mark 来避免。

ABA 问题

因为 CAS 是将期望值和当时内存地址上的值进行对比,假设期望值是 1 ,地址上的值现在是 1,只不过中间被人改成了 2 ,然后又改回了1,所以此时你 CAS 操作去对比是可以替换的,你无法得知中间值是否改过,这种情况就叫 ABA 问题。

而解决 ABA 问题的做法就是用版本号,每次修改版本就+1,这样即使值是一样的但是版本不同,就能得知之前被改过了。

属性更新类型

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,是通过反射,原子化的更新对象的属性,不过要求属性必须用 volatile 修饰来保证可见性,看下源码,很直观。

累加器

上述的都是更新数据,而 DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder 主要用来累加数据。

首先 AtomicLong 也能累加,而 LongAdder 是专业累加,也只能累加,并发度更高,它通过分多个 cells 来减少线程的竞争,提高了并发度。

你可以理解为如果拿 AtomicLong 是实现累加就是一本本子,然后 20 个人要让本子上累加计数。

而 LongAdder 分了 10 个本子,20个人可以分别拿这 10 个本子来计数(减少了竞争,提高了并发度),然后最后的结果再由 10个本子上的数相加即可。

xxxAccumulator 和 xxxAdder 两者的区别?

xxxAccumulator 的功能比 xxxAdder 丰富,可以自定义累加方法,也可以设置初始值,按照注释上的解释 xxxAdder 等价于 new xxxAccumulator((x, y) -> x + y, 0L}。

所以可以说 xxxAdder 是 xxxAccumulator 的一个特例。

34.Synchronized 和 ReentrantLock 区别?

Synchronized 和 ReentrantLock 都是可重入锁,ReentrantLock 需要手动解锁,而 Synchronized 不需要。

ReentrantLock 支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁,可中断,支持条件判断。

Synchronized 不支持超时,非公平,不可中断,不支持条件。

总的而言,一般情况下用 Synchronized 足矣,比较简单,而 ReentrantLock 比较灵活,支持的功能比较多,所以复杂的情况用 ReentrantLock 。

至于说 Synchronized 性能不如 ReentrantLock 的,那都是 N 多年前的事儿了。

35.Synchronized 原理知道不?

Synchronized 的原理其实就是基于一个锁对象和锁对象相关联的一个 monitor 对象。

在偏向锁和轻量级锁的时候只需要利用 CAS 来操控锁对象头即可完成加解锁动作。

在升级为重量级锁之后还需要利用 monitor 对象,利用 CAS 和 mutex 来作为底层实现。

monitor 对象颞部会有等待队列和条件等待队列,未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。

然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁

所以才会有偏向锁和轻量级锁的优化,并且引入自适应自旋机制,来提高锁的性能。

关于 Synchronized 其实我写过两篇文章,看完这两篇文章你可以跟面试官说,你看过 JVM 源码,毫不夸张,因为就是从源码级别上分析的。

而且指明了一个几乎网上都错了的观点和一个常见的认知错误。

总而言之,看完之后对 Synchronized 基本上超越很多人了。

Synchronized 深入JVM分析

Synchronized 升级到重量级锁之后就下不来了?你错了!

36.ReentrantLock 的原理?

ReentrantLock 其实就是基于 AQS 实现的一个可重入锁,支持公平和非公平两种方式。

内部实现其实就是依靠一个 state 变量和两个等待队列:同步队列和等待队列。

利用 CAS 修改 state 来争抢锁。

争抢不到则入同步队列等待,同步队列是一个双向链表。

条件 condition 不满足时候则入等待队列等待,也是个双向链表。

是否是公平锁的区别在于:线程获取锁时是加入到同步队列尾部还是直接利用 CAS 争抢锁。

就是这么回事儿,理解起来应该不难,操心的我再画个图,嘿嘿。

37.说说 AQS 吧?

AQS 的原理其实就是上面提到的,这里就不再赘述了。

如果面试官问你为什么需要 AQS ,就这样回答。

AQS 将一些操作封装起来,比如入队等基本方法,暴露出方法,便于其他相关 JUC 锁的使用。

比如 CountDownLatch、Semaphore 等等。

就是起到了一个抽象,封装的作用。

38.读写锁知道不?

读写锁在 Java 中一般默认指的是 ReentrantReadWriteLock。

读写锁是有两把锁,分别是读锁和写锁。

除了读读操作不互斥之外,其他都互斥。

所以读很多写比较少的情况,用读写锁比较合适。

还有一点要注意,如果不是这种情况不要用读写锁,因为读写锁需要额外维护读锁的状态,所以如果读读操作不多还不如一般的锁

读写锁也是基于 AQS 实现的,再具体点的实现就是将 state分为了两部分,高16bit用于标识读状态、低16bit标识写状态。

就这样灵巧的通过一个 state 实现了两把锁,嘿嘿。

39.CAS 知道不?

CAS 就是 compare and swap,即比较并交换。

举个例子,我们经常有累加需求,比较一个值是否等于 1,如果等于 1 我们将它替换成 2,如果等于 2 替换成 3。

这种比较在多线程的情况下就不安全,比如此时同时有两个线程执行到比较值是否等于 1,然后两个线程发现都等于 1。

然后两个线程都将它变成了 2,这样明明加了两次,值却等于 2。

这种情况其实加锁可以解决,但是加锁是比较消耗资源的。

因此硬件层面就给予支持,将这个比较和交换的动作封装成一个指令,这样就保证了原子性,不会判断值确实等于 1,但是替换的时候值以及不等于 1了。

这指令就是 CAS。

CAS 需要三个操作数,分别是旧的预期值(图中的1),变量内存地址(图中a的内存地址),新值(图中的2)。

指令是根据变量地址拿到值,比较是否和预期值相等,如果是的话则替换成新值,如果不是则不替换

其实 33 题已经提过这个了,包括 ABA 问题,之所以再写一下是可以从我提供的思路跟面试官说。

不要一上来就三个操作数

你把遇到的场景(上面说的累加),然后多线程不安全,然后用锁不好,然后硬件提供了这个指令。

按这样的思路说出来,如果我是面试官,我一听就会觉得,小伙子可以。

40.说说线程的生命周期?

可以分为初始状态、可运行状态、终止状态和休眠状态四大类。

线程新建的时候就是初始状态,还未start。

可运行状态就是可以运行,可能正在运行,也可能正在等 CPU 时间片。

休眠状态分为三种,一种是等待锁的 blocked 状态,一种是等待条件的 waitting 状态,或者有时间限制的等待 timed_waitting 状态。

  • 等待条件的操作有:Object.wait、Thread.join、LockSupport.park()

  • 时间等待就是上面设置了timeout参数的方法,例如Object.wait(1000)。

终止状态就是线程结束执行了,可以是结束任务后的自动结束,也可以是产生了异常而结束。

41.什么是 JMM ?

JMM 即 Java Memory Model,Java 内存模型。

JMM 其实是一组规则,规定了一个线程的写操作何时会对另一个线程可见(JSR133)。

抽象的来看 JMM 会把内存分为本地内存和主存,每个线程都有自己的私有化的本地内存,然后还有个存储共享数据的主存。

由 JMM 来定义这两个内存之间的交互规则。

这里要注意本地内存只是一种抽象的说法,实际指代:寄存器、CPU 缓存等等。

总之 JMM 屏蔽了各大底层硬件的细节,是抽象出来的一套虚拟机层面的内存规范。

42.说说原子性、可见性、有序性?

原子性

指的是一个操作不会被中断,要么这个操作执行完毕,要么不会执行,不会有执行一半的存在。

可见性

指的是一个线程对某个共享变量进行了修改,则其他线程能立刻获取到最新值。

有序性

指的是编译器或者处理器会将指令进行重排,这种操作会影响多线程的执行顺序导致错误。

43.说说 Java 常见的垃圾收集器?

这题我不太想写答案,内容比较死板,建议还是看《深入理解JVM虚拟机》吧,不过那本书里面没有详细说 ZGC。

而 ZGC 我倒是写了一篇,可以看看呐。

美团面试官问我:ZGC 的 Z 是什么意思?

44.垃圾回收,如何判断对象是否是垃圾?

一共有两种方式,分别是引用计数和可达性分析。

引用计数有循环依赖的问题,但是是可以解决的。

可达性分析则是从根引用(GCRoots) 开始进行引用链遍历扫描,如果可达则对象存活,如果不可达则对象已成为垃圾。

所谓的根引用包括全局变量、栈上引用、寄存器上的等。

我之前也写过,详细的看这篇文章吧,看完之后这一块针对面试绝对没问题,而且已经超越了很多面试官。

深度揭秘垃圾回收底层,这次让你彻底弄懂它

45.你知道有哪些垃圾回收算法?

常见的就是:复制、标记-清除、标记整理。

标记-清除

标记-清除算法应该是最符合我们人一开始处理垃圾的思路的算法。

例如我们想清除房间的垃圾,我们肯定是先定位(对应标记)哪些是垃圾,然后把这些垃圾之后扔了(对应清除),简单粗暴,剩下的不是垃圾的东西我也懒得理,不管了哈哈哈。

但是,这算法有个缺点:

  1. 空间碎片问题,这样会使得比较大的对象要申请比较多的连续空间的时候申请不到,明明你空间还很足的。然后导致又一次GC。

复制算法

复制算法一般用于新生代,粗暴的复制算法就是把空间一分为二,然后将一边存活的对象复制到另一边,这样没有空间碎片问题,但是内存利用率太低了,只有 50%,所以 HotSpot 中是把一块空间分为 3 块,一块Eden,两块Survivor。

因为正常情况下新生代的大部分对象都是短命鬼,所以能活下来的不多,所以默认的空间划分比例是 8:1:1。

用法就是每次只使用Eden和一块Survivor,然后把活下来的对象都扔到另一块Survivor。再清理Eden和之前的那块Survivor。然后再把Eden和存放存活对象的那一块Survivor用来迎接新的对象。就等于每次回收了之后都会对调一下两个Survivor。

标记-整理算法

标记-整理算法的思路也是和标记-清除算法一样,先标记那些需要清除的对象,但是后续步骤不一样,它是整理,对就是像上面说的那些清除房间垃圾每次都会整理的人一样那么勤劳。

每次会移动所有存活的对象,且按照内存地址次序依次排列,也就是把活着的对象都像一端移动,然后将末端内存地址以后的内存全部回收。所以用了它也就没有空间碎片的问题了。

来吧,之前写的,看完之后差不多了。

炸了!一口气问了我18个JVM问题!

46.String,Stringbuffer,StringBuilder的区别?

String 是 Java 中基础且重要的类,并且 String 也是 Immutable 类的典型实现,被声明为 final class,除了 hash 这个属性其它属性都声明为 final。

因为它的不可变性,所以例如拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。

StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类,提供 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置。

它的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了 synchronized。但是保证了线程安全是需要性能的代价的。

在很多情况下我们的字符串拼接操作不需要线程安全,这时候StringBuilder登场了,StringBuilder是JDK1.5发布的,它和StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销

StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder ,底层都是利用可修改的 char 数组(JDK 9 以后是 byte 数组)。

所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者StringBuilder 的时候设置好 capacity,避免多次扩容的开销。

扩容要抛弃原有数组,还要进行数组拷贝创建新的数组。

47.happens-before 听过吗?

这个问题估计应该都是来自《深入理解Java虚拟机》这本书的。

happens-before 就是定义的一些规则,在一些特定场景下,一些操作会先行发生于另一些操作。

A先行发生于B,其实含义就是 A 操作得到的结果在 B 操作开始时可以得到,重点不在于 A 执行的时间比 B 早,而是 A 的结果是可以在 B 开始时候被 B 读取的。

这是 JVM 规定的有序性,你也可以认为写 JVM 的程序员需要按照这样的规则来实现 JVM。

如操作符合以下的规则就会按照下面的定义动作先行发生。

  • 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

  • 传递性规则:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

48.什么是锁的自适应自旋?

这里指的就是 Syncronized 在身为重量级锁时候的自旋。

具体指的是在重量级锁时,一个线程如果竞争锁失败会进行自旋操作,说白了就是执行一些无意义的执行,空转 CPU 等着锁的释放。

因为一些情况下可能线程刚被阻塞,锁就被释放了,这样开销就比较大,所以自旋在一定程度上是有优化的。

形象一点就像怠速停车和熄火的区别,如果等待时候很长(长时候都拿不到锁),那肯定熄火划算(阻塞)。

如果一会儿就要出发(拿到锁),那怠速停车(自旋)比较划算。

不过因为这个自旋次数不好判断,所以引入自适应自旋

说白了就是结合经验值来看,如果上次自旋一会儿就拿到锁,那这次多自旋几次,如果上次自旋很久都拿不到,这次就少自旋。

这就叫锁的自适应自旋。

49.JVM 内存区域划分

Java 虚拟机运行时数据区分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。

程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,会随线程消亡而自动回收,所以不需要管理。

而堆和方法区是线程共享的,所以垃圾回收器会关注这两个地方。

堆只要存放的就是平时 new 的对象。

方法区存放的就是加载的类型信息、即时编译(JIT)后的代码等。

50. 指令重排知道吗?

为了提高程序执行的效率,CPU或者编译器就将执行命令重排序。

原因是因为内存访问的速度比 CPU 运行速度慢很多,因此需要编排一下执行的顺序,防止因为访问内存的比较慢的指令而使得 CPU 闲置着。

CPU 执行有个指令流水线的概念,还有分支预测等,关于这个我之前写过一篇文章,可以看下CPU分支预测

总之为了提高效率就会有指令重排的情况,导致指令乱序执行的情况发生,不过会保证结果肯定是与单线程执行结果一致的,这叫 as-if-serial

不过多线程就无法保证了,在 Java 中的 volatile 关键字可以禁止修饰变量前后的指令重排。

51.final 和可以保证可见性吗?

不可以

你可能看到一些答案说可以保证可见性,那不是我们常说的可见性

一般而言我们指的可见性是一个线程修改了共享变量,另一个线程可以立马得知更改,得到最新修改后的值。

而 final 并不能保证这种情况的发生,volatile 才可以。

而有些答案提到的 final 可以保证可见性,其实指的是 final 修饰的字段在构造方法初始化完成,并且期间没有把 this 传递出去,那么当构造器执行完毕之后,其他线程就能看见 final 字段的值。

如果不用 final 修饰的话,那么有可能在构造函数里面对字段的写操作被排序到外部,这样别的线程就拿不到写操作之后的值。

来看个代码就比较清晰了。

public class YesFinalTest {
   final int a; 
   int b;
   static YesFinalTest testObj;
​
   public void YesFinalTest () { //对字段赋值
       a = 1;
       b = 2;
   }
​
   public static void newTestObj () {  // 此时线程 A 调用这个方法
       testObj = new YesFinalTest ();
   }
​
   public static void getTestObj () {  // 此时线程 B 执行这个方法
       YesFinalTest object = obj; 
       int a = object.a; //这里读到的肯定是 1
       int b = object.b; //这里读到的可能是 2
   }
}

对于 final 域,编译器和处理器要遵守两个重排序规则(参考自infoq程晓明):

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 初次读一个包含

  2. final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

所以这才是 final 的可见性,这种可见性和我们在并发中常说的可见性不是一个概念!

所以 final 无法保证可见性

52.锁如何优化?

要注意锁的粒度,不能粗暴的直接在方法外围定义锁,锁的代码块越小越好,像双检锁就是典型的优化。

不同场景定义不同的锁,不能粗暴的一把锁搞定,例如在读多写少的场景可以使用读写锁、写时复制等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值