04.单例模式详解(下)

回顾

        在上一节,介绍了单例模式中的懒汉式、饿汉式、静态内部类模式下的单例,这次从注册式单例模式开始

注册式单例

        将每一个实例都缓存到统一的容器中,使用唯一标识获取实例

枚举式单例

/**
 * 注册式单例之枚举单例
 */
public enum RegisterEnum {
    INSTANCE;
    public static RegisterEnum getInstance() {
        return INSTANCE;
    }
}
public class RegisterEnumTest {
    /**
     * 使用序列化形式来创建单例对象
     * @param args
     */
    public static void main(String[] args) {
        RegisterEnum r1;
        RegisterEnum r2 = RegisterEnum.getInstance();
        FileOutputStream fileOutputStream;
        try {
            fileOutputStream = new FileOutputStream("1.obj");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(r2);
            objectOutputStream.flush();
            objectOutputStream.close();

            FileInputStream fileInputStream = new FileInputStream("1.obj");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            r1 = (RegisterEnum) objectInputStream.readObject();
            System.out.println(r1);
            System.out.println(r2);
            System.out.println(r1==r2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
输出的是:true

将序列化后的类反编译后如下(mac下的jad工具只能反编译,无法反编译.obj的文件,在window下试一下),可以看出这时反编译后的枚举类是个饿汉式的单例模式,而饿汉式的单例本身就是线程安全的

思考一下,枚举类中没有重写readResolve(),如果序列化创建枚举对象,枚举是怎么避免序列化破坏单例的呢?还是一样看readObject()的源码,对于枚举的类型的判断,如下图所示

由源码可得,从jdk层面就为枚举不被序列化破坏而起到了根本的保证,那么我们再试试用反射的机制看能不能创建两个对象,检查枚举单例的正确性

/**
 * 使用反射形式来创建单例对象
 *
 * @param args
 */
public static void main(String[] args) {
    try {
        Class<RegisterEnum> enumClass = RegisterEnum.class;
        Constructor<RegisterEnum> constructor = enumClass.getDeclaredConstructor();
        //关闭权限检查
        constructor.setAccessible(true);
        //调用构造方法,进行创建对象
        RegisterEnum registerEnum = constructor.newInstance();
        System.out.println(registerEnum);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

结果抛出以下异常

通过反编译后的代码可知,枚举类中只有一个有参的构造器,而newInstance(),是默认调用无参构造器,所以无法初始化类,修改代码如下,传入一个String类型和int类型的参数

/**
 * 使用类名.class来进行反射
 *
 * @param args
 */
public static void main(String[] args) {
    try {
        Class<RegisterEnum> enumClass = RegisterEnum.class;
        Constructor<RegisterEnum> constructor = enumClass.getDeclaredConstructor(String.class,int.class);
        //关闭权限检查
        constructor.setAccessible(true);
        //调用构造方法,进行创建对象
        RegisterEnum registerEnum = constructor.newInstance("wcj",25);
        System.out.println(registerEnum);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
/**
 * 使用Class.forName来进行反射
 *
 * @param args
 */
public static void main(String[] args) {
    try {
        Constructor<?> declaredConstructor = Class.forName("top.wangcj.chapter02单例模式.register.RegisterEnum").getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        RegisterEnum registerEnum = (RegisterEnum) declaredConstructor.newInstance("wcj", 25);
        System.out.println(registerEnum);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

运行,但是又报出以下错误

查询newInstance()的源码如下

由源码可知,jdk内部就进行的判断,当反射想创建枚举对象时,直接抛出的异常,这属于jdk内部的机制,直接从根本就扼杀了反射创建枚举对象的可能性,自然也保证了枚举单例模式的安全性

延伸:Class.forName()和类名.class有何区别

public class Tom {
    static int num = 10;
    static {
        System.out.println("被执行了:"+num);
    }
}
@Test
public void testTom() {
    try {
        //不会初始化静态成员,无输出,除非进行tomClass.newInstance()操作;
        Class<Tom> tomClass = Tom.class;
        //会初始化静态成员,有输出
        Class<?> aClass = Class.forName("top.wangcj.chapter02单例模式.register.Tom");
    } catch (Exception e) {
        e.printStackTrace();
       

      由上述代码可知,Class.forName显示加载类时候会调用代码中静态块。而直接用类.class则不加载静态代码块,只有在类名.class.newInstance()时候才会初始化这些静态块

容器式单例

Spring框架采用的就是容器式单例模式

/**
 * 容器式单例
 *
 * @author wcj
 * @description
 * @date 2019/8/18 19:11
 */
public class ContainerSingleton {
    private ContainerSingleton() {
    }
    private static Map<String, Object> singleton = new ConcurrentHashMap<>();
    public static Object getInstance(String className) {
        if (!singleton.containsKey(className)) {
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();
                singleton.put(className, obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }
        return singleton.get(className);
    }
}
/**
 * 发令枪,线程并发
 */
public class ConcurrentExecutor {
    /**
     * @param runHandler
     * @param executeCount    发起请求总数
     * @param concurrentCount 同时并发执行的线程数
     * @throws Exception
     */
    public static void execute(final RunHandler runHandler, int executeCount, int concurrentCount) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //控制信号量,此处用于控制并发的线程数
        final Semaphore semaphore = new Semaphore(concurrentCount);
        //闭锁,可实现计数量递减
        final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
        for (int i = 0; i < executeCount; i++) {
            executorService.execute(new Runnable() {
                public void run() {
                    try {
                        //执行此方法用于获取执行许可,当总计未释放的许可数不超过executeCount时,
                        //则允许同性,否则线程阻塞等待,知道获取到许可
                        semaphore.acquire();
                        runHandler.handler();
                        //释放许可
                        semaphore.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();//线程阻塞,知道闭锁值为0时,阻塞才释放,继续往下执行
        executorService.shutdown();
    }

    public interface RunHandler {
        void handler();
    }
}
/**
 * 测试容器式单例线程
 *
 * @author wcj
 * @description
 * @date 2019/8/19 10:02
 */
public class ContainerSingletonTest {
    public static void main(String[] args) {
        try {
            ConcurrentExecutor.execute(new ConcurrentExecutor.RunHandler() {
                public void handler() {
                    Object obj = ContainerSingleton.getInstance("top.wangcj.chapter02单例模式.container.User");
                    ;
                    System.out.println(System.currentTimeMillis() + ": " + obj);
                }
            }, 10, 6);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
输出的结果是:
1566184262618: top.wangcj.chapter02单例模式.container.User@2d74268a
1566184262618: top.wangcj.chapter02单例模式.container.User@1fe9b76c
1566184262618: top.wangcj.chapter02单例模式.container.User@11f3ef5e
1566184262618: top.wangcj.chapter02单例模式.container.User@1dbdc081
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262618: top.wangcj.chapter02单例模式.container.User@b23d21c
1566184262618: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575

由输出结果可知,并不是单例的情况,那么可以针对懒加载的单例模式的改造方案,添加synchronized关键字,但是不要加在静态方法上,因为锁住的是类,可以在方法内部加锁,修改如下

public static Object getInstance(String className) {
    //方法内部加锁
    synchronized (singleton){
        if (!singleton.containsKey(className)) {
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();
                singleton.put(className, obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }
        return singleton.get(className);
    }
}

注意:虽然ConcurrentHashMap类是个线程安全的,它只是控制了自己内部的方法是线程安全的,但是getInstance并不是线程安全的,所以对此方法进行处理,使其安全。

容器式单例的优点

  • 方便对象管理,也是属于是懒加载的方式

ThreadLocal单例模式(也是容器式单例)

/**
 * ThreadLocal单例模式
 * 线程间的线程安全,伪线程安全的
 * @author wcj
 * @description
 * @date 2019/8/19 11:45
 */
public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>() {
        //重写initialValue方法
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    private ThreadLocalSingleton() {
    }
    public static ThreadLocalSingleton getInstance() {
        return threadLocal.get();
    }
}
/**
 * ThreadLocal单例模式测试类
 * @author wcj
 * @description
 * @date 2019/8/19 14:26
 */
public class ThreadLocalSingletonTest {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
        System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
        Thread t1 = new Thread(new ExecuteThread());
        Thread t2 = new Thread(new ExecuteThread());
        t1.start();
        t2.start();
    }
}
输出结果如下:
main:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@66d3c617
main:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@66d3c617
main:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@66d3c617
Thread-0:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@69e15105
Thread-0:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@69e15105
Thread-0:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@69e15105
Thread-1:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@c8a4608
Thread-1:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@c8a4608
Thread-1:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@c8a4608

由输出结果可看出,虽然产生了并发,但是同一个线程,得到的对象还是相同的,虽然是属于线程级别的线程安全,伪线程安全。该种单例模式保证线程内部的全局唯一,且天生线程安全,为什么天生线程安全呢,查看JDK源码如下

由源码可以看出,JDK内部的ThreadLocal类中的静态类ThreadLocalMap类是用当前线程作为key,value就是我们需要的值,就如同上述代码中的其实存在着ThreadLocalMap对象,内部存储的key就是当前线程,value就是ThreadLocalSingleton对象。

使用ThreadLocal可以实现多数据源动态切换功能,后续会有

单例模式优点

  • 在内存中只有一个实例,减少了内存开销
  • 可以避免对资源的多重占用
  • 设置全局访问点,严格控制访问

单例模式缺点

  • 没有接口,扩展困难
  • 如果要扩展单例对象,只有修改代码,没有其他途径

单例模式总结

  • 构造器需要私有化
  • 要确保其线程安全,线程不安全,就没有单例一说了
  • 延迟加载
  • 防止序列化和反序列化破坏单例
  • 防止反射攻击单例

总结每种单例写法的优、缺点。

  • 饿汉式单例:类一加载就创建,线程安全,但是不管用没用到都实例化,如果这样的类有很多,那么会造成很大资源占用
  • 懒汉式单例:用到才创建,节省了资环
    • 不加锁:线程不安全
    • 方法锁:锁权重太大,降低性能
    • 双重检查锁:降低锁权重,相对方法锁提高了效率
  • 静态内部类式单例:内部类虽然是饿汉式的,但是用到时才加载内部类,实例化单例。并且没用到锁,提高了性能
  • 序列化式单例:只是在简单的懒汉式上实现了序列化了,但是如果不实现readReslove()方法,无法保证单例
  • 注册式单例
    • 枚举式单例:JDK内部保障自由序列化,反射无法破坏,且线程安全,推荐使用
    • 容器缓存单例:方便管理大量单例,但存在线程安全问题,spring内部使用的是这种
    • ThreadLocal式单例:单线程内实例唯一,多线程内线程不安全,多数据源的场景会用到

破坏单例模式的方式有哪些?

  • 反射破坏单例,解决方案:在构造方法中做处理,发现已有实例即抛异常;
  • 序列化、反序列化破坏单例,解决方案:在单例类中,声明readReslove()方法,返回单例。
  • 深克隆破坏单例,深克隆使用序列化与反序列化实现克隆,所有可以如上面方法解决。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芦蒿炒香干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值