java之单例模式

首先,先看个例子吧

 1 public class SimpleSingleton
 2 {
 3     // 静态属性,属于类 ,对于任何对象都是唯一的
 4     private static SimpleSingleton simpleSingleton;
 5     // 私有化构造方法,让外部不可以随意的创建对象
 6     private SimpleSingleton() {};
 7     // 提供公共的方法,获取唯一的实例对象
 8     public static SimpleSingleton getInstance() 
 9     {
10         if(null == simpleSingleton) // 如果有多个线程同时都,通过了非null的判断,就会出现多线程环境下的安全问题,就不会保证单例了
11         {
12             simpleSingleton = new SimpleSingleton();
13         }
14         return simpleSingleton;
15     }
16 }

上面的代码,是典型的懒汉式单例模式,在单线程的情况下,完全是没有问题的,但是在多线程的环境下,就很难保证对象的单例了。那应该如何去保证在多线程的环境下的单例呢?相信学过了,学过多线程的基本知识的都知道在可能会产生并发的地方,加上同步就好,即synchronized(同步方法或者同步代码块)。基于这点基本的知识,那就改一改上面的代码吧。

 1 public class SimpleSingleton
 2 {
 3     // 静态属性,属于类 ,对于任何对象都是唯一的
 4     private static SimpleSingleton simpleSingleton;
 5     // 私有化构造方法,让外部不可以随意的创建对象
 6     private SimpleSingleton() {};
 7     // 提供公共的方法,获取唯一的实例对象
 8     public synchronized static SimpleSingleton getInstance() 
 9     {
10         if(null == simpleSingleton) 
11         {
12             simpleSingleton = new SimpleSingleton();
13         }
14         return simpleSingleton;
15     }
16 }

这样,在getInstance的方法之上,加上synchronized就可以保证,在多线程环境下的单例了。看着好像没什么问题了,再看看呢?有没有想过为什么要用同步方法实现上述代码,而没有用同步代码块实现上边的代码?那同步方法和同步代码块有有什么区别呢?一般来说,同步方法因为应用在方法上,同步涉及的范围或者说作用域广一点,那么就意味着包括的代码执行的就多,那消耗的时间就多,别忘了,在同步方法被锁定以后,别的线程想要访问该方法,就要一直苦苦的在门外守候,哎,这样整个性能就被拉低了;而说到同步代码块,一般会放在方法内,相对管的区域就小一点,性能自然就好一点咯。看看就看出问题来了,太不经考验了,来来接着改。注意,只要在“变”的地方加上同步代码块,也就是simpleSingleton = new SimpleSingleton(); 同步方法每次都要获取锁判断一次,其实只是在第一次创建实例的时候真正起作用,同步代码块,可以避免这样的消耗

 1 public class SimpleSingleton
 2 {
 3     // 静态属性,属于类 ,对于任何对象都是唯一的
 4     private static SimpleSingleton simpleSingleton;
 5     // 私有化构造方法,让外部不可以随意的创建对象
 6     private SimpleSingleton() {};
 7     // 提供公共的方法,获取唯一的实例对象
 8     public static SimpleSingleton getInstance() 
 9     {
10         if(null == simpleSingleton) // 如果有多个线程同时都,通过了非null的判断,就会出现多线程环境下的安全问题
11         {
12             synchronized (SimpleSingleton.class)
13             {
14                 simpleSingleton = new SimpleSingleton();
15             }
16         }
17         return simpleSingleton;
18     }
19 }

好了好了,这下该改好了吧,那回忆一下,第一段代码看看是不是有类似的问题啊,现在假设有A,B两个线程,都通过了(null == simpleSingleton)的判断,如果A线程首先获得了锁,进入同步代码块,创建SimpleSingleton的实例,赋值给静态变量simpleSingleton,释放锁,并返回实例;这时B线程也获得了锁,同样再次创建了SimpleSingleton的实例,这样也就出现了问题,单例的多线程安全再次被破坏。这里就需要双重加锁(double kill)的机制了,继续改:

 1 public class SimpleSingleton
 2 {
 3     // 静态属性,属于类 ,对于任何对象都是唯一的
 4     private static SimpleSingleton simpleSingleton;
 5 
 6     // 私有化构造方法,让外部不可以随意的创建对象
 7     private SimpleSingleton()
 8     {
 9     }
10 
11     // 提供公共的方法,获取唯一的实例对象
12     public static SimpleSingleton getInstance()
13     {
14         if (null == simpleSingleton) 
15         {
16             synchronized (SimpleSingleton.class)            //1
17             {
18                 if (null == simpleSingleton)                //2
19                 {
20                     simpleSingleton = new SimpleSingleton();//3
21                 }
22             }
23         }
24         return simpleSingleton;
25     }
26 }

