设计模式之单例模式
单例模式在设计模式里面应该算是很简单的一种,一直以来都以为它的写法很简单,没有太大的难度,但是在看了一些大牛的视频和书籍讲解之后,发现自己其实在单例模式方面算是个小白,因为单例模式的写法居然有达十种之多,简直不可思议。
下面我们一起来聊一聊单例模式的这几种写法:
饿汉模式
饿汉模式和懒汉模式是初学设计模式时接触到的,当初以为就这两种写法,下面我们来看看饿汉模式。
代码如下:
public class HungaryDesignPattern {
private static final HungaryDesignPattern hungary = new HungaryDesignPattern();
private HungaryDesignPattern(){
System.out.println("=============初始化============");
}
public static HungaryDesignPattern getInstance(){
return hungary;
}
public static void main(String[] args) {
}
}
运行结果:
=============初始化============
饿汉模式就是这么简单,当类加载器对其加载后,类加载的准备阶段(因为static关键字)就会对其进行初始化,所以在这里main方法即使没有调用任何的初始化方法,仍然可以打印出构造方法的字符串。这样再通过getInstance()获取到的对象就是已经初始化好了的,可以直接使用。
这种模式,简单、明了,易于使用,也是线程安全的;
懒汉模式
基础懒汉模式
懒汉模式跟饿汉模式稍有不同,并不是一开始就完成初始化,而是当有其他线程要使用到它的实例时,才会去进行初始化,代码如下:
public class LazyDesignPattern_01 {
private static LazyDesignPattern_01 lazy=null;
private LazyDesignPattern_01(){}
public static LazyDesignPattern_01 getInstance(){
if(null==lazy){
lazy = new LazyDesignPattern_01();
}
return lazy;
}
}
这就懒汉模式的基础版本,有开发经验的人一眼就看出症结所在,这个单例并不是线程安全的。先分析下为什么不是线程安全的,比如现在有两个线程A和B,当A线程进入了getInstance()方法,且已经判断了lazy没有初始化,正准备对其进行初始化(初始化操作并不是原子操作),此时B线程也进来,也判断到lazy对象没有初始化,B线程也会对其进行初始化,就会导致两次初始化操作,后初始化会覆盖先初始化的数据。从而造成线程不安全问题。
既然这样不安全,那我对其加锁吧,也就是懒汉模式的另一种写法:
加锁懒汉模式
上面说了,第一种写法是不安全的,那我加上锁吧,代码如下:
public class LazyDesignPattern_02 {
private static LazyDesignPattern_02 lazy=null;
private LazyDesignPattern_02(){}
public static LazyDesignPattern_02 getInstance(){
if(null==lazy){
synchronized (LazyDesignPattern_02.class) {
lazy = new LazyDesignPattern_02();
}
}
return lazy;
}
}
这种写法的意图很明确,当我检测到对象没有初始化时,当前线程优先获取LazyDesignPattern_02 类的Class对象的锁,然后在同步块中对其进行初始化,这样应该就保证了线程的安全了吧。
想法是正确的,但是分析代码来看仍然还是有问题滴,首先在判断没有初始化的时候,实际上两个线程都有可能判断到对象没有被初始化,这时候A、B线程中的A先获取到了锁,B就只有阻塞,等待A释放锁,然后B进入同步块中,还是要进行初始化,最终覆盖A线程初始化的数据,所以这种写法仍然不是线程安全的。
那我们来继续完善下懒汉式的写法:
双重检测懒汉模式
这里不多说,先看代码:
public class LazyDesignPattern_03 {
private static LazyDesignPattern_03 lazy=null;
private LazyDesignPattern_03(){}
public static LazyDesignPattern_03 getInstance(){
if(null==lazy){
synchronized (LazyDesignPattern_03.class) {
if(null==lazy) {
lazy = new LazyDesignPattern_03();
}
}
}
return lazy;
}
}
该代码跟前面的代码没有太大区别,只是在同步块中再一次做了是否初始化的判断检测,所以这种方式称作双重检测。
这种写法的目的是建立在上一种写法上,上面已经说了A、B线程仍然有可能都去初始化对象,那我在同步块中再做一次判断不就可以了吗,这样可以在A初始完成后,B进入同步块发现对象已经初始化,那就不会再去进行初始化操作了,这样总可以避免上一种写法的问题了吧。
这里就需要更深入的分析虚拟机的操作指令了,虽然lazy = new LazyDesignPattern_03(),在代码中只有一行,但实际上再jvm中编译的指令并不只是一条,一起来看看下图中这部分代码的编译后的jvm指令:
对照上图,synchronized关键字编译后生成的字节码指令 会有一个“ monitorenter”和“ monitorexit”,在这两个指令之间的就是同步块, “if_acmpn”表示的是if判断语句,“new”指令才是初始化对象的开始,到初始化结束(monitorexit),中间还有4条指令。
当初始化完成后赋值给lazy,虽然A线程在工作内存中完成了赋值,但还要将赋值结果同步到主内存中,在这个过程中B线程很可能获取到的并不是赋值后的lazy,那么后续的if判断和同步块也会执行,也会出现非线程安全的情况。
那到底应该怎样才能让懒汉模式的单例能够保证线程安全呢,我们继续往下看:
双重检测+Volatile懒汉模式
直接看代码:
public class LazyDesignPattern_04 {
private static volatile LazyDesignPattern_04 lazy=null;
static{
System.out.println("=======================");
}
private LazyDesignPattern_04(){
System.out.println("============初始化==========");
}
public static LazyDesignPattern_04 getInstance(){
if(null==lazy){
synchronized (LazyDesignPattern_04.class) {
if(null==lazy) {
lazy = new LazyDesignPattern_04();
}
}
}
return lazy;
}
public static void main(String[] args) {
LazyDesignPattern_04.getInstance();
}
}
当前代码跟上面代码相比只是在lazy对象上添加了volatile关键字修饰,怎么就能够保证线程的安全了呢,这个要从volatile关键字的线程可见性讲起,简单来讲,就是A线程对lazy赋值后,B线程立即可见,那么在双重检测的情况下,就可以检测到lazy已经初始完毕,就不会再次进行初始化。
而且该关键字修饰的共享遍量,写操作会优先于读操作,就是说A线程在赋值成功后,立即同步到主线程,而B线程已经获取到的lazy副本已经失效,在判断时,必须重新读取主内存的中lazy,这样就保证了在后续的为空判断中不会成功,直接返回lazy对象。
最后这一种写法在多线程情况下可以保证安全了,已经是可以正常用作开发的了。后面我们在讲几种非正常情况初始化,从而导致单例模式遭到破坏的情况
破坏单例模式
反射破坏单例模式,以及修复
先来看一段代码:
public class BrokenLazyDesignPattern_01 {
private static volatile BrokenLazyDesignPattern_01 lazy=null;
private BrokenLazyDesignPattern_01(){}
public static BrokenLazyDesignPattern_01 getInstance(){
if(null==lazy){
synchronized (BrokenLazyDesignPattern_01.class) {
if(null==lazy) {
lazy = new BrokenLazyDesignPattern_01();
}
}
}
return lazy;
}
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
BrokenLazyDesignPattern_01 instance = BrokenLazyDesignPattern_01.getInstance();
Object o = Class.forName("com.fq.thread.design.BrokenLazyDesignPattern_01").newInstance();
BrokenLazyDesignPattern_01 lazy = (BrokenLazyDesignPattern_01)o;
System.out.println("instance.hashCode="+instance.hashCode());
System.out.println("lazy.hashCode="+lazy.hashCode());
}
}
打印结果:
instance.hashCode=356573597
lazy.hashCode=1735600054
由以上结果可知,直接调用getInstance()返回的对象,跟通过Class.forName反射得出的对象,并不是同一个对象,因此单例遭到破坏。那么怎么让反射的时候初始化的对象,跟直接调用getInstance()返回的对象是同一个呢,这里有两种写法:
静态内部类防止单例破坏
先看一段静态内部类的代码:
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){
System.out.println("=======StaticInnerClassSingleton========");
if(null!=SingletonHolder.holder){
throw new RuntimeException("不能通过反射初始化该对象");
}
}
public static StaticInnerClassSingleton getInstance(){
return SingletonHolder.holder;
}
private static class SingletonHolder{
static{
System.out.println("===========SingletonHolder==========");
}
private static final StaticInnerClassSingleton holder = new StaticInnerClassSingleton();
}
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
Object o = Class.forName("com.fq.thread.design.StaticInnerClassSingleton").newInstance();
StaticInnerClassSingleton lazy = (StaticInnerClassSingleton)o;
System.out.println("instance.hashCode="+instance.hashCode());
System.out.println("lazy.hashCode="+lazy.hashCode());
}
输出结果:
===========SingletonHolder==========
=======StaticInnerClassSingleton========
=======StaticInnerClassSingleton========
Exception in thread "main" java.lang.RuntimeException: 不能通过反射初始化该对象
at com.fq.thread.design.StaticInnerClassSingleton.<init>(StaticInnerClassSingleton.java:13)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at com.fq.thread.design.StaticInnerClassSingleton.main(StaticInnerClassSingleton.java:33)
这样就杜绝了反射初始化的意图,那到底是怎么解决的呢?
首先我们需要知道,同一个类加载器,对同一个类只会加载一次,而SingletonHolder类属于内部类,不能被外部访问,所以一开始并不会加载;
当调用getInstance()方法时,这里调用了内部类的静态常量holder,此时会触发SingletonHolder的加载,同时也会触发StaticInnerClassSingleton的初始化;
在StaticInnerClassSingleton的构造方法中null!=SingletonHolder.holder的判断是false,所以就完成了StaticInnerClassSingleton类的初始化;
此时Class.forName想通过反射newInstance()调用无参构造函数时,null!=SingletonHolder.holder的判断结果为true(因为SingletonHolder.holder属于类常量,且不能再次修改),返回异常。
枚举类防止单例破坏
通过枚举的方式来防止单例遭到破坏,看代码:
public enum EnumerationSingleton {
INSTANCE;
private int count=0;
private EnumerationSingleton(){
count=8;
}
public int getCount() {
return count;
}
public static void main(String[] args)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
EnumerationSingleton instance = EnumerationSingleton.INSTANCE;
System.out.println("count="+instance.getCount());
Object o = Class.forName("com.fq.thread.design.EnumerationSingleton").newInstance();
EnumerationSingleton lazy = (EnumerationSingleton)o;
System.out.println("instance.hashCode="+instance.hashCode());
System.out.println("lazy.hashCode="+lazy.hashCode());
}
}
运行结果:
count=8
Exception in thread "main" java.lang.InstantiationException: com.fq.thread.design.EnumerationSingleton
at java.lang.Class.newInstance(Class.java:427)
at com.fq.thread.design.EnumerationSingleton.main(EnumerationSingleton.java:27)
Caused by: java.lang.NoSuchMethodException: com.fq.thread.design.EnumerationSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 1 more
由此可以看出枚举类是线程安全的,即使反射也不能成功,因为枚举类没有构造函数,所以通过反射来获取会导致异常。
反序列化破坏单例模式
看下面的代码:
public class SerializableBrokenSingleton implements Serializable {
private static final SerializableBrokenSingleton singleton = new SerializableBrokenSingleton();
public int count = 1;
private SerializableBrokenSingleton(){
System.out.println("=============初始化============");
}
public static SerializableBrokenSingleton getInstance(){
return singleton;
}
public static void main(String[] args){
ObjectOutputStream objectOutputStream = null;
ObjectInputStream objectInputStream = null;
try {
SerializableBrokenSingleton instance = SerializableBrokenSingleton.getInstance();
objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.obj"));
objectOutputStream.writeObject(instance);
objectOutputStream.flush();
objectInputStream = new ObjectInputStream(new FileInputStream("out.obj"));
Object o = objectInputStream.readObject();
SerializableBrokenSingleton brokenSingleton = (SerializableBrokenSingleton)o;
System.out.println("instance.hashCode="+instance.hashCode());
System.out.println("brokenSingleton.hashCode="+brokenSingleton.hashCode());
objectInputStream.close();
}catch (Exception e){
}
}
}
打印结果:
=============初始化============
instance.hashCode=1735600054
brokenSingleton.hashCode=21685669
有打印结果可以看到反序列化之后的对象跟序列化之前的对象hashCode并不相同,所以对象并不相等。那有什么办法解决呢?只需要在类中添加一个方法即可,看代码:
public class SerializableBrokenSingleton implements Serializable {
private static final SerializableBrokenSingleton singleton = new SerializableBrokenSingleton();
public int count = 1;
private SerializableBrokenSingleton(){
System.out.println("=============初始化============");
}
public static SerializableBrokenSingleton getInstance(){
return singleton;
}
private Object readResolve(){
return singleton;
}
public static void main(String[] args){
ObjectOutputStream objectOutputStream = null;
ObjectInputStream objectInputStream = null;
try {
SerializableBrokenSingleton instance = SerializableBrokenSingleton.getInstance();
objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.obj"));
objectOutputStream.writeObject(instance);
objectOutputStream.flush();
objectInputStream = new ObjectInputStream(new FileInputStream("out.obj"));
Object o = objectInputStream.readObject();
SerializableBrokenSingleton brokenSingleton = (SerializableBrokenSingleton)o;
System.out.println("instance.hashCode="+instance.hashCode());
System.out.println("brokenSingleton.hashCode="+brokenSingleton.hashCode());
objectInputStream.close();
}catch (Exception e){
}
}
}
打印结果:
=============初始化============
instance.hashCode=1735600054
brokenSingleton.hashCode=1735600054
此时序列化前后对象的hashCode都是相当,表示对象都是相同的。那为什么添加了一个readResolve()方法并返回instance之后,对象就一样了呢 ,我们一起来分析下ObjectOutputStream,ObjectInputStream的代码:
先看ObjectInputStream的readObject:
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
...
} finally {
...
}
}
private Object readObject0(boolean unshared) throws IOException {
...
depth++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
...
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
...
case TC_ENDBLOCKDATA:
...
}
...
}
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
...
}
由以上代码我们可以知道,在调用desc.hasReadResolveMethod()时,调用的过程:首先是调用readObject0(),然后在里面判断类型为Object时,调用desc.hasReadResolveMethod()方法,再在该方法中判断desc.hasReadResolveMethod()(即是否有readResolve方法),如果有则通过** desc.invokeReadResolve(obj)**调用该方法,并返回对象。那么是在什么时候对hasReadResolveMethod属性赋值的呢,那就要看看ObjectOutputStream的writeObject():
public final void writeObject(Object obj) throws IOException {
..
writeObject0(obj, false);
...
}
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
...
desc = ObjectStreamClass.lookup(cl, true);
...
}
//ObjectStreamClass 类中的方法
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
...
entry = new ObjectStreamClass(cl);
...
}
//调用构造方法
private ObjectStreamClass(final Class<?> cl) {
...
if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class },
Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(
cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
...
}
所以最终得出结果,在将对象写入到文件的时候,生成ObjectStreamClass的时候,就会判断是否存在私有的readResolve方法,这样在反序列化的时候就可以调用该方法返回指定的对象。
容器单例模式
public class ContainerSingleton {
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
private ContainerSingleton(){}
public static Object getBean(String className){
synchronized (ioc){
if(!ioc.containsKey(className)){
Object obj = null;
try {
Object o = Class.forName(className).newInstance();
ioc.put(className, o);
}catch (Exception e){
}
return obj;
}else {
return ioc.get(className);
}
}
}
}
这种是模仿spring的ioc容器来实现的单例,类似于懒汉模式基础版本,也存在线程安全的问题,具体改进方式可以参考懒汉模式。
线程单例模式
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocal.get();
}
}
上面这种写法是通过ThreadLocal来实现,ThreadLocal表示线程独有,因此会在每个线程中都会生成一份独立的ThreadLocal对象,线程之间互不影响,所以是绝对线程安全的。
以上就是总结的单例模式的多种写法,有不正确的地方还希望广大网友指正。。。。