如果说语法是一个编程语言的骨架,那么关键字就是骨架的关节。
在Java中有各种功能的关键字,最常用的int,break,public这些关键字都是在编程过程中必不可少的。
本文从面试提问的角度聊一下final、volatile、synchronized这三个关键字。
1.开篇
为什么就聊final、volatile、synchronized这三个关键字呢?并不是说其他关键字不重要,而是在笔者面试的过程中,这三个关键字是问的最多,问的最详细的。其实比起这三个关键字,我觉得class关键字更重要啊,没有class我们根本无法写出一个Java程序嘛,但就算没有使用volatile、final、synchronized关键字也一样可以写出一个能正常运行的程序。
就是因为这三个关键字在日常编程中使用相对较少,但是他们在某些场景有起着至关重要的作用,因此对这些关键字的理解是一个不错的考察点。
2.final
final关键字在Java程序体系设计和并发方面使用较多。被final关键字标记的对象都具有一种状态保持特性,比如被final关键字修饰的基本变量不能改变值,被final修饰的引用变量不能更换引用的对象,被final标记的类不能被继承等等。final就像是一把锁,锁住了对象当前的状态。
下面通过几个面试题更深入的了解final吧
final、finally与finalize的区别
(ps:我没有在一次面试中遇到这个问题,但是每个面试资料书中都有这个问题,这三个关键字除了长得像了点根本没有其他联系好吧)
final关键字是用来修饰Java中的变量,方法,类的。修饰变量表示这个变量引用的地址不能变化,修饰方法表示这个方法不可覆盖,修饰类表示这个类无法继承。但是当我们用final关键字修饰一个方法然后在子类中写了一个一样的方法时却不会报错,这并不是你的JDK版本太新以至于final有了新的语义,而是子类的方法是作为子类独有的一个新方法出现的,和父类毫无关系。就像在《Java编程思想》中说的一样,每个private修饰的方法默认就是final修饰的,他们无法被覆盖,子类可以重新写一个相同的方法,不过在JVM眼中这是两个毫无关系的方法。
finally关键字是Java异常处理流程中的一个模块,表示无论是否发生异常都会执行的模块,即使在try块、catch块中已经由return关键字finally代码块依旧会执行(这里有一道经典的笔试题,就是try代码块中出现异常,catch块和finally块中都有return的代码,只是返回的值不同,问最后得到的是哪个值)。在日常编码中常常会在finally代码块中进行一些资源关闭操作。
finalize关键字是JVM在回收死亡对象时调用的方法。它看起来和C++的析构函数很像,其实差别很大。C++析构函数在对象内存被回收时就会调用,函数执行完毕内存就释放了。但是finalize方法不一样,要想要JVM执行它,首先需要这个类重写该方法。然后JVM在第一次回收这个对象发现该方法可以被执行时,执行该方法,然后可能在下次GC中回收该对象。为什么是可能呢?因为我们可以在finallize方法中“自救”。大家都知道一般Java程序中被回收的对象一般都是没有引用可以获取到该对象,但是如果在finalize方法中把当前对象加入到一个存活状态的map容器中,这样它就复活了。由于JVM的GC优先级很低,即使显示调用也无法保证它能立刻执行GC,因此finalize方法的执行其实是非常难以控制的。
final关键字的应用场景
1.标识一个常量,防止有些代码不小心修改变量值。
2.标识一个类以,告诉使用者不要继承这个类。比如String类就是用final标识的。
3.在高并发情况下增加安全性。JVM执行字节码的过程中会进行一些指令重排序以便获得更高的性能,如果一个类中有些变量的初始化在构造函数中初始化并且恰好被指令重排序了,那么在高并发情况下,线程可能看到一个值的未初始化的结果。这时如果这个值是个固定值就可以用final关键字修饰,JVM保证被final修饰的变量初始化一定不会因指令重排序逃逸到构造函数外,这样就能保证每个线程只能拿到该变量初始化结束后的正确值。
2.synchronized
问到Java,必问并发;问到并发,必问synchronized。synchronized关键字是Java并发领域不可或缺的一部分。
直接通过几个面试题说这个关键字吧。
用过synchronized吗?说说吧
(ps:这种问题覆盖的面很广,在回答这种问题的时候可以先想想围绕这个主题你能说出哪几方面的内容,然后按点说出,既不容易说漏,也让面试官听的不是很混乱)
说到synchronized。我从三个方面来聊它。
1.synchronized可以做什么?在编码中,我们在一个方法上加上synchronized关键字,表示这个这个实例(静态方法则是类)方法同一时刻只能有一个线程在执行。只有该线程退出方法其他线程才能执行该方法的代码。synchronized关键字可以修饰一个代码块,通过给一个对象加锁来控制执行代码块线程的数量,只有持有锁的线程释放锁,其他线程才能获取锁进而执行代码块。锁代码块的效率通常比直接给方法加锁效率高。
2.synchronized的底层原理。synchronized关键字在字节码层面上实际是通过entermoniter和exitmoniter两个指令实现的。临界区的代码被这两个指令给“包围”,并且禁止指令重排序破坏这一结构。线程到来时,获取锁,执行entermoniter指令,临界区代码执行完毕,执行exitmoniter指令,释放锁。
3.synchronized优化。synchronized关键字使用的锁是一种悲观锁,也是重量级锁。悲观锁,顾名思义就是不管有没有人来竞争都要上锁,如果锁竞争特别少的话这无疑是性能低下的。所以Java在编译的时候会通过锁消除,锁粗话等操作进行一个性能优化(ps:这里可以看我以前的一篇博客 Java锁优化)。再说synchronized关键字使用的是一个重量级锁,重量级锁是通过操作系统提供互斥量完成的,有一个线程阻塞和恢复的操作,这两个操作都需要内核态和用户态的相互转换,因此开销也是很大,在这方面,JVM虚拟机还是提供了优化,通过偏向锁,轻量级来分化锁的等级,以达到减少重量级锁的使用。(ps:这里可以看我之前的博客 Java中的偏向锁,轻量级锁和重量级锁)。
围绕synchronized的代码题
面试时(尤其是视频面试)手写代码是非常重要的一个模块。手写代码不仅仅考验算法,数据结构,有些题也要考察我们在其他方面的知识(ps:有一次面试官让设计一个外卖系统,写出各个实体类,这是一种设计题)。例如这道题:
class A {
private int a = 0;
public void foo() {
...
new Thread(new Runnable() {
public void run() {
...
a = 1;
...
}
}).start();
System.out.print(a);
}
}
这是一道考验线程同步基础的题,面试官先问我a打印的结果是什么。对线程同步有一些基础的同学都知道结果可能是1可能是0。然后面试官又让我在a = 1之后尽快打印其值,也就是先让主线程等待一会儿,然后等a = 1执行完毕再运行。这就是通过synchronized关键字和wait()notify()方法来做的。
类似的题还有很多,一般只要和线程有关的,我们都可以围绕synchronized关键字去解决。
当然,synchronized关键字也不是唯一的解决方案,就拿上面这道题来说,如果用自旋等待的方式去完成并给面试官讲明白自旋和加索的区别,优略和使用场景。相信能给你的面试加更多的分。(ps:这是我面试北京抖音时的一道题,面经里有更详细的说明,大家感兴趣可以去看看,传送门)
volatile
volatile关键字在编码时相比上面两个用的很少(其实并不少,阅读一下Java的JDK源码就能发现用的特别多)。一般大家只知道volatile关键字修饰的变量具有操作原子性。这里要先说明一下。volatile变量的原子性操作,只有读取和赋值两个操作。一般的++操作和其他计算操作都是非原子性的。
还是通过面试问题来聊聊这个关键字吧。
了解volatile关键字吗?说说吧
又是一个大篇幅的题,这类题要求对知识的有系统且清晰的了解。
说到volatile关键字,我从两个方面来聊聊它
1.volatile关键字的用法,一般我们通过volatile关键字去修饰一个变量,之后该变量的读写操作将具有原子性。
2.volatile关键字能解决什么问题。在JDK5之后volatile关键字的语义得到了增强,使得我们对volatile关键字修饰的变量的读写操作具有了和释放锁获取锁操作一样的功能。具体来说就是因为Java的内存模型大体上分为线程共享内存和线程独占内存,每个线程使用共享变量时都是从共享内存中拷贝一份变量值到独占内存中使用的,这就导致当线程更改了共享变量的值以后其他线程并不能立即可见。但是如果把对共享变量的操作代码进行加锁操作,那么在释放锁时就会把线程独占内存中的共享变量的最新值刷新到共享内存中。volatile关键字在语义增强后具有了和锁操作近似的功能。于是在对一个volatile变量的写操作之后,JVM就会把独占内存中共享变量的值都刷新到共享内存中。
这大大减少了多种情景下锁的使用,Java中的乐观锁就是通过valotile关键字配合CAS操作实现的。因此在某些特定的场景下,使用volatile关键字可以代替使用锁以达到提升效率的目的。
volatile关键字在JDK源码中的应用
volatile关键字是Java替代锁的一个解决方案。在并发包中大量使用了volatile关键字。例如用来代替HashTable的ConcurentHashMap中就使用volatile,ConcurrentHashMap与HashTable一样是线程安全的,但它不像HashTable那样大量的使用synchronized关键字。而是使用volatile关键字和自旋等待替代加锁操作,并且把每个key都用volatile修饰,得益于volatile关键字和JMM提供的happen-before语义,所有的volatile变量的读必须在写之后,因此提高了并发性能。
其他诸如重入锁中的关键变量state也是volatile修饰的,使用原理也是自旋CAS来替代锁。
小结
这篇文章只是一个大体的总结,其实这三个关键字从底层实现到使用位置都有着很大的学问。威力强大也伴随着很大的风险。这里推荐大家有时间看看《Java并发编程的艺术》和《Java编程思想》这两本。我今天也是收获了人生中第一个大厂offer,最后祝大家面试顺利。
背景音乐:Beneath the Mask——目黑将司