常用设计模式-单例模式

单例模式 (保证只有一个实例,并只提供一个访问实例的访问点)

单例模式的创建方式:

  • 饿汉模式-静态变量
package com.pattern;

//饿汉模式-静态变量
public class Singleton {
    
    // 类初始化时,会立即加载该对象,线程天生安全,调用效率高
    private static final Singleton singleton = new Singleton();
    
    private Singleton() {
        System.out.println("私有Singleton构造参数初始化");
    }
    
    public static Singleton getInstance() {
        return singleton;
    }
}

由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所有写入操作已经完成,所以保证了JVM层面的线程安全。

  • 饿汉模式-静态代码块

 

package com.pattern;

//饿汉模式-静态代码块
public class Singleton {
    
    private static  Singleton singleton;
    
    private Singleton() {
        System.out.println("私有Singleton构造参数初始化");
    }
    
    static {
        try {
            //Do something ... //new 放在static代码块里可初始化一些变量或者读取一些配置文件等
             singleton = new Singleton()
        }catch (Exception e){
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }
    public static Singleton getInstance() {
        return singleton;
    }
}
  • 懒汉式-单线程
package com.singleton;

//懒汉式-单线程
public class Singleton {
    
    //类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能
    private static Singleton singleton;

    private Singleton() {
        System.out.println("私有Singleton构造参数初始化");
    }
    
    public  static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

只适用于单线程场景,多线程情况下可能发生线程安全问题,导致创建不同实例的情况发生。如果是多线程同时调用getInstance(),会有并发问题啊,多个线程可能同时拿到instance == null的判断,这样就会重复实例化,单例就不是单例。

解决见下面

  • 懒汉式-synchronized
package com.singleton;

//懒汉式-synchronized
public class Singleton {
    
    private static Singleton singleton;

    private Singleton() {
        System.out.println("私有Singleton构造参数初始化");
    }
    //synchronized 保证了同步访问该方法,严格串行制,性能降低
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
  • 懒汉式-Double check-volatile
package com.singleton;

//懒汉式-Double check-volatile
public class Singleton {
    
    private static volatile Singleton singleton;

    private Singleton() {
        System.out.println("私有Singleton构造参数初始化");
    }
    //该方式通过缩小同步范围提高访问性能,同步代码块控制并发创建实例。采用双重检验(内外两个判空)
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

内层的判空的作用:

当两个线程同时执行第一个判空时,都满足的情况下,都会进来,然后去争锁,假设线程1拿到了锁,执行同步代码块的内容,创建了实例并返回,释放锁,然后线程2获得锁,执行同步代码块内的代码,因为此时线程1已经创建了,所以线程2虽然拿到锁了,如果内部不加判空的话,线程2会再new一次,导致两个线程获得的不是同一个实例。线程安全的控制其实是内部判空在起作用。

可以只加内层判空是ok的

外层的判空的作用:

  • 内层判空已经可以满足线程安全了,加外层判空的目的是为了提高效率。
  • 因为可能存在这样的情况:如果不加外层判空,线程1拿到锁后执行同步代码块,在new之后,还没有释放锁的时候,线程2过来了,它在等待锁(此时线程1已经创建了实例,只不过还没释放锁,线程2就来了),然后线程1释放锁后,线程2拿到锁,进入同步代码块中,判空不成立,直接返回实例。
  • 这种情况线程2是不是不用去等待锁了?因为线程1已经创建了实例,只不过还没释放锁。
  • 所以在外层又加了一个判空就是为了防止这种情况,线程2过来后先判空,不为空就不用去等待锁了,这样提高了效率。

volatile作用:

  • 在多线程的情况下,双重检查锁模式可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作,因为new那行代码并不是一个原子指令,会被分割成多个指令。
  实例化对象实际上可以分解成以下4个步骤:
    1. 为对象分配内存空间
    2. 初始化默认值(区别于构造器方法的初始化)
    3. 执行构造器方法
    4. 将对象指向刚分配的内存空间
  编译器或处理器为了性能的原因,可能会将第3步和第4步进行重排序:
    1. 为对象分配内存空间
    2. 初始化默认值
    3. 将对象指向刚分配的内存空间
    4. 执行构造器方法线程可能获得一个初始化未完成的对象

 

  • 静态内部类
package com.singleton;

// 静态内部类方式
public class Singleton {
    
    //结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
    private Singleton() {
        System.out.println("私有Singleton构造参数初始化");
    }
    
    public static class SingletonClassInstance {
        private static final Singleton singleton = new Singleton();
    }
    
    // 方法没有同步
    public static Singleton getInstance() {
        return Singleton.singleton;
    }
}

该方式是线程安全的,适用于多线程,利用了java内部类的特性:

  静态内部类不会自动随着外部类的加载和初始化而初始化,内部类是要单独加载和初始化的。此方式单例对象是在内部类加载和初始化时才创建的,因此它是线程安全的,且实现了延迟初始化

 

  • 枚举单例式

枚举是最简洁的,不需要考虑构造方法私有化。

值得注意的是枚举类不允许被继承,因为枚举类编译后默认为final class,可防止被子类修改。

。枚举类是利用JVM类加载机制来保证线程安全的(细节见这篇),并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

package com.singleton;

// getInstance()访问性能高,线程安全    非延迟初始化
public enum EnumSingleton {

    INSTANCE;
    
    private Resource instance;
    
    EnumSingleton(){
    	//doSomething();
        instance = new Resource();
    }
    
    public Resource getInstance() {
        return instance;
    }
    
}

public class EnumSingletonEnumTest {
    public static void main(String[] args) {
        Resource instance = EnumSingleton.INSTANCE.getInstance();
        System.out.println(instance);
    }
}

破坏单例模式的方法和防范措施

反射是通过强行调用私有构造方法生成新的对象。

防范方法

如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例,则阻止生成新的实例。

private Singleton(){
    if (instance != null){
        throw new RuntimeException("实例已经存在,请通过 getInstance()方法正确获取");
    }
}

序列化和单例模式

有时在分布式系统中,我们需要在单例类中实现可序列化接口,这样我们就可以在文件系统中存储它的状态,并在以后的时间点检索它。

每当反序列化它时,它都会创建该类的新实例。对比其hashCode值不一致

防范方法
  1. 不实现序列化接口
  2. 如果必须实现序列化接口,可以重写反序列化方法readResolve(),反序列化时直接返回相关单例对象。
protected Object readResolve() {
    return getInstance();
}

cloneable接口的破坏

和可序列化接口有些类似,当需要实现单例的类允许clone()时,如果处理不当,也会导致程序中出现不止一个实例。 

防范方法
重写clone()方法,调clone()时直接返回已经实例的对象。
protected Object clone() throws CloneNotSupportedException {
        return instance;
}

 

posted @ 2024-04-26 12:47  加了冰的才叫可乐  阅读(3)  评论(0编辑  收藏  举报