Java深入学习之单例模式
程序员文章站
2022-07-15 13:59:23
...
Java设计模式自学之单例模式
对于单例模式来说,最重要的就是私有构造函数,提供静态的实例化方法,所以单例模式的几个关键字:private 的构造函数,public static 提供的实例化方法,private static 的实体类成员变量,只要满足这三个基本的要素,就能实现单例模式。
1、懒汉模式
懒汉模式是最基本的单例模式之一,满足最基本的单例模式条件
以上代码就实现的懒汉单例模式,在初始化时不会创建对象实例,只要当getInstance被调用时才会创建。懒汉嘛,就是不到最后一刻不去真正做事,就是形容开始时不创建,要用的时候才创建实例。这种方式降低了初始时的内存空间,但后续的调用都需要判断的时间。
上面的懒汉模式在多线程下会出现安全问题,线程安全就是多个线程同时访问会出现安全性问题。比如一个房间,只有第一个进入的人才能留下自己的签名,其他人发现房间里有签名时,就不能再签名了。但如果两个人同时进门,两者发现房间里面没签名,都可以进入房间留下签名,就不符合房间只能存在一个签名的场景,所以这种情况下就需要给房间加锁。
懒汉模式变种一
这种情况是直接给房间门上把锁,第一个进入的人锁住门,后面的人就无法在他签名时再次进入房间。不算有多少人同时到达,但是只有一个人能拿到锁,所以不会存在线程安全问题。
这种加锁方式与在getInstance方法上加锁效果是一样的。但是这种方式有很大的效率问题,就是外面的人,都需要等里面的人把门打开,才能发现房间里是不是有签名了,因为门被锁住时,大家不知道里面的人会不会真的留下签名,如果人一旦过多,就会等待(Java跟人不同,不会因为房间被锁住就退走,而是继续等待),其实大家只需要看下里面是否有签名,而看签名的时间比起锁门再解锁的时间根本不是一个层级上的,这样就造成效率过低。
懒汉模式变种二
另外一种方式就是给房间加上一扇窗户。大家进入房间时,先通过窗户看下,里面有没有签名,有的话直接退走,没有的话就继续往门方向走。由于窗户是大家都可以看的,需要等待的只有在通过窗户发现里面没签名的人才会继续往前走,比起上一种人人都需要等待时间消耗少非常多。
这种方式也叫做双重锁校验机制。理论上这种方式是完全没问题的,但是由于Java虚拟机加载机制的问题,在JDK1.5之后,需要给成员变量添加一个volatile关键字,它可以保证变量的可见性和有序性,被它修饰的变量的值,不会被本地线程存储,所有对其修饰变量的操作都是直接共享内存,保证多个线程可以同时可见。
即:private volatile static Single3 single = null,这样才能完整的保证这种方式是在任何情况下都是具备正确性的。
补充:Java虚拟机加载过程主要可以分为三个步骤:装载、连接和初始化
1、装载阶段:就是将java文件对应的.class文件以二进制数据加载到JVM中,加载的实例和类位于堆中,然后创建一个java.lang.Class对象来封装类信息数据结构,而类信息则被放到方法区中。以“类的全限定名+ClassLoader实例ID”来标明这些类,这里也会涉及到“双亲委派模型”机制。
2、连接阶段:这个阶段分为三个步骤,
3、初始化:初始化类中的静态变量,并执行类中的static代码、构造函数。初始化顺序:
在JVM中存在一个很大的问题就是加载过程并不是时时都是有序的,内存模型中允许存在“无序写入”。比如:single = new Single3();这段代码就不是原子性操作,在JVM处理时大概可以分为三步。
* 第一步,给Single3分配内存;
* 第二步,初始化Single3的构造器;
* 第三步,将single对象执行已经分配内存空间Single3**(此时,single已经不是null,而是有空间的内存)**;
但是由于该语句并不是原子操作,所以这三步执行在JVM实际的顺序可能是1,3,2这样执行,所以,如果线程B执行到第一个if(null == single)时而线程A恰好是在1,3,2中的3时,线程B拿到的也不是一个非null的对象,而是一个没有值得内存空间,导致直接返回,但是实际上是没有数据的,从而造成了线程安全的问题。
2、饿汉模式
当然,如果在懒汉模式中初始化成员变量是直接就进行赋值,是什么情况呢
在类初始化的时候,按照JVM初始化方式,Single4 single = new Single4() 在类初始时就会被实例化,但是可能等到程序结束也不会被调用,所以这种方法称为“饿汉模式”。这种方式根据JVM本身的特性,不会存在线程安全问题,但是在初始化时就会占据内存空间。
3、内部类方式
将饿汉模式和懒汉模式总体结合归纳下,两种都有一定的利弊性,而另外一种方式完美的融合两种方式的问题。
这种方式即实现了延迟加载,也保证了线程安全,所以是用得最多的一种方式。
4、单例方法屏蔽
在使用单例模式中,可以通过其他途径创建其他实例:
第一种;通过反射构造单例对象,反射时可以使用setAccessible方法来突破private的限制,获取到新的实例,而打破单例模式;
第二种;通过反序列化构造单例对象。
测试代码:
测试结果分别为false,true,false;由此可见,以上两种方法都能破坏单例模式。所以,要完完全全的实现单例模式,必须需要进行一些完善,比如序列化时,添加readResolve方法,返回获取的instance对象;而反射,则需要跟多的权限处理等。
对于单例模式来说,最重要的就是私有构造函数,提供静态的实例化方法,所以单例模式的几个关键字:private 的构造函数,public static 提供的实例化方法,private static 的实体类成员变量,只要满足这三个基本的要素,就能实现单例模式。
1、懒汉模式
懒汉模式是最基本的单例模式之一,满足最基本的单例模式条件
public class Single1 { private static Single1 single = null; //私有构造函数 private Single1(){ } //提供实例化方法 public static Single1 getInstance(){ if (null == single) { single = new Single1(); } return single; } }
以上代码就实现的懒汉单例模式,在初始化时不会创建对象实例,只要当getInstance被调用时才会创建。懒汉嘛,就是不到最后一刻不去真正做事,就是形容开始时不创建,要用的时候才创建实例。这种方式降低了初始时的内存空间,但后续的调用都需要判断的时间。
上面的懒汉模式在多线程下会出现安全问题,线程安全就是多个线程同时访问会出现安全性问题。比如一个房间,只有第一个进入的人才能留下自己的签名,其他人发现房间里有签名时,就不能再签名了。但如果两个人同时进门,两者发现房间里面没签名,都可以进入房间留下签名,就不符合房间只能存在一个签名的场景,所以这种情况下就需要给房间加锁。
懒汉模式变种一
这种情况是直接给房间门上把锁,第一个进入的人锁住门,后面的人就无法在他签名时再次进入房间。不算有多少人同时到达,但是只有一个人能拿到锁,所以不会存在线程安全问题。
public class Single2 { private static Single2 single = null; private Single2(){ } public static Single2 getInstance(){ //使用锁关键字 synchronized(Single2.class){ if (null == single) { single = new Single2(); } } return single; } }
这种加锁方式与在getInstance方法上加锁效果是一样的。但是这种方式有很大的效率问题,就是外面的人,都需要等里面的人把门打开,才能发现房间里是不是有签名了,因为门被锁住时,大家不知道里面的人会不会真的留下签名,如果人一旦过多,就会等待(Java跟人不同,不会因为房间被锁住就退走,而是继续等待),其实大家只需要看下里面是否有签名,而看签名的时间比起锁门再解锁的时间根本不是一个层级上的,这样就造成效率过低。
懒汉模式变种二
另外一种方式就是给房间加上一扇窗户。大家进入房间时,先通过窗户看下,里面有没有签名,有的话直接退走,没有的话就继续往门方向走。由于窗户是大家都可以看的,需要等待的只有在通过窗户发现里面没签名的人才会继续往前走,比起上一种人人都需要等待时间消耗少非常多。
public class Single3 { private static Single3 single = null; private Single3(){ } public static Single3 getInstance(){ //先判断是否为空,相当于一扇窗户 if (null == single) { synchronized(Single3.class){ if (null == single) { single = new Single3(); } } } return single; } }
这种方式也叫做双重锁校验机制。理论上这种方式是完全没问题的,但是由于Java虚拟机加载机制的问题,在JDK1.5之后,需要给成员变量添加一个volatile关键字,它可以保证变量的可见性和有序性,被它修饰的变量的值,不会被本地线程存储,所有对其修饰变量的操作都是直接共享内存,保证多个线程可以同时可见。
即:private volatile static Single3 single = null,这样才能完整的保证这种方式是在任何情况下都是具备正确性的。
补充:Java虚拟机加载过程主要可以分为三个步骤:装载、连接和初始化
1、装载阶段:就是将java文件对应的.class文件以二进制数据加载到JVM中,加载的实例和类位于堆中,然后创建一个java.lang.Class对象来封装类信息数据结构,而类信息则被放到方法区中。以“类的全限定名+ClassLoader实例ID”来标明这些类,这里也会涉及到“双亲委派模型”机制。
2、连接阶段:这个阶段分为三个步骤,
- 步骤一:验证,验证这个class文件里面的二进制数据是否符合java规范,并且符合当前JVM;
- 步骤二:准备,为该类的静态变量分配内存空间并赋值为默认值;
- 步骤三:解析,将类的常量池中的符号引用解析为直接引用,也可以在用到相应的引用时再解析。
3、初始化:初始化类中的静态变量,并执行类中的static代码、构造函数。初始化顺序:
- 1. 为静态变量分配内存并赋值或者执行静态代码块;
- 2. 为非静态属性分配内存并赋值;
- 3. 构造方法;
- 4. 执行非静态代码块 或 静态方法(都是调用了才加载)。
在JVM中存在一个很大的问题就是加载过程并不是时时都是有序的,内存模型中允许存在“无序写入”。比如:single = new Single3();这段代码就不是原子性操作,在JVM处理时大概可以分为三步。
* 第一步,给Single3分配内存;
* 第二步,初始化Single3的构造器;
* 第三步,将single对象执行已经分配内存空间Single3**(此时,single已经不是null,而是有空间的内存)**;
但是由于该语句并不是原子操作,所以这三步执行在JVM实际的顺序可能是1,3,2这样执行,所以,如果线程B执行到第一个if(null == single)时而线程A恰好是在1,3,2中的3时,线程B拿到的也不是一个非null的对象,而是一个没有值得内存空间,导致直接返回,但是实际上是没有数据的,从而造成了线程安全的问题。
2、饿汉模式
当然,如果在懒汉模式中初始化成员变量是直接就进行赋值,是什么情况呢
public class Single4 { private static Single4 single = new Single4(); private Single4(){ } public static Single4 getInstance(){ return single; } }
在类初始化的时候,按照JVM初始化方式,Single4 single = new Single4() 在类初始时就会被实例化,但是可能等到程序结束也不会被调用,所以这种方法称为“饿汉模式”。这种方式根据JVM本身的特性,不会存在线程安全问题,但是在初始化时就会占据内存空间。
3、内部类方式
将饿汉模式和懒汉模式总体结合归纳下,两种都有一定的利弊性,而另外一种方式完美的融合两种方式的问题。
public class Single5 { /** * 加载类时,内部类实例化与外部类没有绑定关系, * 所以只有在调用时才会加载,实现延迟加载 * 而且内部类初始化时就实例化了变量,并且只有一次,保证了线程安全 */ private static class Instance{ private static Single5 single = new Single5(); } public static Single5 getInstance(){ return Instance.single; } }
这种方式即实现了延迟加载,也保证了线程安全,所以是用得最多的一种方式。
4、单例方法屏蔽
在使用单例模式中,可以通过其他途径创建其他实例:
第一种;通过反射构造单例对象,反射时可以使用setAccessible方法来突破private的限制,获取到新的实例,而打破单例模式;
public static Single5 refCopy() throws Exception{ //通过反射获取构造函数 Constructor<Single5> con = Single5.class.getDeclaredConstructor(); con.setAccessible(true); //获取实例 Single5 refTest = con.newInstance(); return refTest; }
第二种;通过反序列化构造单例对象。
/** * 序列化克隆 * @return * @throws Exception */ public static Single5 deepCopy() throws Exception{ ByteArrayOutputStream os = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(getInstance()); InputStream is = new ByteArrayInputStream(os.toByteArray()); ObjectInputStream ois = new ObjectInputStream(is); Single5 test = (Single5) ois.readObject(); return test; }
测试代码:
Single5 test = Single5.getInstance(); Single5 test1 = Single5.getInstance(); Single5 test3 = Single5.deepCopy(); Single5 refTest = Single5.refCopy(); System.out.println(refTest.equals(test)); System.out.println(test == test1); System.out.println(test3.equals(test1));
测试结果分别为false,true,false;由此可见,以上两种方法都能破坏单例模式。所以,要完完全全的实现单例模式,必须需要进行一些完善,比如序列化时,添加readResolve方法,返回获取的instance对象;而反射,则需要跟多的权限处理等。
public Object readResolve(){ return getInstance(); }
上一篇: Java深入学习之死锁
下一篇: linux虚拟机安装