1. 什么是单例模式
单例模式指的是在应用整个生命周期内只能存在一个实例。
单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。
2. 单例模式和静态类的区别
首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态field,构造器被private修饰,因此不能被实例化。Math类就是一个静态类。
知道了什么是静态类后,来说一下他们两者之间的区别:
1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;
2)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;
如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用“单例模式”。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。
3.如何实现单例模式
3.1------创建饿汉式单例
所谓饿汉模式就是立即加载,一般情况下再调用getInstancef方法之前就已经产生了实例,也就是在类加载的时候已经产生了。这种模式的缺点很明显,就是占用资源,当单例类很大的时候,其实我们是想使用的时候再产生实例。因此这种方式适合占用资源少,在初始化的时候就会被用到的类。
public class SingletonTest { //创建饿汉式单例模式 private static SingletonTest singletonHungary = new SingletonTest(); //构造方法私有化 private SingletonTest() { } public static SingletonTest getSingleton(){ return singletonHungary; } }
3.2-------创建懒汉式单例
懒汉模式就是延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。针对懒汉模式,这里给出了5种实现方式,有些实现方式是线程不安全的,也就是说在多线程并发的环境下可能出现资源同步问题。
首先第一种方式,在单线程下没问题,在多线程下就出现问题了。
用几个多线程跑下,得出每个单例对象的哈希值,可以看出有不重复的值,那么说明有不同的对象生成。
public class SingletonTest { //创建懒汉式单例模式 private static SingletonTest singletonHungary ; //构造方法私有化 private SingletonTest() { } public static SingletonTest getSingleton(){ if(singletonHungary==null){ singletonHungary=new SingletonTest(); } return singletonHungary; } }
3.3那么怎样才能保证什么情况下单例模式在多线程情况下能够实现呢
用基于JVM的关键字synchronized
public class SingletonTest { //创建懒汉式单例模式 private static SingletonTest singletonHungary ; //构造方法私有化 private SingletonTest() { } public static SingletonTest getSingleton(){ synchronized(SingletonTest.class){ if(singletonHungary==null){ //代码一 singletonHungary=new SingletonTest(); } } return singletonHungary; } }
3.4,看起来代码没有问题,实际测你就会发现并不能实现单例。当A,B两线程同时访问到代码一出,它们对象都为空,都会创建对象。那么我们需要双重判断就可以解决
public class SingletonTest { //创建懒汉式单例模式 private static SingletonTest singletonHungary ; //构造方法私有化 private SingletonTest() { } public static SingletonTest getSingleton(){ if(singletonHungary==null){ synchronized(SingletonTest.class){ if(singletonHungary==null) { singletonHungary = new SingletonTest(); } } } return singletonHungary; } }
我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?
这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。
private static volatile
SingletonTest singletonHungary
= null;
3.5 这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给singletonHungary
字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给singletonHungary
字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了singletonHungary
变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。
3.6,这种基于JVM的双重锁机制处理问题在我们工作中也常见,例如我们在多线程情况下导入一百万条订单中用户的信息,订单中用户有重复,会遇到这种操作:【if(user==null) 插入用户信息】那么双重锁的机制可以解决这个问题了。
4.静态类下的单例模式
public class SingletonTest { private SingletonTest() { } private static class SingletonInner { private static SingletonTest singletonStaticInner = new SingletonTest(); } public static SingletonTest getInstance() { return SingletonInner.singletonStaticInner; } }
可以看到使用这种方式我们没有显式的进行任何同步操作,那他是如何保证线程安全呢?和饿汉模式一样,是靠JVM保证类的静态成员只能被加载一次的特点,这样就从JVM层面保证了只会有一个实例对象。那么问题来了,这种方式和饿汉模式又有什么区别呢?不也是立即加载么?实则不然,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
可以说这种方式是实现单例模式的最优解。