单例模式~

1. 单例模式

1.1 介绍

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的特点

  • 构造方法私有、实例化的变量引用私有、获取实例的方法共有

1.2 优缺点

优点

  • 在内存中只有一个实例,减少了内存的开销,尤其是频繁地创建和销毁实例
  • 避免对资源的多重占用

缺点

  • 没有接口,不能继承,与单一职责原则冲突,一个类不应该关心外面怎么样来实例化
  • 容易出现线程安全问题

1.3 使用场景

  • 主要为了避免一个全局使用的类频繁创建和销毁

1.4 注意事项

2. 代码实现

饿汉模式 :初始化时创建,线程安全,但浪费资源

public class Singleton
{
    private static Singleton instance = new Singleton(); // 类加载时初始化,浪费资源
    private Singleton(){};
    
    public static Singleton getInstance()
    {
        return instance;
    }
}

懒汉模式 :用时创建,线程不安全

public class Singleton
{
    private static Singleton instance;
    
    private Singleton(){};
    
    // 对外提供该类实例
    public static Singleton getInstance()
    {
        if(instance == NULL)
        {
            instance = new Singleton();
        }
        
        return instance;
    }
}

懒汉模式:线程安全,但开销大

public class Singleton
{
    private static Singleton instance;
    
    private Singleton(){};
    
    // 对外提供该类实例
    public static synchronized Singleton getInstance()
    {
        if(instance == NULL)
        {
            instance = new Singleton();
        }
        
        return instance;
    }
}

双重校验锁:线程安全,需要禁止指令重排,不防反射/序列化

public class Singleton
{
    // 指令重排是指JVM在实例化对象时会将如下三步重新排序:
	// 1. 分配内存空间  2. 初始化对象  3. 将初始化对象指向分配的内存空间    
    private volatile static Singleton singleton;    // 给变量加锁,禁止指令重排
    private Singleton(){};
    
    public static Singleton getInstance()
    {
        if(singleton == NULL)
        {
            synchronized(Singleton.class)  // 这个地方的锁被当前线程占领直至创建对象成功   	 
            {
                if (singleton == null) 	// 为什么要两次判空
                {  
                    singleton = new Singleton();  
                }
            }
        }
        
        return instance;
    }
}

为什么要两次判空:

首先介绍两者的功能,第一个if是为了减少性能开销,第二个if是为了避免生成多个实例。

  • 如果只有外面的if,则会生成多个对象,

  • 如果只有里面的if,则每次都会加锁并判空。

枚举类型:初始化时创建,线程安全,防反射/序列化

public enum Singleton{
    //定义1个枚举的元素,即为单例类的1个实例
    INSTANCE;

    // 隐藏了1个空的、私有的 构造方法
    // private Singleton () {}
}

// 获取单例的方式:
Singleton singleton = Singleton.INSTANCE;

登记式/静态内部类:使用静态内部类,即避免了类加载时实例化对象,同时可以实现单例模式。

public class Singleton
{
    private Singleton (){};
    
    // 静态内部类会在第一次使用时才被加载和初始化
    private static class SingletonHolder 
    {  
        private static final Singleton INSTANCE = new Singleton();
    }  

    
    public static final Singleton getInstance() 
    {  
        return SingletonHolder.INSTANCE;
    } 
}

静态内部类和双重检验锁的比较:

在早期的JVM中,同步(甚至是无竞争的同步)都存在着巨大的性能开销。因此,人们想出来了许多“聪明的”技巧来降低同步的影响,有些技巧很好,有些技巧是不好的,甚至是糟糕的,DCL就属于“糟糕”的一类。

DCL的真正问题在于:当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕的事情只是看到一个失效值(在这种情况下是一个空值),此时DCL方法将通过在持有锁的情况下再次尝试来避免这种风险。然而实际情况远比这种情况糟糕——线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。也就是说,JVM早期的对象创建很慢,因此可能在持有锁时读到空对象。

然而,DCL的这种使用方法已经被广泛地废弃了——促使该模式出现的动力(无竞争同步的执行速度很慢,以及JVM启动时很慢)已经不复存在了,因为它不是一种高效地优化措施。延迟初始化占位类模式(静态内部类)能带来同样的优势,并且更容易理解。

通过反射破坏单例模式

public static void main(String[] args) {
    Class<Girlfriend_2> clazz = Girlfriend_2.class;
    try {
        // 通过反射获取私有构造方法
        Constructor<Girlfriend_2> declaredConstructor = clazz.getDeclaredConstructor(null);
        // 强制访问
        declaredConstructor.setAccessible(true);
        
        // 通过修改后的构造方法创建实例
        Girlfriend_2 girlfriend1 = declaredConstructor.newInstance();
        Girlfriend_2 girlfriend2 = declaredConstructor.newInstance();
        
        System.out.println(girlfriend1);
        System.out.println(girlfriend2);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

通过序列化/反序列化破坏单例模式

public static void main(String[] args) throws Exception {
    Singleton_2 instance = Singleton_2.getInstance3();
    
    // 序列化
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("temp"));
    oos.writeObject(instance);
    oos.flush();
    oos.close();

    // 反序列化
    FileInputStream fis = new FileInputStream("temp");
    ObjectInputStream ois = new ObjectInputStream(fis);
    Singleton_2 object = (Singleton_2) ois.readObject();
    ois.close();

    System.out.println(instance);	// 两者一定不同
    System.out.println(object);
}

因为readObject内部通过反射新建了一个实例,并返回。可以通过在单例类中重写readResolve方法防止通过序列化破坏单例模式

private Object readResolve(){
    return instance;
}

通过枚举类型避免反射/序列化破坏单例模式

反序列化中通过反射构造对象,而枚举类天然地避免了反射攻击。

3. 源码实现

3.1 java.lang.Runtime

public class Runtime {
    private static Runtime currentRuntime = new Runtime();	// 饿汉式

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}

3.2 Spring中的Bean

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值