如果涉及到JVM底层的时候,上述的双重加锁还是可能有问题的

双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。

双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。

我们应该明白一点,JVM在创建实例的看似是一部操作,其实是分成好几步来完成的,也就是说从JVM的底层角度来说,创建对象并非原子性的操作过程,一般来说,双重加锁的单例模式不会有什么问题,但也有万一的可能。创建对象的步骤:1.分配内存空间 2.初始化构造器 3.将对象指向分配的内存地址  按照正常的逻辑,是没有问题。但是由于JVM会对字节码进行调优,其中就有一条调优:调整指令的执行顺序  如果2和3两步骤颠倒就会出现,返回一个未完成初始化构造器的未完成初始化的对象


 

无序写入(例证) 

为解释该问题,需要重新考察上述代码段的 //3 行。此行代码创建了一个 SimpleSingleton 对象并初始化变量 simpleSingleton 来引用此对象。这行代码的问题是:在 SimpleSingleton构造函数体执行之前,变量 simpleSingleton可能成为非 null 的。

什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设上述代码段执行以下事件序列:

  1. 线程 1 进入 getInstance() 方法。

  2. 由于 simpleSingleton 为 null,线程 1 在 //1 处进入 synchronized 块。 

  3. 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null 

  4. 线程 1 被线程 2 预占。

  5. 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 simpleSingleton 引用返回给一个构造完整但部分初始化了的 SimpleSingleton对象。 

  6. 线程 2 被线程 1 预占。

  7. 线程 1 通过运行 SimpleSingleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

此事件序列发生在线程 2 返回一个尚未执行构造函数的对象的时候。


 

那有没有好的解决方案呢?且听我慢慢说来。。。。。。

方案一:就要说到了volatile这关键词了,简单讲一讲不过多涉及,上边不是因为JVM优化导致陷入不可的错误中,如果对于属性加上这个关键词就表示此属性不需要优化,也就不会出现创建对象创建一般的情况了,加了volatile就等于禁止JVM进行自动指令重排序优化,并且强制保证当前线程对于变量的任何写入操作对于其他线程都是即时可见的,总之volatile会强行将对该变量的所有读和取操作绑定成一个不可拆分的动作

 1 public class SimpleSingleton
 2 {
 3     // 静态属性,属于类 ,对于任何对象都是唯一的,添加volatile避免指令优化重排
 4     private volatile static SimpleSingleton simpleSingleton;
 5 
 6     // 私有化构造方法,让外部不可以随意的创建对象
 7     private SimpleSingleton()
 8     {
 9     }
10 
11     // 提供公共的方法,获取唯一的实例对象,双重加锁机制
12     public static SimpleSingleton getInstance()
13     {
14         if (null == simpleSingleton) 
15         {
16             synchronized (SimpleSingleton.class)
17             {
18                 if (null == simpleSingleton)
19                 {
20                     simpleSingleton = new SimpleSingleton();
21                 }
22             }
23         }
24         return simpleSingleton;
25     }
26 }

 

 注意:volatile关键字在JDK1.5及以后的版本才具有上述的意义

方案二:私有静态内部类

 1 public class InnerSingleton
 2 {
 3     //私有化构造方法
 4     private InnerSingleton() {}
 5     //提供公共的方法获取实例
 6     public static InnerSingleton getInstance() 
 7     {
 8         return InnerSingletonInstance.innerSingleton;
 9     }
10     //创建私有静态内部类,以InnerSingleton的声明静态属性
11     private static class InnerSingletonInstance 
12     {
13         static InnerSingleton innerSingleton = new InnerSingleton();
14     }
15 }

 解释一下:因为类的静态属性只有在第一次类加载的时候初始化,而且只有一次,所有可以保证是单例的,并且整个类初始化的过程是JVM保证整个过程是同步的,所以就不用担心会因为中途终止,而出现上述只初始化一部分的情况。

方案三:饿汉式单例模式

 1 public class HungrySingleton
 2 {
 3     //私有静态属性,类加载即初始化完成,且只有一次
 4     private static HungrySingleton hungrySingleton = new HungrySingleton();
 5     //私有化构造方法
 6     private HungrySingleton() 
 7     {
 8         
 9     }
10     //提供公共的方法获取实例
11     public static HungrySingleton getInstance() 
12     {
13         return hungrySingleton;
14     }
15 }
 //或者在静态代码块中,初始化静态属性
