zoukankan      html  css  js  c++  java
  • 设计模式—单例模式

    一、单例模式介绍

      单例模式模式在程序的设计领域被广泛使用,例如设计一个单例模式对象来读取配置文件信息等。单例模式的主要特点是在内存中只存在一份对象,该对象的生命周期从创建到应用的结束。

    其中单例模式又分为懒汉式以及饿汉式的单例模式,他们各自有各自的优缺点,具体使用哪种方式需要根据对象的特点来做出选择。懒汉式的单例模式使用的是时间换空间,其只在第一次使用的时候创建对象,因此,使用时创建对象需消耗

    时间性能。饿汉式使用的是空间换

    时间,在类第一次被装在时就创建对象,不管有没有使用该对象,因此在以后的访问中提高了对象的访问性能。下面就各种单例模式的写法做出归纳总结:

    二、非线程安全的单例模式

    public class Singleton {
    	private static Singleton single;
    
    	private Singleton()
    	{
    
    	}
    
    	public static Singleton getInstance()
    	{
    		if(single == null)
    			single = new Singleton();
    		return single;
    	}
    
    }
    

     上面这段代码写的是一个懒汉式的单例模式,但是很显然,在多线程的环境下,这段代码是不安全的。因为在single = new Singleton()这个语句被翻译成机器码时会有多个执行指令。首先,会创建Singleton对象,其次调用其构造方法,实例化成员变量,最后将创建的对象的堆上地址复制给single变量。那么当同时有多个线程在执行时,若其中有一个线程执行创建Singleton对象后还没有赋值给single变量,则下一个线程执行到if语句判断,这时single为null,则该线程同样会继续创建对象,因此,这段代码在多线程的情况下会产生多份实例。那么接下来我们通过加锁机制来实现线程安全的懒汉模式。

    三、线程安全的懒汉模式

     1 public class Singleton {
     2     private static Singleton single;
     3 
     4     private Singleton()
     5     {
     6 
     7     }
     8 
     9     public static synchronized Singleton getInstance()
    10     {
    11 
    12         if(single == null)
    13         {
    14         single = new Singleton();
    15         }
    16        17         return single;
    18     }
    19 
    20 }

           很简单,只要在方法前面加上synchronized关键字即可实现线程安全的单例模式,但是该方法也有一个很大的缺陷,锁的粒度太大,当多个线程同时调用该方法时,一次只能有一个线程调用该方法读取single对象(特别是singleton对象已经创建了)。这种实现方式很影响程序的性能。那么下面我们需要对锁的粒度进行缩小,即在第一次创建single对象的时候对其进行加锁,以后对象创建后,我们不需要获取锁来保持互斥。这就是下面将要讲的双重锁检查。

    四、懒汉模式(双重检查锁)

    public class Singleton {
    	private static Singleton single;
    
    	private Singleton()
    	{
    	}
    
    	public static  Singleton getInstance()
    	{
    		if(single == null)
    		{
    			synchronized (Singleton.class) {
    				if(single == null)
    					single = new Singleton();
    			}
    		}
    
    		return single;
    	}
    }
    

        为什么上面的代码需要在synchronized同步块里面加上对single==null的判断?我们可以假设两个线程同时进入到第一个single==null的块内,此时有且仅有一个线程获取到同步锁,另一个线程等待,假如不在同步块里面加上single==null的判断,当第一个线程创建完对象离开同步块时,第二个线程进入同步块,此时它又创建了对象,这就导致了两个对象的创建。那么是不是双重检查锁就一定是线程安全的呢?当编译器对生成的指令做出优化或重排序时,这段代码还是存在问题。正如我们上面所说的single = new Singleton()对应了多个指令操作。通常情况下,编译器在不改变单线程的执行正确性的前提下,为提高程序的性能会给指令进行重排序。我们现在假设这样的一种场景发生的情况下,将会产生非线程安全的单例模式。

    首先将single = new Singleton()拆分成三条指令。(1)对象创建。(2)调用构造函数,初始化实例变量 (3)将创建的对象地址赋值给single。

      现在编译器做了优化,使得指令的执行步骤为(1)(3)(2),因为这在单线程的情况下是不影响程序的正确性的,编译器可以做这种优化。当一个线程执行到指令(3)时,此时变量已经赋值。另外一个线程执行到第一个single == null的条件判断,发现single不等于null,则直接返回single但是此时的对象并没有初始化,因此其获取到的是一个未经初始化的对象,所以在这种情况下双重检查锁是非线程安全的。

    那么怎么才能使其变成线程安全的呢,我们可以在single的声明中加上volatile关键字,即:

    private static volatile Singleton single;
    

      volatile关键词的作用禁止编译器对指令进行重排序。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

    其中需要注意的是在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即使将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

     

    五、饿汉式

    上面讲的几种方式都是与懒汉式相关的,下面我们来看看饿汉式的单例模式。

    public class Singleton {
        private static final Singleton single = new Singleton();
    
        private Singleton()
        {
        }
    
        public static  Singleton getInstance()
        {
            return single;
        }
    
    }

    书写起来方面简单,唯一的缺点也是先前提到的在类装载时,便创建对象驻留在内存中,但是优点也是明显的。

    六、静态内部类的方式。

    public class Singleton {
        private static class SingletonHolder{
            private static final Singleton SINGLETON = new Singleton();
        }
    
        private Singleton()
        {
        }
    
        public static  Singleton getInstance()
        {
            return SingletonHolder.SINGLETON;
        }
    
    }

    这种方式使用了JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

     七、枚举方式

    1 public enum EasySingleton{
    2 INSTANCE;
    3 }
    4 
    5  

    简单,便捷。与此同时,它能够防止反序列化时创建新对象,其次线程安全,同时能够防止反射攻击。

    以上就是在java中创建单例模式的总结。

     

  • 相关阅读:
    php发送post请求的方法
    跨域请求的三种解决办法
    php验证码+js点击刷新
    13.mysql数据类型
    12.dateformat常用格式
    11.设计的三大范式
    nginx passwd (http://www.voidcn.com/article/p-suebfyqy-nx.html)
    删除文件 过滤某个文件
    mac必装软件
    elasticsearch 安装
  • 原文地址:https://www.cnblogs.com/justinli/p/design_pattern.html
Copyright © 2011-2022 走看看