美文网首页
单例模式

单例模式

作者: vnsun | 来源:发表于2020-03-29 14:38 被阅读0次

单例模式

为了解决资源浪费、资源共享问题,只需要初始化一次,其他人都能复用

所有单例都需要解决的问题

  • 将构造函数私有化
  • 通过静态方法获取一个唯一实例
  • 保证线程安全
  • 防止反序列化造成的新实例等。

单例模式实现方式

  • 饿汉式
  • 懒汉式
  • 注册登记式(容器式、序列化、枚举式都属于注册登记式的一种)
  • 枚举式

饿汉式单例

特点

  在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快

优点

   使用前初始化,避免了线程安全问题,快,但浪费内存,不管用没用到这个类都占用内存,用户体验+

实现

package com.vnsun.one.hungry;

/**
 * 饿汉式单例
 * @author vnsun
 */
public class Hungry {

    private static final Hungry hungry = new Hungry();

    // 私有构造
    private Hungry() {

    }

    public static Hungry newInstance() {
        return hungry;
    }
}

懒汉式单例

特点

  在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢

优点

  一定程度上避免了内存浪费,提高了JVM加载效率。但相对于饿汉式单例效率不高,因为存在线程安全问题,需要某些手段来保证其线程安全,

传统单例实现

package com.vnsun.one.lazy;

/**
 * 懒汉式单例
 * @author vnsun
 */
public class LazyOne {

    private volatile static LazyOne lazyone;

    private LazyOne() {
    }

    /**
     * 改造二
     * double lock锁机制 虽然优化了效率,但依然很慢
     * 此时线程安全
     * @return
     */
    public static final LazyOne newInstance() {
        if (lazyone == null) {
            synchronized (LazyOne.class) {
                if (lazyone == null) {
                    lazyone = new LazyOne();
                }
            }
        }
        return lazyone;
    }

    /**
     * 改造一
     * synchronized 加锁形式严重影响效率
     * 此时线程安全
     * @return
     */
    /*public synchronized final static LazyOne newInstance() {
        if (lazyone == null) {
            lazyone = new LazyOne();
        }
        return lazyone;
    }*/

    /**
     * 多线程情况下有线程安全问题
     * @return
     */
    /*public final static LazyOne newInstance() {
        if (lazyone == null) {
            lazyone = new LazyOne();
        }
        return lazyone;
    }*/

}
上述代码虽已经进行了改造但效率依然不高,加入synchronized关键字后,造成不必要的同步开销。不建议使用,虽然JDK1.6及后续版本对改关键字进行了大量优化
double lock在高并发情况下也会存在一定缺陷,但是几率是极小的
double lock DCL失效

   假设线程A执行LazyOne one = LazyOne.newInstance();线程大概做了三件事

  1. 给实例分配内存
  2. 调用构造函数,初始化成员字段
  3. 将instance 对象指向分配的内存空间(此时sInstance不是null)

  如果执行顺序是1-3-2,那多线程下,A线程先执行3,2还没执行的时候,此时instance!=null 这时候,B线程直接取走instance ,使用会出错,难以追踪。JDK1.5及之后的volatile 解决了DCL失效问题(双重锁定失效)

synchronized百度百科

测试单例安全

/**
 * 测试单例并发
 * @author vnsun
 */
public class ThredsalfTest {

