Java设计模式-单例模式详解

今天我们来详细的学习一下你见过的或没见过的Java单例模式,对不同的单例模式写法,尽可能的说明其原理。
单例模式的核心是一个类只能被创建一个实例化对象

单例模式的实现

  • 构造函数私有化,避免外部通过new创建
  • 确保单例的线程安全
  • 确保单例的唯一性,不能被重复创建

1、饿汉式

public class Signleton{
    /**
    * 对于一个final变量。  
    * 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;  
    * 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
    */ 
   private final static Signleton instance = new Signleton();
   
   private Signleton(){
       
   }
 
   public static  Signleton getInstance(){
        return instance;
   }
}

优点:饿汉式在类加载的过程中就会创建实例化对象,在getInstance()方法中只是直接返回对象引用。天生就是线程安全的。
缺点:无论类是否被使用,从始至终都会占据内存资源。

2、懒汉式

/**
* 普通懒汉式
*/
public class Signleton{
  private static Signleton instance = null;
  
  private Signleton(){
  
  }
  
  public static Signleton getInstance(){
     if(instance==null){
        instance = new Signleton();
     }
     return instance;
  }
}

Singleton的静态属性instance中,只有instance为null的时候才创建一个实例,构造函数私有,确保每次都只创建一个,避免重复创建。但如果线程A进入if判断,并且判断为空,开始创建对象,与此同时,线程B也进入if判断,而线程A还未完成对象的实例化,那么线程B也会创建对象,所以此方式在多线程场景下是线程不安全的。

/**
* 加锁懒汉式
*/
public class Signleton{
  private static Signleton instance = null;
  
  private Signleton(){
  
  }
  
  public synchronized static Signleton getInstance(){
     if(instance==null){
        instance = new Signleton();
     }
     return instance;
  }
}

给方法加锁,若有ABCD线程使用,A线程先进入,BCD线程都需要等待A线程执行完毕释放锁才能获得锁执行该方法,但这样的方式每次通过getInstance()方法去获取对象都会加锁,效率很低,继续改造。

/**
* 双重锁检查懒汉式(DCL模式)
*/
public class Signleton {
  //使用volatile关键字修饰,避免jvm指令重排序
  private volatile static Signleton instance = null;
  
  private Signleton(){
  
  }

  //假设有ABCD个线程同时使用这个方法
  public static Signleton getInstance(){
  //BCD都进入了这个方法
    if(instance==null){
      //而A线程已经给第二个的判断加锁了 
      synchronized (Signleton.class){
         //这时A挂起,对象instance还没创建 ,故BCD都进入了第一个判断里面,并排队等待A释放锁
         //A唤醒继续执行并创建了instance对象,执行完毕释放锁。
         //此时到B线程进入到第二个判断并加锁,但由于B进入第二个判断时instance 不为null了  故需要再判断多一次  不然会再创建一次实例
          if(instance==null){
             instance = new Signleton();
          }
       
      } 
    }
    return instance;
   }
}

通过双重锁检测,即实现了多线程场景下的线程安全,又避免了在对象已经创建的情况下调用getInstance()方法对其加锁,提高了效率。但细心的朋友可能已经发现了,对象使用了volatile关键字修饰,原因是JVM在执行对象创建的过程中存在指令重排序的情况,加上volatile关键字可以避免指令重排序,那什么又是指令重排序呢?

instance = new Signleton();

创建对象的过程在JVM中执行分为三个步骤:

  1. 在堆内存开辟内存空间。
  2. 调用Signleton的构造函数来初始化成员变量,形成实例。
  3. 把对象指向堆内存空间。

然而指令在执行的过程中第2步和第3步的顺序是不确定的,有可能按照123的顺序执行,也有可能按照132的顺序执行,如果线程A按照132的顺序执行,执行到3时(执行外3后instance非null)时间片用完,2还未执行,此时切换到线程B上,由于instance已经不为null,所以线程B直接返回,但同时Signleton类还没有实例,然后就顺理成章的报错了。因此,指令重排序会导致DCL失效。