1
public class HungrySingleton 2 { 3 //私有静态属性,类加载即初始化完成,且只有一次 4 private static HungrySingleton hungrySingleton; 5 //在静态代码块中初始化,静态属性,同样保证单例 6 static 7 { 8 hungrySingleton = new HungrySingleton(); 9 } 10 //私有化构造方法 11 private HungrySingleton() 12 { 13 14 } 15 //提供公共的方法获取实例 16 public static HungrySingleton getInstance() 17 { 18 return hungrySingleton; 19 } 20 }

 

接下来看几个特殊的单例模式

1.序列化和反序列化的单例模式

 1 public class SerializableSingleton implements Serializable
 2 {
 3 
 4     /** 
 5      * @Fields serialVersionUID : 注释内容
 6      */ 
 7     private static final long serialVersionUID = 1L;
 8     
 9     private SerializableSingleton() 
10     {
11         
12     }
13     
14     private static class SerializableSingletonInstance
15     {
16         private static SerializableSingleton serializableSingleton = new SerializableSingleton();
17     }
18     public static SerializableSingleton getInstance() 
19     {
20         return SerializableSingletonInstance.serializableSingleton;
21     }
22     
23 }

序列化和反序列化测试代码

 1 public class SerialAndDeserialTest
 2 {
 3     public static void main(String[] args)
 4     {
 5         SerializableSingleton singleton = SerializableSingleton.getInstance();
 6         File file = new File("Singleton.txt");
 7         //序列化
 8         try
 9         {
10             FileOutputStream fos = new FileOutputStream(file);
11             ObjectOutputStream oos = new ObjectOutputStream(fos);
12             oos.writeObject(singleton);
13             oos.close();
14             fos.close();
15             System.out.println(singleton.hashCode());
16 
17         }
18         catch (FileNotFoundException e)
19         {
20             e.printStackTrace();
21         }
22         catch (IOException e)
23         {
24             e.printStackTrace();
25         }
26         //反序列化
27         try
28         {
29             FileInputStream fis = new FileInputStream(file);
30             ObjectInputStream ois = new ObjectInputStream(fis);
31             SerializableSingleton singleton2 = (SerializableSingleton) ois.readObject();
32             ois.close();
33             fis.close();
34             System.out.println(singleton2.hashCode());
35         }
36         catch (IOException e)
37         {
38             e.printStackTrace();
39         }
40         catch (ClassNotFoundException e)
41         {
42             e.printStackTrace();
43         }
44     }
45 }

通过上面的测代码可以发现:上面的代码序列化和反序列化的对象不是同一个对象了,为了解决这个问题,需要我们遵循约定,在单例模式中,添加固定的方法readResolve()保证反序列化后的单例,修改后的代码如下所示:

public class SerializableSingleton implements Serializable
{
    /** 
     * @Fields serialVersionUID : 注释内容
     */ 
    private static final long serialVersionUID = 1L;
    
    private SerializableSingleton() 
    {
        
    }
    
    private static class SerializableSingletonInstance
    {
        private static SerializableSingleton serializableSingleton = new SerializableSingleton();
    }
    public static SerializableSingleton getInstance() 
    {
        return SerializableSingletonInstance.serializableSingleton;
    }
    //该方法会在反序列化的时候被调用
    protected Object readResolve() throws ObjectStreamException
    {
        System.out.println("----Hello,readResolve---");
        return SerializableSingletonInstance.serializableSingleton;
    }
}

 3.枚举类型的单例模式

 1 public class EnumSingleton
 2 {
 3     private enum GetSingleton
 4     {
 5         INSTANCE;//声明一个实例
 6         private Singleton singleton;
 7         private GetSingleton()
 8         {
 9             singleton = new Singleton();//枚举类型的构造方法,在类加载的时候,初始化
10         }
11         public Singleton getInstance()
12         {
13             return singleton;
14         }
15     }
16     //提供公共的方法获取单例
17     public static Singleton getInstance() 
18     {
19         return GetSingleton.INSTANCE.getInstance();
20     }
21     
22 }
23 //要获取单例的类
24 class Singleton
25 {
26 }

 

总结

单例模式几点保证:

    1.Singleton最多只有一个实例,在不考虑反射强行突破访问限制的情况下。

    2.保证了并发访问的情况下,不会发生由于并发而产生多个实例。

    3.保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。

本文参考:http://www.zuoxiaolong.com/blog/article.ftl?id=124 

                  http://blog.csdn.net/chenchaofuck1/article/details/51702129

                  http://blog.csdn.net/cselmu9/article/details/51366946

目前认识有限,未完待续。。。。。。。。。。。。。。。。

posted @ 2017-09-27 00:27  朱洪昌  阅读(326)  评论(0编辑  收藏  举报