单例模式详解

单例模式

单例模式(Singleton Pattern)是 Java 中23种设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
应用实例:
1、一个班级只有一个班主任。
2、一个国家只能有一个领导人
3、2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。

实现方式

最为熟知的莫过于饿汉式懒汉式了。

1)饿汉式

是否 Lazy 初始化:否
是否多线程安全:是
特点:典型的以空间换时间,它在类加载的过程就实例化对象,不管你使不使用都先创建出来。
优点:线程安全且没有加锁,执行效率高,实现简单
缺点:类加载时就初始化,可能之后并不会使用而浪费内存

public class Singleton {
    private static Singleton singleton=new Singleton();

    private Singleton(){

    }

    public static Singleton getInstance(){
        return singleton;
    }
}

2)懒汉式(线程不安全)

是否 Lazy 初始化:是
是否多线程安全:否
特点:最基本的实现方式,实现简单。但是在多线程环境下会产生并发问题,不适用于多线程。

public class Singleton {
    private static Singleton singleton;

    private Singleton(){

    }

    public static Singleton getInstance(){
        if (singleton==null){
            singleton=new Singleton();
        }
        return singleton;
    }
}

3)DCL懒汉式(线程安全)

是否 Lazy 初始化:是
是否多线程安全:是
特点:DCL全称为double-checked locking(双重检测锁),它采用双锁机制,在保证多线程安全的同时也能提升执行效率。

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {

    }

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

介绍:这种经典的多线程下实现单例模式的方式。

(1)两层判断的作用?

在getInstance()方法内它进行了两次判断,内层的判断用于保证对象只能在第一次调用时初始化(即确保单例),外层的判断使得程序效率大幅提升,因为在synchronized 锁的同步下,每一次线程访问getInstance()都会被阻塞(即使对象已经被创建),而加上外层判断则保证了对象在创建完成后不必访问同步代码块了,从而提升效率

(2)为什么singleton的定义要加上volatile关键字?

这个就要从volatile的作用去回答了。说到volatile大家一定能会想起它的三个特点:保证可见性,不保证原子性、指令重排序。这个关键字涉及到JMM与缓存一致型协议,具体详情可以参考:Java并发编程:volatile关键字解析,这里使用volatile就是为了防止指令重排序

对于指令重排序,可以举个简单的例子:

public class test {
    public static void main(String[] args) throws InterruptedException {
        int a=1;      // 步骤1
        int b=2;	  // 步骤2
        int c=a+b;    // 步骤3
    }
}

在上面的例子中,由于步骤1和步骤2之间并没有相互依赖的关系,因此它们是有可能相互交换位置的,而步骤3则依赖于步骤1和步骤2,所以步骤3不会参与交换。最终指令的顺序可能是1,2,3也有可能是1,3,2。

而在创建对象(singleton = new Singleton())的时候会经过三个步骤:
1、分配内存空间
2、执行构造方法,初始化对象
3、把singleton指向这个空间
这里可以看到步骤3和步骤2是没有相互依赖关系的,因此指令重排后可能会出现1,3,2的顺序,这在单线程下是绝对没问题的,但在多线程的时候就会出错。
假设有线程1和线程2,线程1先执行,当它执行到步骤三时(注意现在是1,3,2的顺序),singleton已经指向了一个对象,但是这个对象想在还没有初始化(还没执行步骤2)。此时轮到线程2执行,它在进行到 if (singleton == null) 这步时,由于singleton已经指向一个对象了,因此它会直接返回这个对象,但此时对象还没有初始化。

所以为了保证安全,我们必须要在定义时加上volatile关键字。

(3)DCL懒汉式一定能保证单例吗?

对于多线程环境,DCL懒汉式确实解决了并发问题,但是它仍然无法确保单例。破坏单例的实现方式有反射clone方法序列化对象方式,这些都可能导致单例失效。后两种我们可以通过不实现相应接口来解决,但私有属性和方法对于反射就相当于一层纸,可以轻易破坏。
反射破坏单例:

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {

    }

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

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton instance1 = Singleton.getInstance();
        Constructor<? extends Singleton> constructor = instance1.getClass().getDeclaredConstructor();
        constructor.setAccessible(true);        // 无视私有构造器
        Singleton instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果如下:

Singleton@1540e19d
Singleton@677327b6

可以看到这两个对象是不一样的,因此单例模式又失效了,为了解决这个问题,我们可以在设一个静态变量,并在私有构造器中进行判断
设置一个静态flag:

public class Singleton {
    private volatile static Singleton singleton;

    private static boolean flag=false;      // 定义一个静态类型的flag变量

