引入
volatile
关键字的特点:
- volatile可以保证可见性;
- 不能保证原子性
- 由于内存屏障,可以保证避免指令重排的现象产生
面试官:那么你知道在哪里用这个内存屏障用得最多呢?
答:单例模式。
单例模式
最重要的特点:构造器私有,单例类的实例对象最多只有一个(也可能没有)
饿汉式、DCL懒汉式、静态内部类
1)饿汉式
饿汉式是最简单的一种单例模式,就是无论在这个类的外部需不需要调用这个对象,都会先创建一个实例。定义一个static方法getInstance()
,当需要调用这个对象的时候,可以通过这个static方法来获取这个单例对象。
弊端:一开始就创建对象,对象中的成员变量和成员方法需要分配空间,如果后续不会用到这个对象,就会浪费空间。
/**
* 饿汉式单例,一开始就在类的内部创建唯一的实例
*/
public class Hungry {
/**
* 一开始就创建对象,对象会有成员变量和成员方法,可能会浪费空间
*/
private byte[] data1=new byte[1024*1024];
private byte[] data2=new byte[1024*1024];
private byte[] data3=new byte[1024*1024];
private byte[] data4=new byte[1024*1024];
//私有化构造器
private Hungry(){
}
//在内部创建单例模式的唯一实例
private final static Hungry hungry = new Hungry();
//定义static方法,以便获取单例模式中的唯一实例
public static Hungry getInstance(){
return hungry;
}
}
2)DCL懒汉式
懒汉式是指在获取这个类的对象时,创建一个唯一的实例,之后再获取就不会创建新的实例,而是返回的最开始创建的实例对象。
懒汉式需要细分为以下三种情况,主要看代码和注释进行理解。
①单线程模式下,根据以下代码可以看到只需要先堆单例对象进行声明,然后通过判断是否为空决定是创建唯一的实例还是直接返回唯一的实例即可。
/**
* 懒汉式单例模式,需要的时候再创建唯一的单例对象
*/
public class LazyMan {
private LazyMan(){
}
//声明唯一的单例对象
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
但是以上模式在多线程模式中,如果几个线程在进行判断之后都进入了if语句内部,就会创建多个实例,违背了单例模式的原则,因此需要做出改进。
②多线程情况下,通过synchronized
关键字实现双重检测锁处理并发的问题。
/**
* 懒汉式单例模式,需要的时候再创建唯一单例对象
*/
public class LazyMan {
private LazyMan(){
}
//声明唯一的单例对象
private static LazyMan lazyMan;
//双重检测锁模式 简称DCL懒汉式
public static LazyMan getInstance(){
//需要加锁
if(lazyMan == null){
synchronized (LazyMan.class){
if(lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
这里采用双重检测锁,把类作为锁的对象,确保不同线程不会同时进入创建唯一单例的语句,从而就确保只会创建唯一一个实例。对于synchronized,这里并未直接添加到方法上,因为锁机制是很影响效率的,因此尽可能减小锁的范围,从而提高程序的运行效率。
然而,这个单例模式还是不完美的,在创建唯一实例的语句中,可能会发生重排序,导致出现对象指向空内存的问题。
双重检测加锁保证的原子性,是指只有一个线程能创建实例,其他线程无法创建第二个实例。
指令重排序的理解:
指令重排序涉及JVM的知识,我们平时编写的一条Java语句,Java虚拟机解释的过程中,其实是由几条机器指令实现的,而为了提高代码效率,在确保代码结果不变的情况下,就会对机器指令进行重排序。
示例
对于
lazyMan = new LazyMan();
这条Java语句,底层实现分成了三个步骤。
1、分配内存空间
2、执行构造方法,初始化对象
3、把这个对象指向这个空间
假如经过重排序后,变成如下的执行顺序
1、分配内存空间
2、把这个对象指向这个空间
3、执行构造方法,初始化对象
当三条语句都执行完的时候,结果是不会有任何问题的。但是假设有那么一种极端情况,有可能线程A创建了一个实例。虚拟机只执行了分配空间,对象地址引用这两步,这时线程B过来发现对象已经被创建了,经过语句if (lazyMan == null)
之后,lazyMan存储了一个引用地址,因此肯定是不为空的,就是直接返回一个Instance,但是获取到的这个对象是没有被初始化的。
这个问题更像是一个安全问题,获取的一个空对象,那么后续操作这个实例的时候就很可能会发生空指针的错误。
这时候回过头来看我们在引用时提到的volatile
关键字,其中一个作用就是
- 由于内存屏障,可以保证避免指令重排的现象产生,那么我们第三种情况就是通过volatile实现禁止进行指令重排。
/**
* 懒汉式单例模式,需要的时候再创建唯一单例对象
*/
public class LazyMan {
private LazyMan(){
}
//声明唯一的单例对象,并且通过volatile关键字修饰
private volatile static LazyMan lazyMan;
//双重检测锁模式 简称DCL懒汉式
public static LazyMan getInstance(){
//需要加锁
if(lazyMan == null){
synchronized (LazyMan.class){
if(lazyMan == null){
lazyMan = new LazyMan();
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 就有可能出现指令重排问题
* 比如执行的顺序是1 3 2 等
* 我们就可以添加volatile保证指令重排问题
*/
}
}
}
return lazyMan;
}
}
3)静态内部类
//静态内部类
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}
因为在Java中有反射机制的存在,所以单例模式是不安全的
因为在Java中有反射机制的存在,所以单例模式是不安全的
因为在Java中有反射机制的存在,所以单例模式是不安全的
具体看以下的代码,本质上就是在反射时调用setAccessible(true)
方法可以突破构造方法、成员方法、成员变量的权限设置,从而破坏单例模式。
/**
* 懒汉式单例模式,需要的时候再创建唯一单例对象
*/
public class LazyMan {
private static boolean key = false;
private LazyMan(){
synchronized (LazyMan.class){
if (key==false){
key=true;
}
else{
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println(Thread.currentThread().getName()+" ok");
}
//声明唯一的单例对象
private volatile static LazyMan lazyMan;
//双重检测锁模式 简称DCL懒汉式
public static LazyMan getInstance(){
//需要加锁
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
/**
* 1、分配内存空间
* 2、执行构造方法,初始化对象
* 3、把这个对象指向这个空间
*
* 就有可能出现指令重排问题
* 比如执行的顺序是1 3 2 等
* 我们就可以添加volatile保证指令重排问题
*/
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
//Java中有反射
// LazyMan instance = LazyMan.getInstance();
Field key = LazyMan.class.getDeclaredField("key");
key.setAccessible(true);
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //无视了私有的构造器
LazyMan lazyMan1 = declaredConstructor.newInstance();
key.set(lazyMan1,false);
LazyMan instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(lazyMan1);
System.out.println(instance == lazyMan1);
}
}
要解决的反射机制对单例模式带来的问题,需要通过枚举。
4)枚举
enum 是什么? enum本身就是一个Class 类,并且Java规定反射是不能破坏枚举的。
然后我们看枚举类的源代码,这里会有一个默认的无参构造函数,那这时是不是说明我们可以在反射时调用这个无参构造验证上述的Java规定,反射不能破坏枚举类呢?
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//java.lang.NoSuchMethodException: com.kuang.single.EnumSingle.<init>()
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
执行完代码后会出现下述错误,但是这并不是我们预期的结果,这里显示没有这个无参构造方法。
NoSuchMethodException: com.kuang.single.EnumSingle.<init’>();
那么接下来,打开命令行窗口,通过java -p EnumSingle.class
命令反编译这个枚举类,结果如下图所示,很明显可以看到倒数第三行显示出这个类有一个无参构造函数,但是这和上述的内容一样,肯定不符合我们的预期结果。
那么究竟是怎么回事呢,我们要借助jad反编译工具,通过jad -sjava EnumSingle.class
命令反编译这个这个枚举类的源文件。
枚举类型经过jad工具反编译源码,并非无参构造函数,而是有参构造函数,第一个参数为String型,第二个参数为int型。
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/kuang/single/EnumSingle, name);
}
//并非无参构造函数
private EnumSingle(String s, int i)
{
super(s, i);
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
因此我们修改之前的代码,如下图所示,把传入的参数null改为String.class和int.class。
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor =
//注意这里传入的参数为String.class和int.class
EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
最终结果如下图所示,符合预期结果。
经过分析,对于单例模式,使用枚举类最终可以有效防止反射机制的破坏!!!