【Java 基础 9】单例模式及其设计方法、DCL、破坏单例模式方法以及防破坏手段

目录

单例模式

单例模式应用场景

Java中单例模式体现

单例模式的优点

单例模式缺点

单例模式设计方法

饿汉式

懒汉式

静态内部类法

枚举法

破坏单例模式的方式

防止破坏单例模式的手段

线程唯一单例


单例模式

一个类只允许创建一个实例

 

单例模式应用场景

1. 外部资源,配置文件等,比如打印

2. 数据库连接池

3. 计数器

4. 日志工具

5. 等

 

Java中单例模式体现

1. Runtime

2. spring IOC容器

3. 等

 

单例模式的优点

1. 由于在进程中只存在一个对象,当需要频繁创建和销毁的对象时,单例模式无疑可以节约系统资源,提高系统的性能;

 

单例模式缺点

1. 不适用于变化的对象;

2. 单例模式的构造函数被私有化并且实例在内部方法中创建,不能被子类继承也不能被抽象化,因此扩展性差;

3. 对象长时间不用,被GC回收,丢失对象状态;

4. 对象被大量线程使用,比如数据库连接池被设计为单例模式,大量连接造成出现连接池溢出;

 

单例模式设计方法

饿汉式,懒汉式(线程安全优化),静态内部类法,枚举法

 

饿汉式

// 饿汉式:
// 实例在编译期就被创建,随取随用,没有延时,其唯一性和线程安全性都由JVM保证
class SingletonHunger{

    // 1. 内部只有一个私有的单例类的实例
    private static SingletonHunger st=new SingletonHunger();
    
    // 2. 构造器私有化,不允许外部程序new新的实例
    private SingletonHunger(){}

    // 3. 提供一个静态方法,将唯一实例提供给外部程序
    public static SingletonHunger getInstance(){
        return st;
    }

    // ... 方法
}

懒汉式

// 懒汉式:
// 要的时候才创建,有就直接给
// 创建过程要保证实例的唯一和线程安全
class SingletonLazy{

    //懒汉式需要volatile保证实例的创建过程禁止指令重排
    // st的创建过程包括
    // 1. 在堆内存上分配SingletonLazy对象的内存空间
    // 2. 在堆内存上初始化对象
    // 3. st指向这块内存
    // 由于多线程环境中编译器优化指令重排, 可能执行132的顺序,执行了3 st就非null了,这时另一个线程可能就直接拿到了这个未被初始化的st对象
    private volatile static SingletonLazy st=null;

    private SingletonLazy() {}

//    // synchronized 修饰 getInstance方法,线程安全
//    // 但是在方法上加锁,会导致实例已经被创建时,获取实例仍需要先获取锁,效率低下
//    public static synchronized SingletonLazy getInstance() throws Exception {
//        if(st==null)
//            st = new SingletonLazy();
//        return st;
//    }


    // 当实例第一次被创建时才需要锁,所以在方法中设置同步方法块,当实例未被创建时加锁,已创建就直接获取就行,提高效率
    public static SingletonLazy getInstance() throws Exception {
        // 同步代码块 DCL(double check lock)双检查锁机制
        // 双重检查是为了避免另一个线程判断st==null后等待锁,获取到锁接着创建实例的情况,
        // DCL双重检查锁机制很好的解决了懒汉式单例模式的效率问题和线程安全问题
        if(st==null) { //第一次检查是为了提高锁的效率
            synchronized (SingletonLazy.class) {
                if (st == null)  //第二次检查是为了保证线程安全,保证实例的唯一
                    st = new SingletonLazy();
            }
        }
        return st;
    }

    // ... 方法

}

静态内部类法

// 静态内部类实现单例,由JVM的类加载机制保证单一实例
class SingletonStaticInner{

    //通过静态内部类的方式实现单例模式是线程安全的,同时静态内部类不会在Singleton类加载时就加载,而是在调用getInstance()方法时才进行加载,达到了懒加载的效果。
    private static class Singleton{
        private static SingletonStaticInner st = new SingletonStaticInner();
    }

    private SingletonStaticInner(){}

    //第一次调用getInstance()方法时,加载静态内部类St,创建St3的对象st,由于JVM类的加载机制,后面不会再加载St,实现了饿汉式的懒加载
    public static SingletonStaticInner getInstance(){
        return Singleton.st;
    }

    // ... 方法

}

枚举法

// 枚举法实现的单例模式
// 枚举变量都是通过static代码块来定义和初始化的,他们会在类被加载时完成初始化,而Java的类加载由JVM保证线程安全
// 禁止克隆(Java.lang.Enum类中clone()方法直接抛出CloneNotSupportException异常)
// 禁止利用反射构建单例实例(Constructor类的newInstance()方法中先判断对象类型是否为枚举类型,如果是,直接抛出new IllegalArgumentException("Cannot reflectively create enum objects");)
// 反序列化时放回同一个实例(ObjectInputStream.readObject()时,调用readObject0()扫描到对象类型为Enum,调用readEnum()方法返回和该枚举常量name相同的对象,就是返回这个唯一对象)
// 枚举法实现单例模式代码量少,简洁

enum SingletontEnum{
    st;
    //枚举类构造器默认私有
    // ... 方法
}

 