    public static void main(String[] args) {
        int count = 1000;
        // 发令枪,模拟多线程
        CountDownLatch latch = new CountDownLatch(count);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            new Thread() {
                @Override
                public void run() {
                    try {
                        latch.await();
                        LazyOne lazyOne = LazyOne.newInstance();
                        //Hungry hungry = Hungry.newInstance();
                        System.out.println(System.currentTimeMillis() + ":" + lazyOne);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }.start();
            latch.countDown();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时:"+ (endTime - startTime));
    }
}
image.png

静态内部类

优点:线程安全、保证单例对象唯一性,同时也延迟了单例的实例化,避免了反射入侵
缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是 Class 对象还是会被创建,而且是属于永久代的对象。(综合以为,这种方式是最好的单例模式)

package com.vnsun.one.lazy;

/**
 * 单例模式二
 * @author
 */
public class LazyTwo {

    /**
     * 存在问题:通过反射暴力提升访问权限,修改掉此值(一般是闲得蛋疼)
     * */
    private static boolean initialized = false;

    /**
     * 防止暴力初始化
     */
    private LazyTwo() {
        synchronized (LazyTwo.class) {
            if (!initialized) {
                initialized = true;
            } else {
                throw new RuntimeException("单例已被破坏");
            }
        }
    }

    public static final LazyTwo newInstance() {
        return LazyInside.LAZY;
    }

    private static class LazyInside {
        private static final LazyTwo LAZY = new LazyTwo();
    }
}

暴力测试单例侵犯

/**
 * 暴力测试单例侵犯
 */
public class LazyTwoTest {

    public static void main(String[] args) {
        Class<LazyTwo> clazz = LazyTwo.class;
        try {
            // 拿到私有构造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            // 访问权限提升
            c.setAccessible(true);
            Object o = c.newInstance();
            System.out.println(o);
            Object o1 = c.newInstance();
            System.out.println(01);
            System.out.println(0 == 01);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
image.png

枚举单例

枚举反序列化不会生成新的实例
优点:线程安全
缺点:枚举耗内存,能不用枚举就不用

class Resource{
 }
 
 public enum SomeThing {
     INSTANCE;
     private Resource instance;
     SomeThing() {
         instance = new Resource();
     }
    public Resource getInstance() {
        return instance;
    }
}

容器单例

利用了Map的key唯一性来保证单例

/**
 * 注册登记式
 * spring中就是使用这种做法
 * @author vnsun
 */
public class BeamFactory {

    private BeamFactory() {}

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

    public static Object getBean(String className) {
        if (ioc.containsKey(className)) {
            return ioc.get(className);
        } else {
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();
                ioc.put(className, obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }
    }
}

容器测试

public static void main(String[] args) {
        int count = 100;
        // 发令枪,模拟多线程
        CountDownLatch latch = new CountDownLatch(count);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            new Thread() {
                @Override
                public void run() {
                    try {
                        latch.await();
                        Object bean = BeamFactory.getBean("com.vnsun.one.lazy.OneFactory");
                        System.out.println(System.currentTimeMillis() + ":" + bean);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }.start();
            latch.countDown();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时:"+ (endTime - startTime));
    }
image.png

此时可看到容器单例也会有线程安全问题,目前只有+syncharized才能保证线程安全

所以容器单例 适合在程序初始化的时候,把多个单例对象放到map里边统一管理

序列化单例

public class Seriable implements Serializable {
    public final static Seriable INSTACE = new Seriable();

    private Seriable(){}

    public static Seriable getInstance() {
        return INSTACE;
    }

    /**
     * 不加此方法,反序列化时,单例会是一个新实例
     * @return
     */
    /*private Object readResolve() {
        return INSTACE;
    }*/
}

测试序列化与反序列化单例

/**
 * 序列化与反序列化 测试单例问题
 */
public class SeriableTest {

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

            FileInputStream fis = new FileInputStream("Seriable.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (Seriable)ois.readObject();
            ois.close();
            fis.close();
            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
image.png
解释一下为何反序列化后破坏了单例:

这是因为反序列化中的IO操作会重新从磁盘上读取此实例内容,而在操作的过程中会重新生产一个新的对象。
原因在于ObjectInputStream的readObject的调用栈:
readObject--->readObject0--->readOrdinaryObject--->checkResolve

readOrdinaryObject代码片段

Object obj;
try {
   // desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象
    obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
    throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
}
防止序列化破坏单例模式

把上述代码中注释掉的readResolve()方法放开后可以防止反序列化破坏单例

序列化相关知识点transient

总结

比较推荐的用法 double lock 、静态内部类(更为推荐)

单例优点

  • 只有一个对象,内存开支少、性能好(当一个对象的产生需要比较多的资源,如读取配置、产生其他依赖对象时,可以通过应用启动时直接产生一个单例对象,让其永驻内存的方式解决)
  • 避免对资源的多重占用(一个写文件操作,只有一个实例存在内存中,避免对同一个资源文件同时写操作)
  • 在系统设置全局访问点,优化和共享资源访问(如:设计一个单例类,负责所有数据表的映射处理)

单例缺点

  • 一般没有接口,扩展难

相关文章

  • 【设计模式】单例模式

    单例模式 常用单例模式: 懒汉单例模式: 静态内部类单例模式: Android Application 中使用单例模式:

  • Android设计模式总结

    单例模式:饿汉单例模式://饿汉单例模式 懒汉单例模式: Double CheckLock(DCL)实现单例 Bu...

  • 2018-04-08php实战设计模式

    一、单例模式 单例模式是最经典的设计模式之一,到底什么是单例?单例模式适用场景是什么?单例模式如何设计?php中单...

  • 设计模式之单例模式详解

    设计模式之单例模式详解 单例模式写法大全,也许有你不知道的写法 导航 引言 什么是单例? 单例模式作用 单例模式的...

  • Telegram开源项目之单例模式

    NotificationCenter的单例模式 NotificationCenter的单例模式分析 这种单例模式是...

  • 单例模式Java篇

    单例设计模式- 饿汉式 单例设计模式 - 懒汉式 单例设计模式 - 懒汉式 - 多线程并发 单例设计模式 - 懒汉...

  • IOS单例模式的底层原理

    单例介绍 本文源码下载地址 1.什么是单例 说到单例首先要提到单例模式,因为单例模式是单例存在的目的 单例模式是一...

  • 单例

    iOS单例模式iOS之单例模式初探iOS单例详解

  • 单例模式

    单例模式1 单例模式2

  • java的单例模式

    饿汉单例模式 懒汉单例模式

网友评论

      本文标题:单例模式

      本文链接:https://www.haomeiwen.com/subject/gifsuhtx.html