1.简介
单例模式是设计模式中最简单的形式之一。这一模式的目的是使某个类在系统中有且仅有一个实例。通过单例模式可以保证系统中该类只有一个实例而且该实例易于外界访问,从而达到使用目的,同时还能方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
要实现这一点,可以从客户端对其进行实例化入手。因此需要用一种只允许生成对象类的唯一实例的机制,“阻止”其他所有想要生成对象的访问。
在我的工作中接触到不少单例模式的例子,例如一个工厂类,或是对DAO层封装好了的工具类,它们都有一个共性就是这些类没有自己的状态,无论实例化多少次它们都是一样的,如果不做成单例模式的话应用里就有可能会有很多很多一摸一样的实例,这样会造成内存的浪费,GC工作量的增加等。
2.特点
显然单例模式的特点有三个:
1. 该类只能有一个实例;
2. 该类必须自行创建这个实例;
3.单例类对外提供一个访问该单例的全局访问点;
从具体实现角度来说,就是以下三点:
1. 单例类只提供私有的构造方法;
2. 单例类定义中含有一个该类的静态私有对象;
3. 单例类提供了一个静态的公有的方法用于创建或获取它本身的静态私有对象。
单例模式的结构图:
单例模式分为懒汉式和饿汉式;二者的区别在于创建实例的时间不同,具体区别如下:
懒汉模式:在类加载时没有创建该实例,当第一次调用getlnstance 方法时才去创建这个单例;
饿汉模式:在单例类加载时就创建一个实例,保证在调用 getInstance 方法之前单例已经存在了,直接返回该单例。
懒汉式的单例类实现如下:
public class Singleton {
// 私有构造函数
private Singleton() {}
// 静态实例
private static Singleton singleton;
// 静态方法返回一个实例
public static Singleton getInstance() {
if(null == singleton) {
singleton = new Singleton();
}
return singleton;
}
}
1. 构造函数私有化。这样,在访问客户端就无法调用构造方法(通过new关键字)创建该类的实例了。
2. 静态实例。带有static的属性在每一个类中都是唯一的。
3. 静态方法返回一个实例。注意一定要是静态方法,如果不是静态方法,那就需要实例化一个对象才能使用这个方法,这与单例模式互相矛盾了,与实际不符。
但是上述的单例模式还是存在很多问题的,比如在并发的情况下,有多个线程同时进入getInstance方法,同时进入if判断并判断到静态实例为null,这样就会实例化多个Singleton出来,单例模式就崩塌了。如何解决该问题呢?
为了解决上述问题,有些人通过synchronized关键字为访问方法加锁(实现getInstance方法为synchronized 方法)避免多线程环境下的单例对象重复创建
public class Singleton {
// 构造函数私有化
private Singleton() {}
// 静态实例
private static Singleton singleton;
// 静态方法返回一个实例
public static synchronized Singleton getInstance() {
if(null == singleton) {
singleton = new Singleton();
}
return singleton;
}
}
这次我们给返回实例的方法加上synchronized,将整个方法同步加锁,虽然可行,但问题也很大:
缺点:性能问题。 只有第一次执行此方法创建单例对象时才需要同步,一旦创建了该对象以后,多线程并发访问该对象就不需要同步这个方法。此后的每次操作都会消耗在同步上。设想在一个高并发的环境下,有一个线程访问了这个方法,其他线程都得处于无谓的等待状态,这种设计太烂了。
解决此问题的办法是
双重加锁:其实同步的地方只需在实例还未实例化的时候,在实例化之后就没有必要再进行同步控制了,所以可以将上面的单例模式改成很多地方都出现过的单例模式的版本,称为双重加锁。
public class Singleton {
// 构造函数私有化
private Singleton() {}
// 静态实例
private static Singleton singleton;
// 静态方法返回一个实例
public static Singleton getInstance() {
if(null == singleton) {
synchronized(Singleton.class) {
if(null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这样的单例模式看起来就比较完美了,例如有两个线程判断到实例为null,有一个线程进入到synchronized里,第二个线程等待进入,第一个线程实例化完成后释放锁,第二个线程进入synchronized,但里面还有一层判断,此时实例不为null,则不会进行实例化。在实例不为null的时候就不会进入同步块了,这样就节省了很多线程的无谓等待。
但是,上述的单例模式在我这种初学者眼中像是完美的状态,但如果深入到JVM的层面上来说,还是存在一些问题,因为JVM创建一个新的对象(demo = new Demo())是分三步走的。
1. 分配内存(分配对象的内存空间)
2. 初始化构造器(初始化对象)
3. 将对象指向分配的内存的地址上(设置对象变量指向刚分配的内存地址)
在1 2 3 这种步骤下上述的单例模式是没有问题的,但JVM会对字节码进行调优(调整指令的执行顺序),所以如果2 3的执行顺序对调,变成 1 3 2,那么如果JVM先给对象分配了内存,而还没开始初始化构造器时,有一个线程进入getInstance方法,会判断实例不为null,此时会直接返回一个引用,这时是在初始化构造器之前的,所以会产生异常。
解决如上问题的最终解决方案 采用volatile关键字修饰单例对象变量 :
public class Singleton {
// 构造函数私有化
private Singleton() {}
// 静态实例
private static volatile Singleton singleton;
// 静态方法返回一个实例
public static Singleton getInstance() {
if(null == singleton) {
synchronized(Singleton.class) {
if(null == singleton) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
关于volatile关键字详见:https://www.cnblogs.com/zhengbin/p/5654805.html
其次,可以通过Java 内部类解决上述问题实现懒汉式单例模式,形成比较标准的写法:
public class Singleton {
// 构造函数私有化
private Singleton() {}
// 静态方法返回一个实例
public static Singleton getInstance() {
return SingletonInstance.Instance;
}
//静态内部类中有静态属性创建实例
private static class SingletonInstance {
static Singleton Instance = new Singleton();
}
}
上述的单例模式主要是利用了类中的静态属性JVM只会在第一次加载类时初始化的特点,所以静态属性只会初始化一次,符合单例。并发的情况下,初始化静态属性时JVM会强行同步这一过程,所以无需考虑并发访问的问题。
饿汉式单例模式的实现是通过final关键字修饰的静态单例变量在类加载时创建单例对象:
public class HungrySingleton
{
private static final HungrySingleton instance=new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance()
{
return instance;
}
}
参考文档:
https://blog.youkuaiyun.com/qq_41737716/article/details/80510539
https://blog.youkuaiyun.com/www1575066083/article/details/80371085