单例模式

单例模式

1、定义

单例模式是一种创建型的设计模式。

采用一定的策略保证整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式常常被用来管理共享的资源,如数据库连接或者连接池。

单例模式的实现有多种方式,接下来将详解常见的几种

单例模式实现

1、饿汉式(静态常量)

实现步骤
  1. 构造器私有化(防止外界new)
  2. 在类的内部创建对象
  3. 对外暴露一个静态方法getInstance,获取对象实例
代码实现
public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){}

	//外界通过该方法获取实例
    public static Singleton getInstance(){
        return INSTANCE;
    }
}

从代码中,我们INSTANCE被static修饰,所以在程序启动的时候就会被创建,即使没有被使用到。这就是饿汉式名称的由来。

接下来就是如何获取对象实例了

public class Test {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        //打印true
        System.out.println(instance1 == instance2);
    }
}
饿汉式优缺点

优点:代码简单,类的实例化过程是在加载的时候完成,所以不存在线程安全问题。

缺点:如果实例没有被使用到,就可能造成内存浪费。

2、懒汉式

定义

为了避免饿汉式可能造成的内存浪费,懒汉式孕育而生。所谓的懒汉式就是单例对象只有在被使用到的时候采用创建。

懒汉式的实现方式也有多种,下面介绍俩种线程安全的写法:

使用synchronized实现线程安全
class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

synchronized加在方法级别上虽然可以解决线程安全,但是存在效率问题。因为当synchronized加在方法级别上时,意味着每次进入该方法的时候都需要进行加锁。但是实际上,我们只有第一次调用的时候才需要进行同步加锁,所以会存在性能损耗。

使用双重检查可以解决这个问题。

使用双重检查实现线程安全
public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton(){}

    public static Singleton getInstance(){
        if (INSTANCE == null){
            synchronized (Singleton.class){
                if (INSTANCE == null){
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

在双重检查机制中,第一次调用时,INSTANCE == null条件成立,会进行同步,后面的的调用将不会进入if代码块中,也就不会有同步。

这里之所以要使用俩个if是因为在多线程环境下,可能同时多个线程进入该方法,同时通过了第一个if判断。但当第一个线程创建后,后面的线程就不该创建了,所以需要第二个if来进行判断。

3、静态内部类实现

代码实现
class Singleton {
	private static volatile Singleton instance;
	
	//构造器私有化
	private Singleton() {}
	
	//写一个静态内部类,该类中有一个静态属性 Singleton
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
	
	//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
	public static synchronized Singleton getInstance() {
		return SingletonInstance.INSTANCE;
	}
}

public class Test {

    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2);
    }
}
分析

这里利用了静态内部类的加载机制:

  1. 外部类初次加载时,会初始化静态变量、静态代码块、静态内部类,但不会加载内部类和静态内部类
  2. 调用外部类的静态方法、静态变量时,外部类必须先进行加载,但只加载一次
  3. 直接调用静态内部类时,外部类不会加载

综上,由于静态内部类只有在使用到的时候,才会被加载,所以instance是在使用到的时候才会被创建。此外由于是JVM加载的时候创建,所以也是线程安全的。

结论,静态内部类实现单例拥有以下特点:

  • 属于懒加载
  • 线程安全
静态内部类可能存在的问题

不止静态内部类,之前说的单例实现在一些特殊情况下都可能出现问题:

  • 使用反射强行创建对象
  • 使用序列化和反序列化导致创建多个对象实例

使用反射破坏单例模式:

   public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //使用静态内部类获取单例对象
        Singleton instance = Singleton.getInstance();

        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        // 通过反射强行创建对象
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println(singleton == instance);//打印false
    }

使用序列化反序列化破坏单例:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
        //使用静态内部类获取单例对象
        Singleton instance = Singleton.getInstance();

        //序列化
        ByteArrayOutputStream aos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(aos);
        oos.writeObject(instance);

        //反序列化
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(aos.toByteArray());
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Singleton singleton = (Singleton) objectInputStream.readObject();
        //打印false
        System.out.println(instance == singleton);

        //关闭资源...
    }

https://blog.csdn.net/qq_28181131/article/details/82258877

4、枚举实现单例模式

enum Singleton {
	INSTANCE; //属性
	public void sayOK() {
		System.out.println("ok~");
	}
}

使用枚举的优势:

  • 借助DK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
  • 枚举可以防止反射强行创建多个实例

为什么单例可以防止序列化重新创建新的对象
为什么单例可以防止反射强行创建多个实例
枚举可以在研究研究。

问题环节
1、能不能创建一个类,并把所有的方法和变量都定义为静态的,把类直接当作一个单例?

如果你的类完全自给自足,而且不依赖复杂的初始化,那么你可以这么做。但是,因为静态初始化的控制权在Java手上,这么做有可能导致混乱,特别是当许多类涉及其中的时候,常常会造成一些微妙的、不容易发现的与初始化次序相关的BUG。除非你有绝对的必要使用类的单例,否则还是建议使用对象的单例,这样比较保险。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值