Effective Java学习笔记--单例(Singleton)及其属性的增强

什么是单例(Singleton)

Singleton就是指仅仅被实例化一次的类。他是一种设计模式(而不是一种专有的类型),属于创建型模式的一种,这种设计模式主要是控制类的实例化过程,确保在应用程序运行期间,对于某个特定类全局只存在一个实例,并且提供一个全局访问点来获取这个实例。这也就引出了单例模式的设计核心:如何确保全局只有一个实例。

单例模式的实现方式

  • 饿汉式:类加载时就完成初始化,这种方式没有线程安全的问题,但是会造成资源的浪费。
public class Singleton1 {
    private static Singleton1 instance = new Singleton1();

    private Singleton1(){};

    public static Singleton1 getInstance()
    {
        return instance;
    }
}
  • 懒汉式:需要手动通过静态工厂方法等构造方法进行初始化,实现了延时加载,但是需要考虑线程安全的问题。
public class Singleton1 {
    private static Singleton1 instance;

    private Singleton1(){};

    public static synchronized Singleton1 getInstance()
    {
        if (instance==null) {
            instance = new Singleton1();
        }
        return instance;
    }
}
  • 双重检查锁定(Double-Checked Locking):结合懒汉式和性能优化,通过双重检查确保线程安全的同时减少锁的开销。
public class Singleton1 {
    private static volatile Singleton1 instance;

    private Singleton1(){};

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

相比于懒汉式在调用getInstance方法时就调用同步锁,双重检查锁定是在第一次判断instance为空的所有线程里加同步锁,节约了锁资源。

  • 静态内部类:利用Java类加载机制保证初始化的线程安全和延时加载。这里应用了Java类加载机制的特性来确保单例实例仅在真正需要时才被创建,实现了延时加载,同时应用了类加载的线性安全性,实现了线性安全。
public class Singleton1 {
    private Singleton1(){};

    private static class SingletonHolder {
        private static final Singleton1 instance = new Singleton1();
    }

    public static Singleton1 getInstance() {
        return SingletonHolder.instance;
    }
}

这里回顾一下类的加载机制:

类的加载时机:在Java中,类只有在首次被“主动引用”时才会被加载。静态内部类不会随着外部类的加载而加载,它有自己的加载时机,及首次被访问时(静态内部类也只有在被调用时(getInstance方法的return SingletonHolder.instance)才会加载)。

类加载的唯一性:Java中的类只会在首次被引用时加载一次,之后这个类及其静态成员都不会再次被加载。静态内部类也不例外,它作为外部类的一部分,会在其首次被访问时被加载到JVM中。因此,静态内部类中的静态成员(在这个场景中是单例实例)也会随之初始化。

  • 枚举:Java中使用枚举来实现单例是最简洁且线程安全的方式。因为JVM构建枚举实例本身就是线程安全的,而且对于instance的构建会默认隐式调用构造函数。
public enum Singleton2 {
    // 定义枚举实例,此实例在类加载时自动创建,保证全局唯一
    instance;

    // 提供公共的静态方法,用于获取单例实例
    public static Singleton2 getInstance() {
        return instance; // 直接返回枚举实例
    }

    // 枚举类型的构造方法默认是私有的,此处显式声明是为了强调这一点
    // 构造方法在枚举实例定义时自动调用,外部无法直接访问
    private Singleton2(){};
}

实现单例需要注意什么

单例模式最需要保障的就是实例的唯一性,最核心的两个方法就是初始化和引用。初始化方法要考虑到资源浪费和线程安全这两个因素,引用要考虑保障线程安全。

  • 单例的初始化方式:是在类加载时初始化还是通过静态方法初始化,两者各有利弊。
  • 单例的引用方式:单例的引用一般通过静态方法,但是这里要重点考虑的是线程安全。

除此之外,单例模式为了保障实例的唯一性,还需要考虑其他的因素来增强其属性,首先是类的构造器。

通过私有化构造器可以进一步强化单例属性

因为构造器只能在类中被单例的初始化方式调用,所以保证了单例的全局唯一性。但是书中提醒享有特权的客户端可以通过AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,所以如果想要抵御这项攻击,就需要在私有构造器里设置异常处理方法(在创建第二个实例时抛出异常)。

对于需要序列化和反序列化的对象需进行的操作

仅仅实现Serializable接口并不能在可序列化和反序列化过程中很好的保持单例特性。这里就要实现Object的readReslove方法,这个方法是 Java 序列化机制中的一个回调方法,它在对象反序列化过程中被调用,允许开发者自定义反序列化后的对象。当 ObjectInputStream 读取一个对象并准备返回时,它会检查该对象是否定义了 readResolve()方法。如果定义了,那么 ObjectInputStream 会调用该方法,并使用其返回的对象替换原本通过反序列化创建的对象。这里就避免了反序列化默认创建新的对象。

    private Object readResolve() {  
        return instance;  
    }  

注意这里的readResolve方法定义为私有方法,该方法会由JVM隐式调用。

文中作者还提到了需要将所有的实例域都声明为瞬时(transient)的。我们知道声明为瞬时的属性在序列化的时候并不会被写入字节码,这也就保证了实例域在序列化时的状态不会被反序列化时写回主程序。另一方面,也有效避免了单例新增一个分支版本。(但是,我个人觉得对于主程序的单例唯一性并不会有影响,因为readResolve方法在反序列化的时候就已经保证返回一个指向主程序instance的引用,就算反序列化中带着原来实例域的状态,由于有readResolve方法的实现也不会被创建出来,以下是我的验证,请大家批评指正)

带有实例域的Singleton类,其中实例域未声明为瞬时。

import java.io.Serializable;

public class Singleton implements Serializable{
    private String name;//实例域,没有transient
    private String password;//实例域,没有transient

    private Singleton(){};
    private Singleton(String name, String password){
        this.name = name;
        this.password = password;
    };

    private static Singleton instance = new Singleton("JIM", "123");
    public static Singleton getInstance(){
        return instance;
    }

    private Object readResolve(){
        return instance;
    }

    public static String getPassword() {
        return instance.password;
    }

    public static void setPassword(String password) {
        instance.password = password;
    }
}

客户端程序

package Singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/*
 * 这里先创建了一个单例的引用:singleton,在序列化后,修改了主程序password的状态,
 * 然后反序列化并创建第二个单例的引用singleton2,发现两者依然指向相同的对象。
 */
public class SingletonApplication {
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
        oos.writeObject(singleton);

        singleton.setPassword("321");

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
        Singleton singleton2 = (Singleton) ois.readObject();

        
        System.out.println(singleton == singleton2);

    }
}

输出结果

(base) MacBook-Pro:Singleton User$ java Singleton/SingletonApplication
true
  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值