单例模式
一、什么是单例模式
在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点.
单例模式是5个创建型模式的最后一个,可以说是整个设计中最简单的模式之⼀,其定义是单例对象的类只能允许一个实例存在。
单例例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升整体的代码的性能。
单例的实现主要是通过以下两个步骤:
1.将该类的构造方法定义为私有方法, 这样其他处的代码就无法通过调用该类的构造方法来实例话该类的对象, 只有通过该类提供的静态方法来得到该类的唯一实例.
2.在该类内提供一个静态方法, 当我们调用这个方法时, 如果类持有的引用不为空就放回这个引用, 如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用.
二、使用场景
1、数据库的连接池
2、spring中一个单例模式bean的生成和使用
3、应用程序的日志应用
4、回收站
5、网页计数器
适用场景:
- 需要生成唯一序列的环境
- 需要频繁实例化然后销毁的对象
- 创建对象时耗时过多或者消耗资源过多,但又要经常使用的对象
- 方便资源相互通信的环境
三、七种单例模式实现
1.饿汉模式(线程安全)
public class Hungry {
//指向自己实例的私有静态引用,主动创建
private static Hungry instance = new Hungry();
//私有构造方法
private Hungry(){
}
//以自己实例为返回值的静态的共有方法,静态方法
public static Hungry getInstance(){
return instance;
}
}
优点: 类加载时实例化,避免线程同步问题
缺点: 不能达到懒加载的效果,可能浪费内存空间
2.懒汉模式(线程不安全)
public class LazyNoSave {
private static LazyNoSave instance;
private LazyNoSave(){}
public static LazyNoSave getInstance(){
if(null != instance) return instance;
instance = new LazyNoSave();
return instance;
}
}
优点: 实现了延迟加载,解决了饿汉模式的缺点
缺点: 只能在单线程下使用,多线程情况下,如果多个线程都执行到if(null != instance) 后,会创建多个实例,也就破环了单例的效果.
3.懒汉模式(线程安全)(不建议)
public class LazySave {
private static LazySave instance;
private LazySave(){}
//加锁
public static synchronized LazySave getInstance(){
if(null != instance) return instance;
instance = new LazySave();
return instance;
}
}
优点: 较上线程安全
缺点: 锁直接加到方法上,导致所有访问都因需要锁占用导致资源浪费.
4.双重锁校验(线程安全)
public class DoubleLock {
//volatile避免指令重排,解决下面new对象不是原子操作的问题
private volatile static DoubleLock instance;
private DoubleLock(){}
//
public static DoubleLock getInstance(){
if(null != instance) return instance;
synchronized (DoubleLock.class){
if(null == instance){
instance = new DoubleLock();//不是原子操作
/**
* 在jvm中有三步
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把这个对象指向这个空间
*
* 可能走123,也有可能走132
* 在多线程环境下,线程a 走完13,分配了虚无的内存空间,线程b此时进入第一层if判断不为空
* 但实际上对象还未完成初始化,可能会造成一些问题
*/
}
}
return instance;
}
}
优点: 优化了上一种方法级锁,减少了部分获取实例的耗时
4.1 双重校验锁进阶版(解决通过反射破坏单例模式问题)
import java.lang.reflect.Constructor;
//第一步,解决一个通过getInstance创建,一个通过反射创建.
public class DoubleLock {
private volatile static DoubleLock instance;
private DoubleLock(){
//解决通过空参构造器创建实例破坏单例模式
//加锁验证
//缺陷:如果两个实例都是通过反射创建则失效
synchronized (DoubleLock.class){
if(null != instance){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
public static DoubleLock getInstance(){
if(null != instance) return instance;
synchronized (DoubleLock.class){
if(null == instance){
instance = new DoubleLock();
}
}
return instance;
}
//不安全,因为可以通过反射获取破环单例
public static void main(String[] args) throws Exception {
DoubleLock instance = DoubleLock.getInstance();
System.out.println(instance);
//通过空参构造器创建
Constructor<DoubleLock> declaredConstructor = DoubleLock.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DoubleLock doubleLock = declaredConstructor.newInstance();
System.out.println(doubleLock);
}
}
import java.lang.reflect.Constructor;
public class DoubleLock {
private volatile static DoubleLock instance;
//增加一个标志,保证空参构造方法只执行一次
private static boolean flag = false;
private DoubleLock(){
//红绿灯
synchronized (DoubleLock.class){
if(flag != false){
flag = true;
}else{
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
public static DoubleLock getInstance(){
if(null != instance) return instance;
synchronized (DoubleLock.class){
if(null == instance){
instance = new DoubleLock();
}
}
return instance;
}
//不安全,因为可以通过反射获取破环单例
public static void main(String[] args) throws Exception {
Constructor<DoubleLock> declaredConstructor = DoubleLock.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DoubleLock doubleLock = declaredConstructor.newInstance();
DoubleLock doubleLock2 = declaredConstructor.newInstance();
System.out.println(doubleLock);
System.out.println(doubleLock2);
}
}
当然也可以通过反射获取字段flag,然后进行修改再创建,从而破坏单例.
结论:道高一尺,魔高一丈.
5.使用类的内部类(线程安全)
public class InnerClassSingleton {
//静态内部类
private static class SingletonHolder{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){}
public static InnerClassSingleton getInstance(){
return SingletonHolder.instance;
}
}
优点:
- 使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能.
- 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载
- 此种方式也是非常推荐使用的一种单例模式
6.枚举(线程安全)
//enum
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
- Effective Java 作者推荐使⽤枚举的⽅式解决单例模式,此种方式可能是平时最少用到的。
- 这种方式解决了最主要的: 线程安全、⾃由串行化、单一实例例。
enum也是类,但是它的空参构造器是假的.