单例模式:属于创建型模式,主要用来创建对象的。保证一个类仅有一个实例,并提供一个访问它的全局访问点。
下面就是个简单的单例模式:
public class Singleton {
//设置静态变量
private static Singleton singleton;
//private构造方法能够确保能通过new来创建对象
private Singleton(){}
//获得对象
public static Singleton getSingleton(){
//判断对象是否为空,如果为空则创建对象
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
public void showMessage(){
System.out.println("我是单例");
}
}
使用的时候我们只需要这样就可以获得对象:
public class SingletonPatternMain {
public static void main (String[] args) {
Singleton singleton = Singleton.getSingleton();
singleton.showMessage();
}
}
那么简单的一个单例模式就创建成功了,下面我们分析一下这样一个单例模式有什么问题。
现在有A、B两个线程,当两个线程同时执行到 if(singleton==null) 的时候发现singleton为null,然后都会去创建对象,这样单例模式就被破坏。
怎么来解决这样的问题呢,我们首先会想到加锁,例如这样:
public class Singleton {
//设置静态变量
private static Singleton singleton;
//private构造方法能够确保能通过new来创建对象
private Singleton(){}
//加锁获得对象
public synchronized static Singleton getSingleton(){
//判断对象是否为空,如果为空则创建对象
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
public void showMessage(){
System.out.println("我是单例");
}
我们使用synchronized来修饰我们的getSingleton()方法,这样就能确保只有一个线程获取到方法锁,来获取对象。
那么我们再来分析一下,这样的单例有什么问题呢?这里有一个效率问题,因为是单例模式,所以我们只需要在第一次创建
对象的时候加锁就可以了。
所以我们可以这样来修改我们代码:利用双重锁定
public class Singleton {
//设置静态变量
private static Singleton singleton;
//private构造方法能够确保能通过new来创建对象
private Singleton(){}
//获得对象
public static Singleton getSingleton(){
//判断对象是否为空,如果为空则创建对象
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
public void showMessage(){
System.out.println("我是单例");
}
}
这样我们就可以解决上面的问题。但是,从jvm角度出发这并不是一个完美的方案,因为jvm有个叫做指令重排的概念,在单线程下没有问题,但是在多线程下就会产生不确定的执行效果,我们来分析一下:
singleton = new Singleton();并不是一个原子性的操作,可以抽象为JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
singleton =memory; //3:设置singleton指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
singleton =memory; //3:singleton 指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
比如现在有A、B两个线程
1、A首先进入synchronized块,由于singleton为null,所以它执行singleton = new Singleton();
2、这时候刚好碰到指令重排,A线程先回分配对象的内存空间,然后singleton去指向刚分配的内存地址(这时候并没有去
初始化对象) 然后A离开了synchronized块,紧接着B线程获得锁,去判断singleton是否为空,发现singleton不为空,
然后返回了singleton对象,这时候去使用singleton的话就会出错。
所以我们可以使用volatile关键字来设置一个内存屏障来防止指令重排:private static volatile Singleton singleton;
有时候为了实现慢加载,并且不希望每次调用getInstance时都必须互斥执行,有一种既方便又简单的解决方法:
public class Singleton01 {
//阻止通过new来获取对象
private Singleton01 () { }
public static Singleton01 getSingleton(){
return getInstance.singleton01;
}
//通过静态内部类来创建对象
private static class getInstance{
private static final Singleton01 singleton01 = new Singleton01();
}
}
这种方法是通过静态内部类来创建单例,我们来分析一下:
1、虚拟机在首次加载Java类时,会对静态初始化块、静态成员变量、静态方法进行一次初始化;静态内容首先被加载,相当于 全局的成员
2、当静态方法geSingleton() 加载时,SingletonHolder.INSTANCE默认初始值是空
3、当去调用geSingleton() 方法时,静态内部类getInstance才去加载,从而去创建Singleton01对象(常量)
要点:
1、线程安全:因为内部的静态类只会被加载一次,只会有一个实例对象,所以是线程安全的
2、内部类的加载机制: java中的内部类是延时加载的,只有在第一次使用时加载;不使用就不加载;