单例模式,饿汉与懒汉

Posted 悲伤猪小猪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式,饿汉与懒汉相关的知识,希望对你有一定的参考价值。

文章目录

什么是单例模式

单例模式其实就是一种设计模式,跟象棋的棋谱一样,给出一些固定的套路帮助你更好的完成代码。设计模式有很多种,单例模式是在校招当中最爱考的设计模式之一。

单例就指的是单个实例,一个程序如果频繁使用一个对象且作用相同,为了防止多次实例化对象,我们就可以使用单例模式,让类只能创建出一个实例,也就是一个对象,减少开销。
有一些场景本身就是要求某一个概念是单例的,例如JDBC里的DateSource

单例模式的两种形式

在Java中实现单例模式有很多种写法,我们这里重点讲解两种,懒汉模式与饿汉模式。

饿汉模式

饿汉模式,顾名思义,当人非常饿的时候,看见了食物,那种心情是怎么样的迫不及待。饿汉模式非常着急在类进行创建时就已经迫不及待的实例化单例对象了

class Singleton 
    private static Singleton singleton = new Singleton();

    public static Singleton getInstance() 
        return singleton;
    



根据我们的描述可以写出这样的代码,但是我们发现,单例模式的初心我们并没有达到,单例模式的初心是让类只能实例化一次,此时我们并没有完成需求。我们通过私有化构造方法的方式来防止类的多次实例化:

class Singleton 
    private static Singleton singleton = new Singleton();

    public static Singleton getInstance() 
        return singleton;
    

    private Singleton () 


此时我们单例模式中的饿汉模式就已经完成了,我们可以来测试一下:

懒汉模式

懒汉,所表示的含义并不是我们理解的流浪汉,相反懒表示的是一种从容不迫,是不着急,这种模式与饿汉模式的迫不及待不同,他只有在真正需要使用对象时才实例化单例对象。懒汉模式同样使用私有化构造方法的形式来完成初心,我们来写一下代码:

class SingletonLazy 
  private static SingletonLazy singletonLazy = null;

  public static SingletonLazy getInstance() 
      if(singletonLazy == null) 
          singletonLazy = new SingletonLazy();
      
      return singletonLazy;
  

  private SingletonLazy () 


懒汉模式与饿汉模式是否线程安全

上面我们完成了懒汉模式与饿汉模式的代码编写,现在我们需要考虑一个问题,上面两个代码,是否线程安全,在多线程下调用getInstance()是否会出现问题。

首先我们来看饿汉模式

饿汉模式的getInstance()方法为只读操作,所以在多线程下调用不会有什么问题,是安全的。

懒汉模式:

懒汉模式在多线程下,无法保证创建对象的唯一性。

例如两个线程同时调用getInstance()方法,代码的执行顺序可能为:
1、线程一进行判断操作
2、线程二进行判断操作
3、线程一实例化对象
4、线程二实例化对象
这样线程一和线程二都会实例化对象,如果是N个线程可能会实例化N个对象,所以懒汉模式在多线程模式下不安全。

懒汉模式的优化

我们需要对懒汉模式进行优化,使得他在多线程下变得安全,如何操作呢?上面的分析中我们提到了,懒汉模式不安全的原因是,判断操作和new操作没有原子性,那么我们让他具有原子性不就可以了。我们就可以通过加锁来完成需求:

class SingletonLazy 
  private static SingletonLazy singletonLazy = null;

  public static SingletonLazy getInstance() 
      synchronized (SingletonLazy.class) 
          if(singletonLazy == null) 
              singletonLazy = new SingletonLazy();
          
      
      return singletonLazy;
  

  private SingletonLazy () 


这样就会有锁竞争,不会在出现向刚才那样两个线程同时进行判断的操作,一定是等一个线程new了之后,另一个线程才能竞争到锁进行判断。
我们觉得这样还是不够,不够高效,这样写虽然可以解决安全问题,但是同时也造成了效率的降低,每个线程都需要阻塞等待,但是我们分析一下,只有singletonLazy == null时才需要进行阻塞,当singletonLazy != null时其实就只是单纯的读操作。所以我们在进行优化:

class SingletonLazy 
    private static SingletonLazy singletonLazy = null;

    public static SingletonLazy getInstance() 
        if (singletonLazy == null) 
            synchronized (SingletonLazy.class) 
                if(singletonLazy == null) 
                    singletonLazy = new SingletonLazy();
                
            
        
        return singletonLazy;
    

    private SingletonLazy () 

这样又解决了我们的问题,上面代码中的两个判断条件看着是一样的,但是初心不一样,第一个是为了提高效率,判断是否需要加锁,第二个是为了判断是否需要实例化对象,两行代码看着离这不远,但是中间有一个加锁的操作,执行的时机其实差别很大。
这样就完了?并没有这里还有一个问题:指令重排序

