设计模式1-单例模式

单例模式的应用场景

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局访问点。单例模式是创建类型模式。在J2EE标准中ServletContext,ServletContextConfig等,在Spring矿家应用这个的ApplicationContext;数据库连接的连接池都是单例的形式。

饿汉式单例

饿汉模式是在类加载的时候就初始化了,并且创建了对象实例。绝对线程安全,在线程还没有出现之前就实例化了,不可能存在安全的问题。

  • 优点 :没有任务锁,执行效率高,代码量小,简单
  • 缺点:在类加载的时候就初始化了,不管是否使用都占用内存。

Spring中IOC容器ApplicationContext就采用的是饿汉模式。
饿汉模式的实现

public class HungrySingleton {
    // 私有的静态变量-类加载时初始化
    private static final HungrySingleton instance = new HungrySingleton();
    // 私有构造函数-确保外部无法在创建对象
    private HungrySingleton(){}
    // 公共的获取实例方法-确保类可以被使用
    public static HungrySingleton getInstance(){
        return instance;
    }
}

另外一种是通过静态代码块来实现

public class HungryStaticSingleton {

    private static final HungryStaticSingleton instance;

    static {
        instance = new HungryStaticSingleton();
    }
    
    private HungryStaticSingleton(){};

    public static HungryStaticSingleton getInstance(){
        return instance;
    }
}

饿汉式适用在单例对象较少的情况。下面我
们来看性能更优的写法

懒汉模式

懒汉模式与饿汉模式的曲边在于:在被外部类调用的时候才会被加载,下面我们看看懒汉模式的单例的实现

简单懒汉单例
public class LazySimpleSingleton {

    private static LazySimpleSingleton instance;

    private LazySimpleSingleton(){}

    public static LazySimpleSingleton getInstance(){
        if(instance==null){
            instance = new LazySimpleSingleton();
        }
        return instance;
    }
}

这种写法是线程不安全的,接下来我们编写测试类测试

public class LazySimpleSingletonThread implements Runnable{
    @Override
    public void run() {
        LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
        System.out.println("当前线程名称为"+Thread.currentThread().getId()+",LazySimpleSingleton实例为:" + instance);
    }
}
public class SingtonTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new LazySimpleSingletonThread());
        Thread t2 = new Thread(new LazySimpleSingletonThread());
        t1.start();
        t2.start();
    }
}

测试结果如下
在这里插入图片描述
可以看到LazySimpleSingleton被创建了2次,因为他们内存地址都不相同,如何解决这个问题呢,我们可以使用synchronized 关键字。

Synchronized饿汉模式
public class LazySynchronizedSingleton {

    private static LazySynchronizedSingleton instance;

    private LazySynchronizedSingleton(){}

    public static synchronized LazySynchronizedSingleton getInstance(){
        if(instance==null){
            instance = new LazySynchronizedSingleton();
        }
        return instance;
    }
}

通过断点调试可知
在这里插入图片描述
当Thread-0进入getInstance方法 线程状态为RUNNING
这是Thread-1进入getInstance方法 线程状态为MONITOR(互斥)
只有当Thread-0执行完之后Thread-1才能执行,这是if中判断为否,直接返回instance,因此保证了线程的安全
在这里插入图片描述
synchronized 这种方式解决了线程安全的问题,但是因为是加锁,在线程多的情况下,CPU跑完,会造成线程阻塞,影响程序的性能
那么有没有既安全又兼顾性能的呢,接下来我们看看双层检查锁的单例模式

双重锁检查饿汉模式
public class LazyDoubleCheckSingleton {

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

这样我们在getInstance的时候就不会出现互斥的情况,synchronized 在getInstance方法体的内部。LazyDoubleCheckSingleton整体是无感的,但是终归是家了锁,会对性能有影响,有没有无锁的解决方案呢,接下来我们看静态内部类的方案

静态内部类饿汉式

这种形式兼顾饿汉式的内存浪费, 也兼顾 synchronized 性能问题

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton(){}

    // static 包装共享
    // final 保证方法不可被继承和重写
    public static final LazyInnerClassSingleton getInstance(){
        return LazyHolder.instance;
    }

    // 内部类默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton instance = new LazyInnerClassSingleton();
    }
}

反射破坏单例

以上所有的单例模式都可能会被反射破坏,接下来我们做一个测试

