Java单例模式讲解和实现及相关问题总结

什么是单例模式

单例模式是指在内存中只会创建且仅创建一次对象的设计模式
在程序多次使用同一个类的对象且作用相同时,为了防止频繁创建对象使内存飙升,单例模式可以在内存中创建这个类的一个对象,供所有需要用到的地方共享这个对象

特点

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

类型

  • 懒汉式:需要使用这个对象的时候才会去创建这个单例对象
  • 饿汉式:在类加载的时候就创建了这个单例对象

懒汉式单例

在程序使用这个对象的时候,去判断这个对象是否为空,若已存在,就直接返回这个对象,若不存在,就去执行实例化操作
在这里插入图片描述
在第一次调用getInstance方法的时候实例化

public class Singleton {
    public static Singleton singleton = null;

    private Singleton() {
    }

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

懒汉式单例通过使用private修饰构造方法保证Singleton在外部无法被实例化,只能通过调用静态方法getInstance来实例化Singleton的唯一实例
(其实也可以通过反射机制来实例化private构造方法的类,但通过反射都会使所有的单例模式失效,所以在此不做讨论)

以上懒汉式单例是线程不安全的,在并发下可能会出现多个Singleton实例,那么如果实现一个线程安全的懒汉式呢?我们会在后面讲解

饿汉式单例

类加载的时候就创建了这个单例对象,程序需要的时候直接返回这个单例对象即可
在这里插入图片描述
在最开始就已经创建了一个实例对象在内存中,getInstance方法直接返回这个对象即可,当类被

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

    public Singleton() {
    }

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

解决懒汉式线程安全问题

看一下懒汉式的getInstance方法

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

此时如果有两个线程,在调用getInstance方法时都同时判断了singleton为空,那么就会各自实例化一个singleton对象,这就不是单例了,线程不安全

既然会有多个线程同时判断的情况,那我们加上锁不就好了么,由此引出第一种方法

加锁

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

或者

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

加锁之后在多线程的情况下就会竞争锁,就可以保证在同一时间下只有一个线程判断singleton是否为空了,但是就出现了另外一个问题:每次都会去竞争锁,在高并发下性能会很差

这样的话,在方法上加锁的方法就不能用了,因为无论如果都会发生锁竞争的情况,由此引出双重检查锁定这一方法

双重检查锁定

public class Singleton {
    public static Singleton singleton = null;

    private Singleton() {
    }

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

这样就可以同时实现线程安全和性能高效了,分析如下:

  • 第一次判空:

如果singleton不为空,直接返回单例对象,如果singleton为空,线程都进入分支,都不需要竞争锁

  • 加锁:

锁竞争,只有一个线程能抢到锁

  • 第二次判空:

因为singleton可能已经被之前的线程实例化了,所以还要判一次空,如果singleton不为空,直接返回单例对象,如果singleton为空,就会实例化对象

  • 实例化之后:

之后进入该方法的线程都不会再发生锁竞争了,因为在第一次判空的时候就直接返回对象了

此时代码已经很完美了,但是还有一个问题:指令重排

使用volatile防止指令重排

在创建对象的时候,JVM会经历三步:

  • 为singleton分配内存空间
  • 初始化singleton对象
  • 使singleton指向分配的内存空间

指令重排序:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

如果发生指令重排序,可能创建对象时三步的顺序变为了1 3 2,在多线程的情况下,某个线程可能会先进行了1 3步,然后其他线程在判断singleton是否为空的时候,判断结果会是不为空,但是第2步还没有完成呢,导致其他线程得到的是一个还没有初始化的singleton对象
在这里插入图片描述
我们可以使用volatile关键字来防止指令重排序,原理可以自行搜索
使用volatile关键字还可以保证singleton对象的可见性,在同一时间在内存中都是最新的那个值,每次操作这个对象的时候都会获取这个对象的值

public class Singleton {
    public static volatile Singleton singleton = null;

    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) {
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton obj1 = construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton obj2 = Singleton.getInstance(); 
    System.out.println(obj1 == obj2); // false
}

序列化与反序列化

readObject()方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址

public static void main(String[] args) {
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("Singleton.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    Singleton newInstance = (Singleton) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == Singleton.getInstance()); // false
}

枚举实现单例模式

JDK1.5之后,也可以使用枚举来实现单例模式

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("这是枚举单例模式!");
    }
}

优点

  • 对比懒汉式和饿汉式,代码更加简洁
  • 可以保证线程是安全的和对象是单一的

测试一下

public class Test {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.INSTANCE;
        Singleton singleton2 = Singleton.INSTANCE;
        System.out.println(singleton1 == singleton2);
    }
}

在这里插入图片描述

  • 防止反射和序列化来破坏单例模式

在使用反射来调用newInstance()方法时,会先判断是否为枚举类,如果是,会报异常
在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作

总结

  • 两种单例模式:懒汉式和饿汉式
  • 懒汉式:在程序使用这个对象的时候,去判断这个对象是否为空,若已存在,就直接返回这个对象,若不存在,就去执行实例化操作,使用双重检查锁定可以实现线程安全和性能高效
  • 饿汉式:在类加载的时候就创建了这个单例对象,程序需要的时候直接返回这个单例对象即可
  • 内存要求高的话,使用懒汉式,只有需要的时候才实例化
  • 内存要求不高的话,使用饿汉式,比较简单,线程安全和性能也不会下降
  • 指令重排序会导致获取空对象,可以通过volatile关键字来防止指令重排序
  • 使用枚举来实现单例模式是最好的,代码简洁,线程安全,不会由于反射和序列化破坏单例模式
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值