    private Singleton() {
        synchronized (Singleton.class){
            if (!flag){          // 判断对象是否创建过
                flag=true;      
            }else {
                throw new RuntimeException("请别用反射破坏单例模式");
            }
        }
    }

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

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton instance1 = Singleton.getInstance();
        Constructor<? extends Singleton> constructor = instance1.getClass().getDeclaredConstructor();
        constructor.setAccessible(true);        // 无视私有构造器
        Singleton instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果如下:

Exception in thread "main" java.lang.reflect.InvocationTargetException <4 internal class>
	at Singleton.main(Singleton.java:38)
Caused by: java.lang.RuntimeException: 请别用反射破坏单例模式
	at Singleton.<init>(Singleton.java:18)
	... 5 more

这样表面上解决了通过反射破坏单例的问题,但是若知道了这个静态变量的名字,仍然可以采用反射的形式破坏单例:

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        Singleton instance1 = Singleton.getInstance();
        Constructor<? extends Singleton> constructor = instance1.getClass().getDeclaredConstructor();
        constructor.setAccessible(true);        // 无视私有构造器
        
        Field flag = instance1.getClass().getDeclaredField("flag");     // 通过名字获取静态变量
        flag.setAccessible(true);               // 无视私有
        flag.set(instance1,false);
        
        Singleton instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }

结果如下:

Singleton@677327b6
Singleton@14ae5a5

因此,即使我们深思熟虑,但仍然可能导致单例失效。

4)静态内部类

是否 Lazy 初始化:是
是否多线程安全:是
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。相比于饿汉式,由于由于内部类在不被访问的情况下是不会被加载的,因此它能够实现延迟加载,节省内存资源。

public class Singleton {
    private Singleton() {}

    public static Singleton getInstance() {
        return innerClass.singleton;
    }

    private static class innerClass {
        private static final Singleton singleton=new Singleton();
    }
}

这种方式仍然可以采用反射、克隆与序列化的方式破坏单例。

克隆与序列化破坏单例
public class Singleton implements Serializable, Cloneable {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return innerClass.singleton;
    }

    private static class innerClass {
        private static final Singleton singleton = new Singleton();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, CloneNotSupportedException {
        Singleton instance1 = Singleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        oos.writeObject(instance1);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        Singleton instance2 = (Singleton) ois.readObject();		// 序列化

        Singleton instance3= (Singleton) instance1.clone();		// 克隆

        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);
    }
}

结果如下:

Singleton@6d6f6e28
Singleton@6d03e736
Singleton@568db2f2

5)枚举

JDK 版本:JDK1.5
是否 Lazy 初始化:否
是否多线程安全:是
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor();
        EnumSingle instance2 = constructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果如下:

Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at EnumSingle.main(EnumSingle.java:17)

可以看到,使用枚举能够很好地解决反射破坏单例的隐患。

另外,枚举这种方式实现自动序列化机制,防止序列化创建对象。

public enum EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, CloneNotSupportedException {
        EnumSingle instance1 = EnumSingle.INSTANCE;

        // 序列化对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        oos.writeObject(instance1);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        Singleton instance2 = (Singleton) ois.readObject();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

结果如下:

Exception in thread "main" java.lang.ClassCastException: EnumSingle cannot be cast to Singleton
	at EnumSingle.main(EnumSingle.java:23)

单例模式的拓展(多例)

要求:创建一个门票类,开启3个线程进行售卖,总共只有500张票。
这里采用DCL懒汉式实现。
门票类:

import java.util.Date;

public class Tickets {
    private static int count = 0;	// 以售卖票数
    private final static int num = 500;		// 门票数目
    private Date time;
    private Double price;

    private Tickets() {
        count++;
    }
	
    private Tickets(Date time, Double price) {
        this.time = time;
        this.price = price;
        count++;
    }

	// 无参
    public static Tickets createTickets(){
        if (count<num){
            synchronized (Tickets.class){
                if (count<num){
                    System.out.println("正在卖出第"+(count+1)+"张票");
                    return new Tickets();
                }else {
                    return null;
                }
            }
        }
        return null;
    }

	// 有参
    public static Tickets createTickets(Date time, Double price){
        if (count<num){
            synchronized (Tickets.class){
                if (count<num){
                    System.out.println("正在卖出第"+(count+1)+"张票");
                    return new Tickets(time,price);
                }else {
                    return null;
                }
            }
        }
        return null;
    }

    // set/get方法
}

测试类:

public class test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(()->{
                while (true){
                    Tickets tickets = Tickets.createTickets();
                    if (tickets==null){
                        System.out.println("对不起,门票已经卖完");
                        break;
                    }
                }
            }).start();
        }
    }
}

测试结果:

正在卖出第1张票
正在卖出第2张票
正在卖出第3张票
...
正在卖出第498张票
正在卖出第499张票
正在卖出第500张票
对不起,门票已经卖完
对不起,门票已经卖完
对不起,门票已经卖完

Process finished with exit code 0

总结

  1. 在单线程的环境中上面各种方法都是没有问题的。但在多线程环境时懒汉式则会出现并发问题,需要采用DCL懒汉式,一般来说不建议采用普通的懒汉式;
  2. 不在意内存的使用或者确保对象一定使用,则可以直接采用饿汉式,方便简单;
  3. 如果特别指明需要延迟加载,可以采用DCL懒汉式或者静态内部类的形式;
  4. 如果涉及到反序列化创建对象或者考虑反射,可以枚举方式(最佳)。

参考

Java设计模式之饿汉式和懒汉式
菜鸟教程:单例模式
java序列化,看这篇就够了
狂神说JUC笔记
Java并发编程:volatile关键字解析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值