public class ReflectSingletonTest {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<LazyInnerClassSingleton> clz = LazyInnerClassSingleton.class;
        Constructor<LazyInnerClassSingleton> declaredConstructor = clz.getDeclaredConstructor(null);
        // 设置构造方法访问权限
        declaredConstructor.setAccessible(true);
        LazyInnerClassSingleton instance1 = declaredConstructor.newInstance(null);
        LazyInnerClassSingleton instance2 = declaredConstructor.newInstance(null);
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

测试结果
在这里插入图片描述
很明显,这里创建了2个对象
最终结局方案

终极饿汉单例模式(最佳)

为了防止反射直接调用构造函数实例化对象,我们在构造函数中做一个判断,如果对象存在还调用构造方法,直接抛出异常

public class LazyInnerClassSingleton {

    private LazyInnerClassSingleton(){
        if(LazyHolder.instance != null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }

    // static 包装共享
    // final 保证方法不可被继承和重写
    public static final LazyInnerClassSingleton getInstance(){

        return LazyHolder.instance;
    }

    // 内部类默认不加载
    private static class LazyHolder{
        private static final LazyInnerClassSingleton instance = new LazyInnerClassSingleton();
    }
}

接下来我们在测试的时候就会出现异常
在这里插入图片描述

序列化破坏单例

当我们将一个单例对象创建好之后,有时候需要写入磁盘中,下次使用的是从磁盘中读取,反序列化转换为内存对象,反序列化的时候,对象会被重新分配内存,也就是重新创建。这种场景想序列化单例的对象就会破坏单例模式

看下面一段代码,与饿汉式一样

public class SeriableSingleton implements Serializable {

    private static final SeriableSingleton instance = new SeriableSingleton();
    
    private SeriableSingleton(){}
    
    public static SeriableSingleton getInstance(){
        return instance;
    }
}

做一个序列化与反序列化的测试

public class SeriableSingletonTest {

    public static void main(String[] args) {
        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
            objectOutputStream.writeObject(s2);
            objectOutputStream.flush();
            objectOutputStream.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream objectInputStream = new ObjectInputStream(fis);
            s1  = (SeriableSingleton)objectInputStream.readObject();
            objectInputStream.close();

            System.out.println(s1);
            System.out.println(s2);
            
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

测试结果
在这里插入图片描述
可以看到s1和s2对象的内存地址不同
那如何才能解决这个序列化和反序列化造成的对象不一致问题呢
这时候我们只需要在

public class SeriableSingleton implements Serializable {

    private static final SeriableSingleton instance = new SeriableSingleton();

    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return instance;
    }

    // ObjectInputStream会执行这段代码
    private Object readResolve(){
        return instance;
    }
}

通过源码跟踪可得知
在ObjectStreamClass中
在这里插入图片描述
会获取readResolve方法
序列化的这种方式最终会产生2个对象

但是有一个不会返回,这样在频繁的序列化和反序列化的操作中出现内存分配的开销
有没有从根本上解决单例的问题的

注册式单例

public enum EnumSingleton {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

注册式单例又称为登记式单例,就是将每一个实例都记录到某一个地方,使用唯一的标识获取实例。因为java 枚举天生具有单例的特性

容器单例模式

public class ContainerSingleton {

    private ContainerSingleton(){}

    private static Map<String,Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className){
        synchronized (ioc){
            Object o = ioc.get(className);
            if(o==null){
                try {
                    Class<?> forName = Class.forName(className);
                    Object o1 = forName.newInstance();
                    ioc.put(className,o1);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
            return ioc.get(className);
        }
    }
}

注册式的本质是让受管理的类只存在一份,而不是容器类本身可以是单例模式

ThreadLocal线程单例

ThreadLocal 不能保证其
创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全

public class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

ThreadLocal的方式确保了再同一个线程中是单例

public class ThreadLocalSingletonTest {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());

        Thread tl =new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());
            }
        });
        Thread t2 =new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId()+":"+ ThreadLocalSingleton.getInstance());
            }
        });
        tl.start();
        t2.start();
        System.out.println("finish");
    }

}

从测试中我们能够看出主线程下所有的实例地址都相同也就是同一个,在子线程下都不相同
在这里插入图片描述

单例模式小结

单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。单例模式看起来非常简单,实现起来其实也非常简单

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值