破坏单例模式的方式

1. 对实现了Cloneable的单例模式类的唯一实例克隆;

2. 对实现了Serializable的单例模式类的唯一实例反序列化;

3. 反射获取单例模式类的私有构造器创建新的实例。

 

防止破坏单例模式的手段

1. 针对实现了Cloneable的单例模式类,重写clone方法,返回唯一实例本身而不是它的副本;

2. 针对实现了Serializable的单例模式类,定义readResolve方法,控制反序列化时返回唯一实例本身而不根据二进制数据流创建它的副本。

3. 针对通过反射获取私有构造器创建对象,在私有构造器中添加判断,如果唯一实例已经被创建,则抛出异常退出构造方法。

// 懒汉式:
// 要的时候才创建,有就直接给
// 创建过程要保证实例的唯一和线程安全
class SingletonLazy implements Cloneable, Serializable{

    //懒汉式需要volatile保证实例的创建过程禁止指令重排
    // st的创建过程包括
    // 1. 在堆内存上分配SingletonLazy对象的内存空间
    // 2. 在堆内存上初始化对象
    // 3. st指向这块内存
    // 由于多线程环境中编译器优化指令重排, 可能执行132的顺序,执行了3 st就非null了,这时另一个线程可能就直接拿到了这个未被初始化的st对象
    private volatile static SingletonLazy st=null;

    // 当实例存在,禁止通过反射得到私有的构造器new新的实例
    private SingletonLazy() throws Exception {
        if(st!=null)
            throw new Exception("禁止通过反射得到私有的构造器new新的实例");
    }

//    // synchronized 修饰 getInstance方法,线程安全
//    // 但是在方法上加锁,会导致实例已经被创建时,获取实例仍需要先获取锁,效率低下
//    public static synchronized SingletonLazy getInstance() throws Exception {
//        if(st==null)
//            st = new SingletonLazy();
//        return st;
//    }


    // 当实例第一次被创建时才需要锁,所以在方法中设置同步方法块,当实例未被创建时加锁,已创建就直接获取就行,提高效率
    public static SingletonLazy getInstance() throws Exception {
        // 同步代码块 DCL(double check lock)双检查锁机制
        // 双重检查是为了避免另一个线程判断st==null后等待锁,获取到锁接着创建实例的情况,
        // DCL双重检查锁机制很好的解决了懒汉式单例模式的效率问题和线程安全问题
        if(st==null) { //第一次检查是为了提高锁的效率
            synchronized (SingletonLazy.class) {
                if (st == null)  //第二次检查是为了保证线程安全,保证实例的唯一
                    st = new SingletonLazy();
            }
        }
        return st;
    }

    // 实现了Cloneable的单例,重写clone方法,直接返回实例
    @Override
    protected Object clone() throws CloneNotSupportedException {
//        return super.clone();
        System.out.println("重写clone()方法,直接返回实例");
        return st;
    }

    // 实现了Serializable的单例,定义readResolve()方法,直接返回实例
    // ObjectInputStream在返回对象前检查类是否定义了readResolve方法
    // 如果定义了方法,则调用readResolve方法允许我们自行控制通过反序列化得到的对象即返回我们指定的对象,必须保证返回的对象类型和返回对象类型一致,否则会抛出ClassCastException
    private Object readResolve() {
        System.out.println("调用readResolve()方法,直接返回实例");
        return st;
    }

    // ... 方法

}

 

线程唯一单例

// 线程唯一单例
// 保证每一个线程中只允许创建一个实例,不同的线程可以拥有不同的实例,所以不考虑线程安全这个问题
class ThreadSingletont{

    private static ConcurrentHashMap<String, ThreadSingletont> threadSts = new ConcurrentHashMap<>();

    private ThreadSingletont(){}

    public static ThreadSingletont getInstance(){
        String tname = Thread.currentThread().getName();
        threadSts.putIfAbsent(tname, new ThreadSingletont());  //putIfAbsent 如果key存在就不更新
        return threadSts.get(tname);
    }
}


// 测试代码
public class Main{

    public static void main(String[] args) throws Exception {

        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for(int i=0;i<10;i++){
            threadPool.execute(() -> {
                ThreadSingletont st = ThreadSingletont.getInstance();
                System.out.println(Thread.currentThread().getName()+"  "+st.hashCode());
            });
        }
    }
}

ThreadLocal类更加方便管理每个线程自己的单例

//线程唯一单例
// 保证每一个线程中只允许创建一个实例,不同的线程可以拥有不同的实例,所以不考虑线程安全这个问题
class ThreadSingletont{

//    private static ConcurrentHashMap<String, ThreadSingletont> threadSts = new ConcurrentHashMap<>();
    private static ThreadLocal<ThreadSingletont> threadSts = new ThreadLocal<>();

    private ThreadSingletont(){}

    public static ThreadSingletont getInstance(){
        
//        String tname = Thread.currentThread().getName();
//        threadSts.putIfAbsent(tname, new ThreadSingletont());  //putIfAbsent 如果key存在就不更新
//        return threadSts.get(tname);
        
        if (threadSts.get() == null)
            threadSts.set(new ThreadSingletont());
        return threadSts.get();
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值