通过不断的改造,懒汉式的单例模式好像已经无懈可击了,但事实并非如此,我们忘了Java还有一个牛逼的存在,那就是Java反射机制,他专治各种花里胡哨,因为他可以直接通过直接调用构造方法来完成对象的创建。所以为了避免反射机制的破坏,我们可以使用一个信号量来控制。

/**
 * 避免反射机制破坏的双重锁检查懒汉式(DCL模式)
 */
public class Signleton {
    //使用volatile关键字修饰,避免jvm指令重排序
    private volatile static Signleton instance = null;

    private static boolean flag = false;

    private Signleton(){
        if (flag){
            throw new RuntimeException("不要试图通过反射机制破坏单例模式");
        } else {
            flag = true;
        }
    }

    //假设有ABCD个线程同时使用这个方法
    public static Signleton getInstance(){
        //BCD都进入了这个方法
        if(instance==null){
            //而A线程已经给第二个的判断加锁了
            synchronized (Signleton.class){
                //这时A挂起,对象instance还没创建 ,故BCD都进入了第一个判断里面,并排队等待A释放锁
                //A唤醒继续执行并创建了instance对象,执行完毕释放锁。
                //此时到B线程进入到第二个判断并加锁,但由于B进入第二个判断时instance 不为null了  故需要再判断多一次  不然会再创建一次实例
                if(instance==null){
                    instance = new Signleton();
                }

            }
        }
        return instance;
    }

    public static void main(String[] args) throws Exception {
        //首先我们获得这个空参构造器
        //由于构造器是私有的,所以我们用.setAccessible()解决了私有构造器的问题

        Constructor<Signleton> declaredConstructor = Signleton.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //这样就又创建出了另一个实例
        Signleton instance1 = declaredConstructor.newInstance();
        Signleton instance2 = declaredConstructor.newInstance();
        System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
}

所以,如果想要并避免反射机制破坏单例模式,可以采用上面的方法,但如果类不会反射机制使用到,大可不必。

3、静态内部类模式

public class Signleton{
   private Signleton{
   }
  
   //静态嵌套类  这里给个链接 区分静态嵌套类和内部类[静态嵌套类和内部类](http://blog.csdn.net/iispring/article/details/46490319)
   private static class  SignletonHolder{
      public static final Signleton instance = new Signleton();
  }
  
  public static Signleton getInstance(){
      return SignletonHolder.instance;
  }
}

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占内存。即当Singleton第一次被加载时,并不需要去加载SingletonHolder,只有当getInstance()方法第一次被调用时,才会去初始化instance,第一次调用getInstance()方法会导致虚拟机加载SingletonHolder类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么instance在创建的过程中又是如何保证线程安全的呢?
虚拟机会保证一个类的clinit()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕。如果在一个类的clinit()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行clinit()方法后,其他线程唤醒之后不会再次进入clinit()方法。同一个加载器下,一个类型只会初始化一次。

静态内部类模式并不是完美的,他无法传递参数,所以实际应用中可以根据实际情况选择静态内部类模式或者DCL模式,或者下面要讲的枚举式

4、枚举式

public class Signleton{

	public static Signleton getInstance(){
	   return SignletonEnum.INSTANCE.getInstance();
	}
	
	public enum SignletonEnum{
	   INSTANCE;
	   
	   private Signleton instance;
	   
	   //由于JVM只会初始化一次枚举实例,所以instance无需加static 
	   private SignletonEnum(){
	        instance = new Signleton();
	   }
	   
	   public getInstance(){
	       return instance;   
	   }
	}
}

定义内部的枚举,由于类加载时JVM只会初始化一次枚举实例,所以在构造函数中创建Signgleton对象并保证了这个对象实例唯一。而且枚举实例的创建是线程安全的,并且不会被反射机制破坏,请看以下源码。
在这里插入图片描述

单元素的枚举类型已经成为实现Singleton的最佳方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值