单例模式

单例模式

  单例模式是24种单例模式中最为基本简单的设计模式。单例模式是为了这个类只提供一个对外的实例。单例模式的基本思想是,构造函数私有化,外部无法直接创建一个实例,只能通过该类的静态方法来获取对象实例。

饿汉模式

  饿汉模式就是说这个实例在类加载初始化的时候就加载好了,因为特别急,所以戏称饿汉模式。


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

懒汉模式

  懒汉模式,顾名思义,就是说我懒得在没人用就创建,等到有人要用的时候就第一次创建,后面的调用方就直接用第一次创建的对象。所以戏称为懒汉模式。


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

  但是有问题,在多线程并发的情况下,这个单例类还没有创建第一个实例时,多个线程同时进入到了if(singleton==null)这个条件里面,就会创建多个实例。考虑到线程安全,可以在这个getInstance这个方法里面这个非null判断加个类锁(静态方法属于这个类本身,所以加类锁)。


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

  但是每一次创建类的时候都要加锁,对系统性能有很大的影响。


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

  这里2重锁非null判断,我们这里的话我们只是第一次创建对象的时候加了锁,而后面的获取对象就不用加锁了。恩,暂时没有问题了。

  为什么要2个非空判断呢?
  A,B2个线程获取实例,都经过了第一个非空判断,A先获取到了类锁,进行初始化,B进入锁等待。A初始化后释放锁,B获取到锁,如果不再进行一次非空判断,就会再次创建一个对象。

  为什么必须要volatile关键字呢?
  显然这样还不行,这里涉及到指令重排序问题。在上述的双重锁机制中,有的线程可能访问到没有被初始化的资源。
  这是因为 single=new Singleton()这段代码不是原子的,它可以分解为3个操作。

memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

  根据java语言规范,在单线程中2和3操作的顺序是不影响程序执行效果的,那么这个顺序就是可以打乱的了,但是在多线程中这个可能会有影响,当a线程第一次访问时,获取到了类锁,进行对象创建,而马上第二个线程b,也访问到了,开始访问第一个非null判断,而a线程创建对象时,到了第3步,没有进行初始化,那么b线程返回的结果可能是一个没有初始化的对象。所以可能出现问题。
  那么怎么解决呢?
  那就是给这个要初始化的对象加volatile关键字,来禁止指令重排序,问题完美解决。

枚举方式实现单例

public enum Singleton{
    INSTANCE;
}

  我们可以通过Singleton.INSTANCE来访问实例。创建枚举默认就是线程安全的,还能防止反序列化导致重新创建新的对象。不然就要像其他单例模式一样要重写readObject方法。