一.何为单例设计模式
如其名字,单例设计模式就是指的是一个类中只允许存在一个对象实例。
在java中,我们存在两种创建单例模式的思路:饿汉式和懒汉式
①饿汉式:在创建类时直接创建对象实例
public class SingleHungryMan {//建立类变量private static SingleHungryMan singleHungryMan=new SingleHungryMan();//定义私有构造方法,避免其他类调用private SingleHungryMan(){}//创建获取类对象的方法public static SingleHungryMan getInstance(){return singleHungryMan;}} ②懒汉式:在需要获取对象实例时再去创建对象实例,效率略高
public class SingleLazyMan {private static SingleLazyMan singleLazyMan=null;//定义私有构造方法private SingleLazyMan(){}//构造获取方法public static SingleLazyMan getInstance() {if(singleLazyMan==null){singleLazyMan=new SingleLazyMan();}return singleLazyMan; }
} 在单线程情况下两种单例设计模式都不需要考虑线程安全问题,但是一旦到多线程情况下,懒汉式的单例设计模式存在线程安全问题,而饿汉式是在创建类之初就把实例对象创建好了,因此在多线程环境下仍然是线程安全的,我们通过下图去解析其问题:
我们进行测试,发现确实存在问题:
这种问题是多线程情况下常见的问题:不满足原子性,这种情况下我们如何解决该问题呢?
最直观的解决措施就是上锁,保持其原子性
public class LazyMan {//懒汉式单例对象//创建单例对象//使用volatile修饰,禁止指令重排序
private static volatile LazyMan lazyMan;
//私有化构造方法private LazyMan(){}//创建实例对象public static LazyMan getLazyMan(){synchronized (LazyMan.class){//判断是否为空,为空则创建if(lazyMan==null) {lazyMan = new LazyMan();}}return lazyMan;}
}
上锁之后,问题得到了解决,但是上锁之后我们又面临了一个问题:效率问题,如果我们创建了多个线程,但是当一个线程已经创建了对象,其他线程也就不需要再进行上锁判断(因为已经创建了对象,不需要考虑线程安全问题),但是上锁操作事实上是一个很降低效率的问题,我们再次对代码进行优化
public class LazyMan {//懒汉式单例对象//创建单例对象//使用volatile修饰,禁止指令重排序
private static volatile LazyMan lazyMan;
//私有化构造方法private LazyMan(){}//创建实例对象public static LazyMan getLazyMan(){//判断是否为空,为空则创建if(lazyMan==null) {synchronized (LazyMan.class){lazyMan = new LazyMan();}}return lazyMan;}
} 但是进行这种优化后,测试结果如下:
我们发现创建的并不是一个单例对象,分析其结果:说到底还是原子性的问题:线程2在线程1判断单例对象为空对象之后,在创建实例对象之前也进行了非空校验,这样下来,即使线程1已经创建了对象,线程2由于已经完成了非空校验,因此还会创建一个对象来覆盖线程1创建的对象,因此创建了第二个对象。因此我们需要在synchronized之内在多加一个是否仍然为空对象的校验
public class LazyMan {//懒汉式单例对象//创建单例对象//使用volatile修饰,禁止指令重排序
private static volatile LazyMan lazyMan;
//私有化构造方法private LazyMan(){}//创建实例对象public static LazyMan getLazyMan(){//判断是否为空,为空则创建if(lazyMan==null) {synchronized (LazyMan.class){//第二次在synchronized之下判断是否为空if(lazyMan==null){lazyMan = new LazyMan();}}}return lazyMan;}
}
此时我们发现创建的对象是同一个单例对象
通过上述操作,我们已经将第一个问题高效的解决了,但是仍然存在第二个问题:
我们在进行创建对象的时候
一般分为三步
①开辟内存空间
②初始化对象
③将内存首地址赋给对象引用
①和③必须按照顺序执行,但是②和③却没有严格的执行顺序,所以②和③可能被编译器进行指令重排序,一旦②和③执行顺序调反,问题就出现了,虽然我们获得了一个内存引用,但是这个对象还没有进行初始化,我们后续操作进行引用的也就是一个无效对象。为了避免该问题的产生,我们应该禁止指令重排序,那么就应该用volatile修饰变量,我们需要说明的是:使用volatile能够避免很多我们无法考虑的线程安全问题。
结果仍然是正确的:
单例模式常见的应用场景
日志系统:在应用程序中,通常只需要一个日志系统,以避免在多个地方创建多个日志对象,并降低资源消耗。
数据库连接池:在应用程序中,数据库连接池是一个非常重要的资源,单例模式可以确保在应用程序中只有一个数据库连接池实例,避免资源浪费。
配置文件管理器:在应用程序中,通常只需要一个配置文件管理器来管理应用程序的配置文件,单例模式可以确保在整个应用程序中只有一个配置文件管理器实例。

















