单例模式
原文链接:https://www.kuangstudy.com/bbs/1374623824745635841
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
单例模式的五种实现方式,分别为:饿汉模式、懒汉模式、DCL懒汉式、静态内部类单例、枚举单例,下面一一介绍每种方式
一、饿汉式
饿汉模式,可以想象一个很饿的人,需要立马吃东西,饿汉模式便是这样,在类加载时就创建对象,由于在类加载时就创建单例,因此不存在线程安全问题
//饿汉式
public class SingletonDemo1 {
//私有化构造器
private SingletonDemo1() {
}
//类初始化时立即加载该对象
private static SingletonDemo1 instance = new SingletonDemo1();
//提供公共的获取方法,由于静态的instance在类加载时就创建,因此不存在线程安全问题
public static SingletonDemo1 getInstance() {
return instance;
}
}
//测试
class SingletonDemo1Test {
public static void main(String[] args) {
SingletonDemo1 instance = SingletonDemo1.getInstance();
SingletonDemo1 instance1 = SingletonDemo1.getInstance();
System.out.println(instance == instance1); //输出true
}
}
但饿汉式也存在一定的问题,即如果在该类里面存在大量开辟空间的语句,如很多数组或集合,但又不马上使用他们,这时这样的单例模式会消耗大量的内存,影响性能
二、懒汉式
顾名思义,懒汉式,就是懒,即在类加载时并不会立马创建单例对象,而是只生成一个单例的引用,即可以延时加载
//懒汉模式
public class SingletonDemo2 {
//私有化构造器
private SingletonDemo2() {
}
//只提供一个实例,并不创建对象
private static SingletonDemo2 instance;
//提供公共的获取方法,因为不是在类加载时就创建对象,因此存在线程安全问题,使用synchronized关键字保证线程安全,效率降低
public static synchronized SingletonDemo2 getInstance() {
if (instance == null) {
instance = new SingletonDemo2();
}
return instance;
}
}
//测试
class SingletonDemo2Test {
public static void main(String[] args) {
SingletonDemo2 instance = SingletonDemo2.getInstance();
SingletonDemo2 instance1 = SingletonDemo2.getInstance();
System.out.println(instance == instance1); //输出true
}
}
懒汉式使用同步锁锁住了整个方法,效率较低
三、DCL懒汉式(双重检测锁模式)
同样是在类加载时只提供一个引用,不会直接创建单例对象,不需要对整个方法进行同步,缩小了锁的范围,只有第一次会进入创建对象的方法,提高了效率
//DCL懒汉式(双重检测锁模式)
public class SingletonDemo3 {
//私有化构造器
private SingletonDemo3() {
}
//只提供一个实例,并不创建对象
//使用避免指令重排带来的线程安全问题
//volatile:对于同一个变量,在一个线程中值发生了改变,则在另一个线程中立即生效,可以大幅度避免下面的问题,不排除极端情况
private static volatile SingletonDemo3 instance;
//提供公共的获取方法,因为不是在类加载时就创建对象,因此存在线程安全问题,使用同步代码块提高效率
//现在不需要对整个方法进行同步,缩小了锁的范围,只有第一次会进入创建对象的方法,提高了效率
//当第一个线程执行到创建对象的方法时,但还未出方法返回,此时第二个线程进入,发现instance不为空,但第一个线程此时还未出去,可能发送意想不到的安全问题
public static SingletonDemo3 getInstance() {
if (instance == null) {
synchronized (SingletonDemo3.class) {
if (instance == null) {
instance = new SingletonDemo3();
}
}
}
return instance;
}
}
//测试
class SingletonDemo3Test {
public static void main(String[] args) {
SingletonDemo3 instance = SingletonDemo3.getInstance();
SingletonDemo3 instance1 = SingletonDemo3.getInstance();
System.out.println(instance == instance1); //输出true
}
}
由于JVM底层内部模型的原因,偶尔会出现问题,因此不建议使用
四、静态内部类式
使用静态内部类解决了线程安全问题,并实现了延时加载
//静态内部类实现
public class SingletonDemo4 {
private SingletonDemo4() {
}
//不会在外部类初始化时就直接加载,只有当调用了getInstance方法时才会静态加载,线程安全,final保证了在内存中只有一份
private static class InnerClass{
private static final SingletonDemo4 instance = new SingletonDemo4();
}
public static SingletonDemo4 getInstance() {
return InnerClass.instance;
}
}
//测试
class SingletonDemo4Test {
public static void main(String[] args) {
SingletonDemo4 instance = SingletonDemo4.getInstance();
SingletonDemo4 instance1 = SingletonDemo4.getInstance();
System.out.println(instance == instance1); //输出true
}
}
五、枚举单例
严格意义上来说以上四种方式实现的单例模式都不是线程安全的,因为反射机制的存在,反射可以破坏私有属性,并且通过反射创建对象,举个例子,通过反射破坏上面的静态内部类方式实现的单例模式
import java.lang.reflect.Constructor;
//静态内部类实现
public class SingletonDemo4 {
private SingletonDemo4() {
}
//不会在外部类初始化时就直接加载,只有当调用了getInstance方法时才会静态加载,线程安全,final保证了在内存中只有一份
private static class InnerClass{
private static final SingletonDemo4 instance = new SingletonDemo4();
}
public static SingletonDemo4 getInstance() {
return InnerClass.instance;
}
}
//测试
class SingletonDemo4Test {
public static void main(String[] args) throws Exception{
SingletonDemo4 instance = SingletonDemo4.getInstance();
SingletonDemo4 instance1 = SingletonDemo4.getInstance();
System.out.println(instance == instance1); //true
Constructor<SingletonDemo4> declaredConstructor = SingletonDemo4.class.getDeclaredConstructor();
//关闭权限检测
declaredConstructor.setAccessible(true);
SingletonDemo4 instance2 = declaredConstructor.newInstance();
System.out.println(instance == instance2); //false
}
}
由上面的例子可以得出,反射是可以破坏以上四种的单例模式(这里不一一演示)
那怎样才能解决这个问题呢,我们来看一下反射创建对象的newInstance()方法:
从源码中可以看出,当反射遇到枚举时直接抛出异常,因此,枚举是创建单例的不二之选
//枚举方式实现单例模式
public enum SingletonDemo5 {
INSTANCE;
public static SingletonDemo5 getInstance() {
return INSTANCE;
}
}
class SingletonDemo5Test {
public static void main(String[] args) {
SingletonDemo5 instance = SingletonDemo5.getInstance();
SingletonDemo5 instance1 = SingletonDemo5.getInstance();
System.out.println(instance == instance1); //true
}
}
我们这是再使用反射尝试破坏一下到单例,会发现不能成功
六、五种实现单例模式的方式的对比
- 饿汉式:线程安全(不排除反射),调用效率高,不能延时加载
- 懒汉式:线程安全(不排除反射),调用效率不高,可以延时加载
- DCL懒汉式:由于JVM底层模型原因,偶尔出现问题,不建议使用
- 静态内部类式:线程安全(不排除反射),调用效率高,可以延时加载
- 枚举单例:线程安全,调用效率高,不能延时加载
七、单例模式常见场景
- Windows的任务管理器、回收站等
- servlet中每个servlet都是单例
- 数据库连接池一般都是单例的
- Spring中每个Bean都是单例的
版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明,KuangStudy,以学为伴,一生相伴!
原文链接:https://www.kuangstudy.com/bbs/1374623824745635841