什么是指令重排序呢?
创建一个对象,在jvm中会经过三步
1、创建内存空间
2、调用构造方法
3、将引用指向分配好的内存空间
我们发现,第二步和第三步好像可以进行交换执行顺序,交换之后对结果并没有影响,而这样不影响结果的情况下,可以不按照程序编码的顺序执行语句,提高程序性能的操作,我们称为指令重排序

这里我们也实力化对象了,所以也可能有指令重排序的操作,例如线程一此时new对象的时候,发生了指令重排序,在没有调用构造方法的情况下进行了分配内存空间,此时系统调度到了线程二,线程二进行判断,此时引用非空就返回,这样我们返回了一个没有调用过构造方法的引用。
如何解决问题呢?我们使用volatile就可以防止指令重排序:

class SingletonLazy 
    volatile private static SingletonLazy singletonLazy = null;

    public static SingletonLazy getInstance() 
        if (singletonLazy == null) 
            synchronized (SingletonLazy.class) 
                if(singletonLazy == null) 
                    singletonLazy = new SingletonLazy();
                
            
        
        return singletonLazy;
    

    private SingletonLazy () 


这样懒汉模式的优化,我们就完成了。

设计模式--单例模式

实例

/**
 * 单例模式
 * 应用场合:有些对象只需要有一个就足够了
 * 作用:保证整个应用程序中某个实例有且只有一个
 * 类型:饿汉与懒汉
 * */
public class Singleton {
	/**
	 * 1.将构造方法私有化,不允许外部直接创建对象
	 * */
	private Singleton(){
	}
	/**
	 * 2.创建类的实例是唯一的,使用private static 修饰
	 * 将类实例定义成static静态可以让外部类通过Singleton s1=Singleton.instance;这种方式调用
	 * */
	static Singleton instance=new Singleton();
}
public class test {
   public static void main(String[] args) {
	   //创建了一个Singleton实例
	   /**
	    * 之所以可以创建实例是因为在Singleton中有一个默认公开的构造方法
	    * 将构造方法重写成私有的,外界就无法在直接通过构造方法创建时实例了,就可避免多个实例出现
	    * 
	    * */
	  Singleton s1=Singleton.instance;
	  Singleton s2=Singleton.instance;
	  
	  if(s1==s2){
		  System.out.println("s1与s2是同一个实例");
	  }else{
		  System.out.println("s1与s2不是同一个实例");
	  }
    }
}

结果

s1与s2是同一个实例

 另一个情况:将成员变量改为private时,外部就无法通过那个Singleton.instance方式调用,解决方法(通过封装的方式调整一下代码)

private static Singleton instance=new Singleton();
	
	/**
	 * 4.提供一个获取实例的方法,使用public static 修饰
	 * */
	public static Singleton getInstance(){
		return instance;
		}

 提供了获取实例的方法就可以通过Singleton.getInstance(),来获取到

Singleton s1=Singleton.getInstance();
	  Singleton s2=Singleton.getInstance();

结果

s1与s2是同一个实例

private static Singleton instance=new Singleton();

这段代码什么时候加载呢:static的静态的成员属于类所有,当类加载的时候它就会去执行,所以当Singleton这个类加载的时候,它就会去创建一个类的实例,不管用户是否会去调用或者获取这个实例,它都已经加载了--称为饿汉

以上为单例模式的饿汉模式

/**
 * 懒汉模式
 * */
public class Singleton2 {
	/**
	 * 1.将构造方法私有化,不允许外界直接创建对象
	 * */
	private Singleton2(){}
	/**
	 * 2.声明类的唯一实例,使用private static修饰
	 * 当类加载的时候,并没有执行,只有在获取的时候再去判断,为空时才去创建实例,
	 * 当第二次,第三次再次获取的时候因为已经创建过了就不会再去创建--懒汉模式
	 * */
	private static Singleton2 instance;
	/**
	 * 3.提供一个用于获取实例的方法,使用public static修饰
	 * */
	public static Singleton2 getInstance(){
		if(instance==null){
			instance=new Singleton2();
		}
		return instance;
	}
}
/**
	   * 懒汉模式
	   * */
	  Singleton s3=Singleton.getInstance();
	  Singleton s4=Singleton.getInstance();
	  
	  if(s3==s4){
		  System.out.println("s3与s4是同一个实例");
	  }else{
		  System.out.println("s3与s4不是同一个实例");
	  }
    }

 区别

饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全的;懒汉模式特点在加载类时比较快,但运行时获取对象的速度比较慢,线程不安全的

以上是关于单例模式,饿汉与懒汉的主要内容,如果未能解决你的问题,请参考以下文章

从零开始的Java开发1-4-2 Java单例模式:单例模式饿汉式懒汉式单例模式特点及应用场景

Java之单例模式(懒汉模式饿汉模式)

Java之单例模式(懒汉模式饿汉模式)

Java-设计模式-单例模式-饿汉模式懒汉模式

Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量

Java 设计模式 -- 单例模式的实现(饿汉式枚举饿汉式懒汉式双检锁懒汉式内部类懒汉式)jdk 中用到单例模式的场景DCL实现单例需用volatile 修饰静态变量