旁白:一般的面试都是从最简单基本的问题开始。
面试官:请在黑板上写出一个线程安全的单例模式的例子。
面试者:
其实线程安全的实现有很多种,根据业务场景可以new一个实例作为私有静态成员变量,这样程序一启动,实例就生成,私有化构造函数,利用公用的静态函数getInstance返回实例。这种预加载的是能保证线程安全的但是如果不是确定会被使用,会造成内存的浪费,所以可以将实例放到私有静态类中作为成员变量。下面只写一种利用锁机制来保证的懒加载方法。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null ) { synchronized (Singleton. class ) { if (singleton == null ) { singleton = new Singleton(); } } } return singleton; } } |
旁白:从这个例子上我能想到的知识点主要有三个
☆ volatile 关键字,可深入到Java VM内存相关
☆ synchronized 关键字,可深入到Java锁机制,高并发相关
☆ new 关键字,可深入到Java VM类加载机制相关
但是面试官一开始可能要先考察一下面试者是否真的理解自己写的代码
面试官:你写的这个程序是怎么保证线程安全的?
面试者:将类的构造方法私有起来,外部调用进行初始化的时候只能通过调用getSingleton这个静态方法来获得实例,静态方法是整个Java虚拟机中只有一个实例。在创建的时候首先进行非空判断,这时候如果实例不存在,对整个类进行加锁同步,为了避免过程中非空状态的改变,同步块内再进行一次判断,如果不存在实例则创建实例返回。使用volatile关键字,下次访问这个方法就能直接看到实例的最新非空状态,直接返回实例。
面试官:volatile 起到了什么作用?
面试者:volatile这个英文单词意思是易变的,用在多线程中来同步变量。Java的对象都是在内存堆中分配空间。但是Java有主内存和线程自己独有的内存拷贝。对于没有volatile修饰的局部变量,线程在运行过程中访问的是工作内存中的变量值,其修改对于主内存不是立即可见。而volatile修饰的值在线程独有的工作内存中无副本,线程直接和主内存交互,修改对主内存立即可见。
面试官:synchronized起到了什么作用?
面试者:锁定对象,限制当前对象只能被一个线程访问。
面试官:synchronized里你传Singleton.class这个参数,起到什么作用,换成别的行不行?
面试者:对当前类加锁,使得这个代码块一次只能被一个线程访问。这里Singleton.class可以换成一个常量字符串或者自己定义一个内部静态Object。
面试官:那传Singleton.class,常量字符串,自己定义一个内部静态Object有区别吗?
面试者:因为这是一个静态方法,相当于一个概念上的类锁,所以在这里起到的效果是一样的。但是如果是原型模式,或者直接每个类都是new出来的,实例不同的话,在一个非静态方法里加这三种锁,这时是一个对象锁,因为Singleton.class或者是静态的一个Object或者是JVM只存一份的字符串常量,这些对象线程间是共享的,会对所有的实例的同步块都加同一把锁,每个实例访问到此对象的同步代码块都会被阻塞。但是如果这时synchronized的参数是this,或者是内部new出来的一个内部非静态Object,则各个实例拥有不同的锁,访问同一个代码相同同步块也是互不干扰。只有实例内部使用了同一个对象锁才会同步等待。
面试官:那你知道synchronized关键字实现同步的原理吗?
面试者:synchronized在Java虚拟机中使用监视器锁来实现。每个对象都有一个监视器锁,当监视器锁被占用时就会处于锁定状态。
线程执行一条叫monitorenter的指令来获取监视器锁的所有权。如果此监视器锁的进入数为0,则线程进入并将进入数设置为1,成为线程所有者。如果线程已经拥有该锁,因为是可重入锁,可以重新进入,则进入数加1.如果线程的监视器锁被其他线程占用,则阻塞直到此监视器锁的进入数为0时才能进入该锁。
线程执行一条叫monitorexit的指令来释放所有权。执行monitorexit的必须是线程的所有者。每次执行此指令,线程进入数减1,直到进入数为0。监视器锁被释放。
面试官:你刚才提到的可重入锁是什么概念,有不可重入锁吗?
面试者:我说的可重入锁是广义的可重入锁,当然jdk1.5引入了concurrent包,里面有Lock接口,它有一个实现叫ReentrantLock。广义的可重入锁也叫递归锁,是指同一线程外层函数获得锁之后,内层还可以再次获得此锁。可重入锁的设计是为了避免死锁。sun的corba里的mutex互斥锁是一种不可重入锁的实现。自旋锁也是一种不可重入锁,本质上是一种忙等锁,CPU一直循环执行"测试并设置"直到可用并取得该锁,在递归的调用该锁时必然会引起死锁。另外,如果锁占用时间较长,自旋锁会过多的占用CPU资源,这时使用基于睡眠原理来实现的锁更加合适。
面试官:你刚才提到了concurrent包,它里面有哪些锁的实现?
面试者:常用的有ReentrantLock,它是一种独占锁。ReadWriteLock接口也是一个锁接口,和Lock接口是一种关联关系,它返回一个只读的Lock和只写的Lock。读写分离,在没有写锁的情况下,读锁是无阻塞的,提高了执行效率,它是一种共享锁。ReadWriteLock的实现类为ReentrantReadWriteLock。ReentrantLock和ReentrantReadWriteLock实现都依赖于AbstractQueuedSynchronizer这种抽象队列同步器。
面试官:锁还有其他维度的分类吗?
面试者:还可以分为公平锁和非公平锁。非公平锁是如果一个线程尝试获取锁时可以获取锁,就直接成功获取。公平锁则在锁被释放后将锁分配给等待队列队首的线程。
面试官:AQS是什么?
面试者:AQS是一个简单的框架,这个框架为同步状态的原子性管理,线程的阻塞和非阻塞以及排队提供了一种通用机制。表现为一个同步器,主要支持获取锁和释放锁。获取锁的时候如果是独占锁就有可能阻塞,如果是共享锁就有可能失败。如果是阻塞,线程就要进入阻塞队列,当状态变成可获得锁就修改状态,已进入阻塞队列的要从阻塞队列中移除。释放锁时修改状态位及唤醒其他被阻塞的线程。
AQS本质是采用CHL模型完成了一个先进先出的队列。对于入队,采用CAS操作,每次比较尾节点是否一致,然后插入到尾节点中。对于出队列,因为每个节点缓存了一个状态位,不满足条件时自旋等待,直到满足条件时将头节点设置为下一个节点。
面试官:那知道这个队列的数据结构吗?
面试者:这个队列是用一个双向链表实现的。
面试官:你刚才提到AQS是一种通用机制,那它还有哪些应用?
面试者:AQS除了刚才提到的可重入锁ReentrantLock和ReentrantReadWriteLock之外,还用于不可重入锁Mutex的实现。java并发包中的同步器如:Semphore,CountDownLatch,FutureTask,CyclicBarrier都是采用这个机制实现的。
旁白:既然问到了并发工具包中的东西,每个都可以引出一堆,但是基本原理已经问出来了,其他的问下去没什么意思。转向下一个问题。
面试官:你黑板上写的实例是通过new对象创建出来的,还可不可以采用别的方法来创建对象呢?
面试者:还可以使用class类的newInstance方法,Constructor构造器类的newInstance方法,克隆方法和反序列法方法。
面试官:两种newInstance方法有没有区别?
面试者:
☆ Class类位于java的lang包中,而构造器类是java反射机制的一部分。
☆ Class类的newInstance只能触发无参数的构造方法创建对象,而构造器类的newInstance能触发有参数或者任意参数的构造方法来创建对象。
☆ Class类的newInstance需要其构造方法是共有的或者对调用方法可见的,而构造器类的newInstance可以在特定环境下调用私有构造方法来创建对象。
☆ Class类的newInstance抛出类构造函数的异常,而构造器类的newInstance包装了一个InvocationTargetException异常。
Class类本质上调用了反射包构造器类中无参数的newInstance方法,捕获了InvocationTargetException,将构造器本身的异常抛出。
面试官:类加载的时候,自己定义了一个类和java自己的类类名和命名空间都一样,JVM加载的是哪一个呢?
面试者:调用的是java自身的,根据双亲委派模型,最委派Bootstrap的ClassLoader来加载,找不到才去使用Extension的ClassLoader,还找不到才去用Application的ClassLoader,这种机制利于保证JVM的安全。
面试官:你刚才提到的java的反射机制是什么概念?
面试者:java的反射机制是在运行状态中,对于任何一个类,都能够知道它所有的属性和方法;对于任何一个对象,都能够调用它的任何一个方法和属性。这种动态的获取信息和动态调用对象的方法的功能就是java的反射机制。它是jdk动态代理的实现方法。
面试官:java还有没有其他的动态代理实现?
面试者:还有cglib动态代理。
面试官:这两种动态代理哪个比较好呢?
面试者:AOP源码中同时使用了这两种动态代理,因为他们各有优劣。jdk动态代理是利用java内部的反射机制来实现,在生成类的过程中比较高效,cglib动态代理则是借助asm来实现,可以利用asm将生成的类进行缓存,所以在类生成之后的相关执行过程中比较高效。但是jdk的动态代理前提是目标类必须基于统一的接口,所以有一定的局限性。
旁白:面试者都已经提到AOP了,那么接下来横向,纵向,怎样都能问出一大堆问题,就不赘述。基于上面问题,读者也可以自己画出一棵知识树,然后就能找到能对答如流的终极方案:就是基本都没超过《深入理解java虚拟器》《java并发编程实践》这两本书,大学学过的《数据结构与算法》《编译原理》掌握的好也可以在面试中加分哦。
原文地址