精华内容
下载资源
问答
  • 利用枚举实现单例模式

    千次阅读 2019-03-17 11:45:56
    Java中的枚举是一个很容易被大家遗忘的知识点,鄙人在工作中也很少使用枚举,最近仔细整理了一下相关知识点...3、单例模式的实现 在WEB开发中,经常需要封装一个ResponseDto类来返回一些调用的结果信息,如下: ...

    Java中的枚举是一个很容易被大家遗忘的知识点,鄙人在工作中也很少使用枚举,最近仔细整理了一下相关知识点吗,发现用好枚举可以为开发带来很多长处。

    枚举早在JDK1.5就被引入了,应该是比较古老的特性了,那枚举具体可以用在哪些场景下呢?

    1、常量的定义

    2、switch的入参

    3、单例模式的实现

    在WEB开发中,经常需要封装一个ResponseDto类来返回一些调用的结果信息,如下:

    /**
     * @author Administrator
     * @date 2019/3/17
     */
    public enum ResponseDto {
        //定义两个实例,一个表示请求成功,一个表示请求失败
        HTTP_200(200,"请求成功"), HTTP_500(500,"请求失败");
    
        //枚举和普通的类一样,可以定义属性,构造函数,getter setter,普通方法,
        private Integer code;
        private String msg;
    
        ResponseDto(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
        public Integer getCode() {
            return code;
        }
    
        public String getMsg() {
            return msg;
        }
    
        public void setCode(Integer code) {
            this.code = code;
        }
    
        public void setMsg(String msg) {
            this.msg = msg;
        }
    }
    /**
     * @author Administrator
     * @date 2019/3/17
     */
    public class Test05 {
        public static void main(String [] args){
            System.out.println(ResponseDto.HTTP_200);
            System.out.println(ResponseDto.HTTP_500);
            System.out.println(ResponseDto.HTTP_200==ResponseDto.HTTP_200);
            System.out.println(ResponseDto.HTTP_200.equals(ResponseDto.HTTP_200));
            System.out.println(ResponseDto.HTTP_200.getMsg());
            System.out.println(ResponseDto.HTTP_500.getMsg());
        }
    }

    结果

    HTTP_200
    HTTP_500
    true
    true
    请求成功
    请求失败

    注意结果:==和equal都是true,这好像与两个普通对象比较的结果不太一样,我们都知道==比较的其实是对象的地址,相等意味着什么呢,意味着两个对象其实就是同一个对象,这是枚举的一个重要特性,也就是说枚举对象是单例的,一种对象只会在内存中保存一份。

    总结来说:1、枚举和普通的类一样,有属性,get,set方法,构造函数。2、枚举继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类,但是可以实现接口,实现接口的时候需要实现接口中定义的所有方法,若没有完全实现,那这个枚举类就是抽象的,只是不需显式加上abstract修饰,系统化会默认加上。3、枚举对象是天生单例。

    利用枚举实现单例模式

    单例模式主要有5中方式实现,懒汉式、饿汉式、静态内部类,双中检测锁,枚举。这里我们主要聊一聊利用枚举的方式实现单例模式。

    /**
     * @author Administrator
     * @date 2019/3/17
     */
    public class User {
        //私有化构造函数
        private User(){ }
    
        //定义一个静态枚举类
        static enum SingletonEnum{
            //创建一个枚举对象,该对象天生为单例
            INSTANCE;
            private User user;
            //私有化枚举的构造函数
            private SingletonEnum(){
                user=new User();
            }
            public User getInstnce(){
                return user;
            }
        }
    
        //对外暴露一个获取User对象的静态方法
        public static User getInstance(){
            return SingletonEnum.INSTANCE.getInstnce();
        }
    }
    /**
     * @author Administrator
     * @date 2019/3/17
     */
    public class Test05 {
        public static void main(String [] args){
            System.out.println(User.getInstance());
            System.out.println(User.getInstance());
            System.out.println(User.getInstance()==User.getInstance());
        }
    }

    结果:

    User@1540e19d
    User@1540e19d
    true

     

    展开全文
  • 为什么枚举实现单例模式是安全的?枚举类的优缺点 枚举类 为什么会有枚举类? 假如说程序中有一些颜色的状态,或者说消息的类型等,在JDK1.5之前,只能用常量来进行表示 public class TestEnum { public static ...

    枚举类

    为什么会有枚举类?

    假如说程序中有一些颜色的状态,或者说消息的类型等,在JDK1.5之前,只能用常量来进行表示

    public class TestEnum {
        public static final int RED = 1;
        public static final int BLACK = 2;
        public static final int GREEN = 3;
        public static void main(String[] args) {
            int num = 1;
            System.out.println(RED == num);
        }
    }
    //true
    

    num不是一个颜色,就与实际不太相符合,于是JDK1.5中引入了枚举类型

    enum EnumColor {
        RED, BLACK, GREEN;
    
        public static void main(String[] args) {
            EnumColor color = EnumColor.BLACK;
            switch (color) {
                case RED:
                    System.out.println("红色");
                    break;
                case BLACK:
                    System.out.println("黑色");
                    break;
                case GREEN:
                    System.out.println("绿色");
                    break;
            }
        }
    }
    //黑色
    

    枚举类的使用

    • 类的对象只有有限个,确定的(星期,性别,季节…)
    • 当需要定义一组常量
    • 使用enum定义的枚举类默认继承了java.lang. Enum类,因此不能再继承其他类
    • 枚举类的构造器只能使用private权限修饰符
    • 枚举类的所有实例必须在枚举类中显式列出(,分隔结尾)。列出的实例
      系统会自动添加public static final修饰必须在枚举类的第一行声明枚举类对象
    • 枚举不能再类外直接实例化,也不能被继承

    如果枚举有参数可以添加对应的构造函数,但要注意:枚举的构造函数默认是私有的。所以不能被继承
    枚举就是定义了一些状态或者常见集合,一般不需要单独实例化,用的时候到枚举类中找合适的即可
    枚举的构造函数不能使用public和protected修饰。

    枚举类的常用方法

    方法名称 描述
    values() 以数组形式返回枚举类型的所有成员
    ordinal() 获取枚举成员的索引位置
    valueOf() 将普通字符串转换为枚举实例
    compareTo() 比较两个枚举成员在定义时的顺序
    enum EnumColor {
        RED, BLACK, GREEN;
        public static void main(String[] args) {
        // 以数组的方式返回所有的枚举成员
            EnumColor[] colors = EnumColor.values();
        // 打印每个枚举成员的索引
            for(int i = 0; i < colors.length; ++i){
                System.out.println(colors[i] + ":" + colors[i].ordinal());
            }
        // 将字符串GREEN转化为枚举类型
            EnumColor color1 = EnumColor.valueOf("GREEN");
            System.out.println(color1);
        // 在进行转换时,如果有对应的枚举类型则转换,否则抛出IllegalArgumentException
    //     EnumColor color2 = EnumColor.valueOf("YELLOW");//定义的枚举类没有YELLOW
    //     System.out.println(color2);
            EnumColor color2 = EnumColor.valueOf("BLACK");//定义的枚举类没有YELLOW
            System.out.println(color2);
            System.out.println("-------------------------------------");
            System.out.println("枚举实例的比较");
        // 注意此处的比较是使用枚举成员的索引来比较了
            EnumColor black = EnumColor.BLACK;
            EnumColor red = EnumColor.RED;
            System.out.println(black.compareTo(red));
            System.out.println(BLACK.compareTo(RED));
            System.out.println(RED.compareTo(BLACK));
        }
        //结果:
        RED:0
    	BLACK:1
    	GREEN:2
    	GREEN
    	BLACK
    	-------------------------------------
    	枚举实例的比较
    	1
    	1
    	-1
    

    枚举的构造

    上述枚举类型有一个不太友好的地方是,枚举中只有枚举常量,拿到一个枚举常量后还是不知道其是什么颜色的,因此可以给枚举增加构造函数。

     enum EnumColor {
        RED("红色", 1), BLACK("黑色", 2), GREEN("绿色", 3);
        private String color;
        private int key;
        // 注意:枚举的构造函数默认是私有的,此处不能增加非private的访问权限进行限制
        /*public*/EnumColor(String color, int key){
            this.color = color;
            this.key = key;
        }
        public static EnumColor getEnum(String str){
            for (EnumColor color : EnumColor.values()){
                if(color.color.equals(str))
                    return color;
            }
            return null;
        }
        public static void main(String[] args) {
            System.out.println(EnumColor.getEnum("红色"));
        }
    }
    

    枚举类型能被反射吗?

    不能不能不能!
    通过看反射的源码,可以看出反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
    在这里插入图片描述

    为什么枚举实现单例模式是安全的?

    enum Singleton5 {//枚举类的单例模式
        INSTANCE;
        public Singleton5 getInstance(){
            return INSTANCE;
        }
    }
    

    1、 私有化构造器并不保险,通过反射机制调用私有构造器。而枚举类不能被反射,所以可以防止反射攻击

    //模拟反射攻击
    class Singleton {//双重校验锁,性能佳线程安全
        private static Singleton4 instance=null;
        private Singleton4() {}
        public static Singleton4 getInstance() {
            if(instance==null) {
                synchronized (Singleton4.class) {
                    if (instance==null) {
                        //new对象分为三个操作
                        //分配内存
                        //调用构造方法初始化
                        //赋值
                        instance=new Singleton4();
                    }
                }
            }
            return instance;
        }
    }
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton s=Singleton.getInstance();
        Singleton sUsual=Singleton.getInstance();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton sReflection=constructor.newInstance();
        System.out.println(s+"\n"+sUsual+"\n"+sReflection);
        System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));
    }
    //com.lxp.pattern.singleton.Singleton@1540e19d
    //com.lxp.pattern.singleton.Singleton@1540e19d
    //.lxp.pattern.singleton.Singleton@677327b6
    //正常情况下,实例化两个实例是否相同:true
    //通过反射攻击单例模式情况下,实例化两个实例是否相同:false
    

    2.避免序列化问题(任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例)

    枚举类的优缺点

    1、优点

    枚举常量更简单安全 。
    枚举具有内置方法 ,代码更优雅
    

    2. 缺点

    不可继承,无法扩展
    
    展开全文
  • 饿汉式和懒汉式都无法避免被反序列化和反射生成多个实例。而枚举方式实现的单例模式不仅能避免多线程同步的问题,也可以防止反...Joshua Bloch 在《Effective Java》中明确表明,枚举类型实现的单例模式是最佳的方式。

    单例设计模式(Singleton Design Pattern)

    一、定义

    一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

    二、用途

    从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。比如,无状态工具类、全局配置信息类。

    三、实现方式

    唯一递增 ID 生成器为案例
    单例模式有两种模式,饿汉式和懒汉式。

    1、饿汉式

    饿汉式在类加载的同时就已经创建好一个静态的对象供系统使用,其唯一性和线程安全性都由JVM保证。

    public class HungryIdGenerator {
        private static final HungryIdGenerator instance = new HungryIdGenerator();
        private AtomicLong id = new AtomicLong(0);
        private HungryIdGenerator() {}
        public static HungryIdGenerator getInstance() {
            return instance;
        }
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    道听途说:

    有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。

    不过,我个人并不认同这样的观点,理由如下:

    • 如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),在程序启动时初始化单例,尽早报OOM,以免程序在正常运行时,因为首次加载单例突然OOM导致系统崩溃。
    • 如果加载比较耗时,更应该在程序启动时初始化好单例了,而不是在程序运行中占用正常请求的时长,导致请求缓慢甚至超时报错。

    2、懒汉式

    懒汉式相对于饿汉式的优势是支持延迟加载。在对外提供的方法内实例化,需要线程安全的处理。

    public class LazyIdGenerator {
        private AtomicLong id = new AtomicLong(0);
        private static volatile LazyIdGenerator instance = null;
        private LazyIdGenerator() {}
    
        public static synchronized LazyIdGenerator getInstance() {
            if (instance != null) {
                return instance;
            }
            instance = new LazyIdGenerator();
            return instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    synchronized加在方法上,使得每次获得实例都要同步,开销很大,性能很低。强烈不推荐这种方式实现单例。

    3、双重检测(double check)

    双重检测是懒汉式的升级版,只有在第一次初始化时加锁,以后不会再加锁。个人觉得双重检测的懒汉式与饿汉式不分哪个最优,他们有着不同的应用场景。

    /**
     * 懒加载:double check
     */
    public class DoubleCheckIdGenerator {
        private AtomicLong id = new AtomicLong(0);
        private static volatile DoubleCheckIdGenerator instance = null;
        private DoubleCheckIdGenerator() {}
        public static DoubleCheckIdGenerator getInstance() {
            if (instance != null) {
                return instance;
            }
            synchronized (DoubleCheckIdGenerator.class) {
                if (instance != null) {
                    return instance;
                }
                instance = new DoubleCheckIdGenerator();
                return instance;
            }
        }
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    instance 成员变量加了volatile 关键字修饰,是为了防止指令重排,因为instance = new DoubleCheckIdGenerator(); 并不是一个原子操作,其在JVM中至少做了三件事:

    1. 给instance在堆上分配内存空间。(分配内存)
    2. 调用DoubleCheckIdGenerator的构造函数等来初始化instance。(初始化)
    3. 将instance对象指向分配的内存空间。(执行完这一步instance就不是null了)

    在没有volatile修饰时,执行顺序可以是1,2,3,也可以是1,3,2。假设有两个线程,当一个线程先执行了3,还没执行2,此时第二个线程来到第一个check,发现instance不为null,就直接返回了,这就出现问题,这时的instance并没有完全完成初始化。

    听说高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。但是并没有在官方资料中看到,所以以防万一,还是加上volatile 这个关键字。

    4、静态内部类

    比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。

    /**
     * 懒加载:静态内部类
     */
    public class StaticInnerClassIdGenerator {
        private AtomicLong id = new AtomicLong(0);
        private StaticInnerClassIdGenerator(){}
        private static class SingletonHolder{
            private static final StaticInnerClassIdGenerator instance = new StaticInnerClassIdGenerator();
        }
    
        public static StaticInnerClassIdGenerator getInstance() {
            return SingletonHolder.instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    SingletonHolder是一个静态内部类,当外部类 StaticInnerClassIdGenerator被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance

    instance 的唯一性、创建过程的线程安全性,都由 JVM来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

    5、枚举

    最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

    /**
     * 枚举单例-饿汉,推荐
     */
    public enum IdGeneratorEnum {
        INSTANCE;
        private AtomicLong id = new AtomicLong(0);
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    四、枚举单例模式是世界上最好的单例模式

    饿汉式以及懒汉式中的双重检查式、静态内部类式都无法避免被反序列化和反射生成多个实例。而枚举方式实现的单例模式不仅能避免多线程同步的问题,也可以防止反序列化和反射的破坏。

    Joshua Bloch 在《Effective Java》中明确表明,枚举类型实现的单例模式是最佳的方式。

    枚举单例模式具有以下三个优点:

    • 写法简洁,代码短小精悍。
    • 线程安全。
    • 防止反序列化和反射的破坏。

    通过jad反编译工具将枚举单例反编译:

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    // Jad home page: http://www.kpdus.com/jad.html
    // Decompiler options: packimports(3) 
    // Source File Name:   IdGeneratorEnum.java
    
    package com.stefan.designPattern.singleton;
    
    import java.util.concurrent.atomic.AtomicLong;
    
    public final class IdGeneratorEnum extends Enum
    {
    
        public static IdGeneratorEnum[] values()
        {
            return (IdGeneratorEnum[])$VALUES.clone();
        }
    
        public static IdGeneratorEnum valueOf(String name)
        {
            return (IdGeneratorEnum)Enum.valueOf(com/stefan/designPattern/singleton/IdGeneratorEnum, name);
        }
    
        private IdGeneratorEnum(String s, int i)
        {
            super(s, i);
            id = new AtomicLong(0L);
        }
    
        public long getId()
        {
            return id.incrementAndGet();
        }
    
        public static final IdGeneratorEnum INSTANCE;
        private AtomicLong id;
        private static final IdGeneratorEnum $VALUES[];
    
        static 
        {
            INSTANCE = new IdGeneratorEnum("INSTANCE", 0);
            $VALUES = (new IdGeneratorEnum[] {
                INSTANCE
            });
        }
    }
    

    1、JVM级别的线程安全

    反编译的代码中可以发现枚举中的各个枚举项都是通过static代码块来定义和初始化的,他们会在类被加载时完成初始化,而Java的类加载由JVM保证线程安全。

    2、防止反序列化的破坏

    Java的序列化专门对枚举的序列化做了规定,在序列化时,只是将枚举对象的name属性输出到结果中,在反序列化时通过java.lang.EnumvalueOf方法根据名字查找对象,而不是新建一个新的对象,所以防止了反序列化对单例的破坏。

    可以查看java.io.ObjectInputStream#readObject验证。readObject判断到枚举类时,调用的了这个方法java.io.ObjectInputStream#readEnum

    private Enum<?> readEnum(boolean unshared) throws IOException {
        if (this.bin.readByte() != 126) {
            throw new InternalError();
        } else {
            ObjectStreamClass desc = this.readClassDesc(false);
            if (!desc.isEnum()) {
                throw new InvalidClassException("non-enum class: " + desc);
            } else {
                int enumHandle = this.handles.assign(unshared ? unsharedMarker : null);
                ClassNotFoundException resolveEx = desc.getResolveException();
                if (resolveEx != null) {
                    this.handles.markException(enumHandle, resolveEx);
                }
    
                String name = this.readString(false);
                Enum<?> result = null;
                Class<?> cl = desc.forClass();
                if (cl != null) {
                    try {
                        Enum<?> en = Enum.valueOf(cl, name);
                        result = en;
                    } catch (IllegalArgumentException var9) {
                        throw (IOException)(new InvalidObjectException("enum constant " + name + " does not exist in " + cl)).initCause(var9);
                    }
    
                    if (!unshared) {
                        this.handles.setObject(enumHandle, result);
                    }
                }
    
                this.handles.finish(enumHandle);
                this.passHandle = enumHandle;
                return result;
            }
        }
    }
    

    3、防止反射的破坏

    对于反射,枚举类同样有防御措施,反射在通过newInstance创建对象时会检查这个类是否是枚举类,如果是枚举类就会throw new IllegalArgumentException("Cannot reflectively create enum objects");,如下是源码java.lang.reflect.Constructor#newInstance

    public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            if (!this.override) {
                Class<?> caller = Reflection.getCallerClass();
                this.checkAccess(caller, this.clazz, this.clazz, this.modifiers);
            }
    
            if ((this.clazz.getModifiers() & 16384) != 0) {
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            } else {
                ConstructorAccessor ca = this.constructorAccessor;
                if (ca == null) {
                    ca = this.acquireConstructorAccessor();
                }
    
                T inst = ca.newInstance(initargs);
                return inst;
            }
        }
    

    五、单例模式存在哪些问题?

    单例模式有节省资源、保证结果正确、方便管理等优点,同时也存在一些问题。

    1、单例对 OOP 特性的支持不友好

    OOP 的四大特性是封装、抽象、继承、多态。单例模式对于其中的抽象、继承、多态都支持得不好。

    就拿唯一递增id生成器来说,上面实现的单例IdGenerator 的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。如果未来某一天,希望针对不同的业务采用不同的 ID 生成算法。比如,订单 ID 和用户 ID 采用不同的 ID 生成器来生成。为了应对这个需求变化,我们需要修改所有用到 IdGenerator 类的地方,这样代码的改动就会比较大。

    除此之外,单例对继承、多态特性的支持也不友好。从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,导致代码的可读性变差。所以,一旦选择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

    2、单例会隐藏类之间的依赖关系

    通过构造函数、参数传递等方式声明的类之间的依赖关系,查看函数的定义,就能很容易识别出来。但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。

    3、单例对代码的扩展性不友好

    单例类只能有一个对象实例。如果未来某一天,需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

    拿数据库连接池来举例说明,在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,初始把数据库连接池类设计成了单例类。后期我们想把慢sql和普通sql进行隔离,这样就需要建立两个数据库连接池实例。但是初始数据库连接池设计成了单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性和灵活性。

    所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

    4、单例不支持有参数的构造函数

    单例不支持有参数的构造函数,比如创建一个连接池的单例对象,没法通过参数来指定连接池的大小。解决方式是,将参数配置化。在单例实例化时,从外部读取参数。

    六、单例模式的替代方案

    为了保证全局唯一,除了使用单例,还可以用静态方法来实现。这其实就是平时常用的工具类了,但是它比单例更加不灵活,比如,它无法支持延迟加载,扩展性也很差。

    public class IdGeneratorUtil {
        private static AtomicLong id = new AtomicLong(0);
        public static long getId() {
            return id.incrementAndGet();
        }
    
        public static void main(String[] args) {
            System.out.println(IdGeneratorUtil.getId());
        }
    }
    

    实际上,类对象的全局唯一性可以通过多种不同的方式来保证,单例模式、工厂模式、IOC 容器(比如 Spring IOC 容器),还可以通过自我约束,在编写代码时自我保证不要创建两个对象。

    七、重新理解单例模式的唯一性

    1、单例模式的唯一性

    经典的单例模式,创建的全局唯一对象,属于进程唯一性,如果一个系统部署了多个实例,就有多个进程,每个进程都存在唯一一个对象,且进程间的实例对象不是同一个对象。

    2、实现线程唯一的单例

    实现线程唯一的单例,就是一个对象在一个线程只能存在一个,不同的线程有不同的对象。

    /**
     * 线程唯一单例
     */
    public class ThreadIdGenerator {
        private AtomicLong id = new AtomicLong(0);
        private static final ConcurrentHashMap<Long, ThreadIdGenerator> instances = new ConcurrentHashMap<Long, ThreadIdGenerator>();
    
        private ThreadIdGenerator() {}
    
        public ThreadIdGenerator getInstance() {
            long threadId = Thread.currentThread().getId();
            instances.putIfAbsent(threadId, new ThreadIdGenerator());
            return instances.get(threadId);
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    

    在代码中,通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样就可以做到不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。

    /**
     * 线程唯一单例  ThreadLocal
     */
    public class ThreadLocalIdGenerator {
        private AtomicLong id = new AtomicLong(0);
        private static ThreadLocal<ThreadLocalIdGenerator> instances = new ThreadLocal<ThreadLocalIdGenerator>();
        private ThreadLocalIdGenerator(){}
        public static ThreadLocalIdGenerator getIntance() {
            ThreadLocalIdGenerator instance = instances.get();
            if (instance != null) {
                return instance;
            }
            instance = new ThreadLocalIdGenerator();
            instances.set(instance);
            return instance;
        }
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    // 代码测试:
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    System.out.println("sub1: " + ThreadLocalIdGenerator.getIntance().toString() + "-->" + ThreadLocalIdGenerator.getIntance().getId());
                    System.out.println("sub2: " + ThreadLocalIdGenerator.getIntance().toString() + "-->" + ThreadLocalIdGenerator.getIntance().getId());
    
                }
            };
            thread.start();
            thread.join();
            
            System.out.println("main1: " + ThreadLocalIdGenerator.getIntance().toString() + "-->" + ThreadLocalIdGenerator.getIntance().getId());
            System.out.println("main2: " + ThreadLocalIdGenerator.getIntance().toString() + "-->" + ThreadLocalIdGenerator.getIntance().getId());
        }
    }
    // 控制台打印
    sub1: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@480a0d08-->1
    sub2: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@480a0d08-->2
    main1: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@4f3f5b24-->1
    main2: com.stefan.designPattern.singleton.ThreadLocalIdGenerator@4f3f5b24-->2  
    

    显然主线程和子线程生成的单例不是同一个,且一个线程调用多次生成的是同一个单例对象。

    3、实现集群唯一的单例

    集群代表着有多个进程,实现集群唯一单例,就是进程间共享一个对象。

    集群唯一的单例实现起来相对复杂。

    (1)需要考虑共享对象存放在哪里,

    (2)进程创建单例对象时需要加分布式锁,防止多个进程创建多个不同的对象。

    4、实现一个多例模式

    “单例”指的是,一个类只能创建一个对象,对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。

    (1)以map存储多例的方式
    /**
     * 多例模式的唯一自增id生成器
     *  用户表一个IdGenerator
     *  商品表一个IdGenerator
     *  订单表一个IdGenerator
     */
    public class MultIdGenerator {
        private AtomicLong id = new AtomicLong(0);
        private MultIdGenerator() {}
        public long getId() {
            return id.incrementAndGet();
        }
        private  static Map<String, MultIdGenerator> instances = new HashMap<String, MultIdGenerator>();
        static {
            instances.put(Type.USER, new MultIdGenerator());
            instances.put(Type.PRODUCT, new MultIdGenerator());
            instances.put(Type.ORDER, new MultIdGenerator());
        }
    
        public static MultIdGenerator getInstance(String type) {
            return instances.get(type);
        }
    
        public static final class Type {
            public static final String USER = "user";
            public static final String PRODUCT = "product";
            public static final String ORDER = "order";
        }
    }
    // 测试
    public class Test3 {
        public static void main(String[] args) {
            System.out.println("USER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).toString() + "-->" + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).getId());
            System.out.println("USER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).toString() + "-->" + MultIdGenerator.getInstance(MultIdGenerator.Type.USER).getId());
    
            System.out.println("PRODUCT: " + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).toString() + "-->" + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).getId());
            System.out.println("PRODUCT: " + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).toString() + "-->" + MultIdGenerator.getInstance(MultIdGenerator.Type.PRODUCT).getId());
    
            System.out.println("ORDER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).toString() + "-->" + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).getId());
            System.out.println("ORDER: " + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).toString() + "-->" + MultIdGenerator.getInstance(MultIdGenerator.Type.ORDER).getId());
    
        }
    }
    
    // 控制台
    USER: com.stefan.designPattern.singleton.MultIdGenerator@4f3f5b24-->1
    USER: com.stefan.designPattern.singleton.MultIdGenerator@4f3f5b24-->2
    PRODUCT: com.stefan.designPattern.singleton.MultIdGenerator@15aeb7ab-->1
    PRODUCT: com.stefan.designPattern.singleton.MultIdGenerator@15aeb7ab-->2
    ORDER: com.stefan.designPattern.singleton.MultIdGenerator@7b23ec81-->1
    ORDER: com.stefan.designPattern.singleton.MultIdGenerator@7b23ec81-->2
    
    (2)使用枚举实现多例模式
    /**
     * 多例模式的唯一自增id生成器
     *  用户表一个IdGenerator
     *  商品表一个IdGenerator
     *  订单表一个IdGenerator
     *
     *  枚举无法实现多例模式
     */
    public enum MultIdGeneratorEnum {
        USER,
        PRODUCT,
        ORDER;
        private AtomicLong id = new AtomicLong(0);
    
        public long getId() {
            return id.incrementAndGet();
        }
    }
    测试
    public class Test2 {
        public static void main(String[] args) {
            System.out.println("USER: " + MultIdGeneratorEnum.USER.toString() + "-->" + MultIdGeneratorEnum.USER.getId());
            System.out.println("USER: " + MultIdGeneratorEnum.USER.toString() + "-->" + MultIdGeneratorEnum.USER.getId());
    
            System.out.println("PRODUCT: " + MultIdGeneratorEnum.PRODUCT.toString() + "-->" + MultIdGeneratorEnum.PRODUCT.getId());
            System.out.println("PRODUCT: " + MultIdGeneratorEnum.PRODUCT.toString() + "-->" + MultIdGeneratorEnum.PRODUCT.getId());
    
            System.out.println("ORDER: " + MultIdGeneratorEnum.ORDER.toString() + "-->" + MultIdGeneratorEnum.ORDER.getId());
            System.out.println("ORDER: " + MultIdGeneratorEnum.ORDER.toString() + "-->" + MultIdGeneratorEnum.ORDER.getId());
            //USER: USER-->1
            //USER: USER-->2
            //PRODUCT: PRODUCT-->1
            //PRODUCT: PRODUCT-->2
            //ORDER: ORDER-->1
            //ORDER: ORDER-->2
        }
    }
    

    枚举的方式明显要简洁很多,强烈推荐使用枚举方式实现单例和多例模式。 通过反编译查看源码,枚举方式实现的多例其实和前面以map存储多例的方式差不多的原理,都是用HashMap把实例化对象存储起来。

    使用jad反编译MultIdGeneratorEnum.class :

    // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
    // Jad home page: http://www.kpdus.com/jad.html
    // Decompiler options: packimports(3) 
    // Source File Name:   MultIdGeneratorEnum.java
    
    package com.stefan.designPattern.singleton;
    
    import java.util.concurrent.atomic.AtomicLong;
    
    public final class MultIdGeneratorEnum extends Enum
    {
    
        public static MultIdGeneratorEnum[] values()
        {
            return (MultIdGeneratorEnum[])$VALUES.clone();
        }
    
        public static MultIdGeneratorEnum valueOf(String name)
        {
            return (MultIdGeneratorEnum)Enum.valueOf(com/stefan/designPattern/singleton/MultIdGeneratorEnum, name);
        }
    
        private MultIdGeneratorEnum(String s, int i)
        {
            super(s, i);
            id = new AtomicLong(0L);
        }
    
        public long getId()
        {
            return id.incrementAndGet();
        }
    
        public static final MultIdGeneratorEnum USER;
        public static final MultIdGeneratorEnum PRODUCT;
        public static final MultIdGeneratorEnum ORDER;
        private AtomicLong id;
        private static final MultIdGeneratorEnum $VALUES[];
    
        static 
        {
            USER = new MultIdGeneratorEnum("USER", 0);
            PRODUCT = new MultIdGeneratorEnum("PRODUCT", 1);
            ORDER = new MultIdGeneratorEnum("ORDER", 2);
            $VALUES = (new MultIdGeneratorEnum[] {
                USER, PRODUCT, ORDER
            });
        }
    }
    

    多例模式很像工厂模式,它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。

    八、枚举源码补充

    一个枚举类反编译之后,可以看到其继承自java.lang.Enum,其中有一个valueof的方法是直接调用java.lang.Enum#valueOf的。其底层是一个keynamevalueEnum类型的实例化对象。通过源码可以验证:

    package java.lang;
    
    import java.io.IOException;
    import java.io.InvalidObjectException;
    import java.io.ObjectInputStream;
    import java.io.ObjectStreamException;
    import java.io.Serializable;
    
    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
        private final String name;
        private final int ordinal;
    
        public final String name() {
            return this.name;
        }
    
        public final int ordinal() {
            return this.ordinal;
        }
    
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
    
        public String toString() {
            return this.name;
        }
    
        public final boolean equals(Object other) {
            return this == other;
        }
    
        public final int hashCode() {
            return super.hashCode();
        }
    
        protected final Object clone() throws CloneNotSupportedException {
            throw new CloneNotSupportedException();
        }
    
        public final int compareTo(E o) {
            if (this.getClass() != o.getClass() && this.getDeclaringClass() != o.getDeclaringClass()) {
                throw new ClassCastException();
            } else {
                return this.ordinal - o.ordinal;
            }
        }
    
        public final Class<E> getDeclaringClass() {
            Class<?> clazz = this.getClass();
            Class<?> zuper = clazz.getSuperclass();
            return zuper == Enum.class ? clazz : zuper;
        }
    
        public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
            //关键性代码,好比是hashmap缓存
            T result = (Enum)enumType.enumConstantDirectory().get(name);
            if (result != null) {
                return result;
            } else if (name == null) {
                throw new NullPointerException("Name is null");
            } else {
                throw new IllegalArgumentException("No enum constant " + enumType.getCanonicalName() + "." + name);
            }
        }
    
        protected final void finalize() {
        }
    
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            throw new InvalidObjectException("can't deserialize enum");
        }
    
        private void readObjectNoData() throws ObjectStreamException {
            throw new InvalidObjectException("can't deserialize enum");
        }
    }
    

    java.lang.Enum#valueOf中第一行代码调用的是java.lang.Class#enumConstantDirectory,部分源码如下,Class类中有一个成员变量是HashMap,原理一目了然。

    private transient volatile Map<String, T> enumConstantDirectory;
    Map<String, T> enumConstantDirectory() {
            Map<String, T> directory = this.enumConstantDirectory;
            if (directory == null) {
                T[] universe = this.getEnumConstantsShared();
                if (universe == null) {
                    throw new IllegalArgumentException(this.getName() + " is not an enum type");
                }
    
                directory = new HashMap((int)((float)universe.length / 0.75F) + 1);
                Object[] var3 = universe;
                int var4 = universe.length;
    
                for(int var5 = 0; var5 < var4; ++var5) {
                    T constant = var3[var5];
                    ((Map)directory).put(((Enum)constant).name(), constant);
                }
    
                this.enumConstantDirectory = (Map)directory;
            }
            return (Map)directory;
    }
    

    如有错误和遗漏,欢迎在评论区指正和补充。共同学习,共同进步。

    展开全文
  • 使用枚举实现单例

    2019-03-22 16:33:49
    枚举实现单例,除了线程同步之外貌似没有其他什么作用吧?我的写法不对吗? package thread;... * 线程安全、防反射、防反序列化的单例模式 * * @author LYW * @date 2019/3/21 * @since J...

    用枚举实现单例,除了线程同步之外貌似没有其他什么作用吧?是我的写法不对吗?

    package thread;
    
    import java.io.ObjectStreamException;
    import java.io.Serializable;
    
    /**
     * 线程安全、防反射、防反序列化的单例模式
     *
     * @author LYW
     * @date 2019/3/21
     * @since JDK 1.8
     */
    public class SingletonDemo implements Serializable {
    
    	private static final long serialVersionUID = 8371689835544598243L;
    
    	private SingletonDemo() {
    		if (Singleton.INSTANCE != null) {
    			throw new RuntimeException("单例模式被侵犯!");
    		}
    	}
    
    	public static SingletonDemo getInstance() {
    		return Singleton.INSTANCE.getInstance();
    	}
    
    	/**
    	 * 防反序列化,在反序列化获取对象时,直接返回当前对象
    	 *
    	 * @return
    	 * @throws ObjectStreamException
    	 */
    	private Object readResolve() throws ObjectStreamException {
    		return Singleton.INSTANCE.getInstance();
    	}
    
    	private enum Singleton {
    		INSTANCE();
    
    		private SingletonDemo instance;
    
    		Singleton() {
    			instance = new SingletonDemo();
    		}
    
    		public SingletonDemo getInstance() {
    			return instance;
    		}
    	}
    }
    

     

    展开全文
  • 单例模式

    2018-06-15 14:59:12
    单例模式 单例模式是最常见也最简单的设计...枚举实现单例模式 注意 关于volatile 饿汉模式 顾名思义吗,饿汉模式就是在一开始便将单例实现,保证所有的实例都同一个在最初便实现的单例。 ...
  • 转载:你真的会写单例模式吗——Java实现 单例模式可能代码最少的模式了,但是少不一定意味着简单,想要用好、用对单例模式,还真得费一番脑筋。本文对Java中常见的单例模式写法做了一个总结,如有错漏之处,恳请...
  • 设计模式之单例模式-单例模式的几种实现方式及小案例 本文来源:凯哥Java(wx:kaigejava) 单例模式有几种?饿汉式、懒汉式。这两种最常见的。还有几种对其扩展的。具体如下: 我们可以从上图看到,共有六...
  • 设计模式之单例模式-单例模式的几种实现方式及小案例本文来源:凯哥Java(wx:kaigejava)单例模式有几种?饿汉式、懒汉式。这两种最常见的。还有几种对其扩展的。具体如下:我们可以从上图看到,共有六种方式。...
  • 目录饿汉法单线程写法考虑线程安全的写法兼顾线程安全和效率的写法坑静态内部类法枚举写法总结参考资料转载: 你真的会写单例模式吗——Java实现单例模式可能代码最少的模式了,但是少不一定意味着简单,想要用好...
  • 单例模式是我们大部分人接触的第一个设计模式,因此网上的分享泛滥,良莠不齐,甚至出现不少错误。因为比较简单,所以没有自行研究学习,只是从网上的分享中吸收。前不久与同事的交流中发现与自己的认知有些出入,...
  • 单例模式在我们书写代码中最经常使用的一种设计模式,但是这种设计模式真的安全吗?如果不安全的话,我们有没有安全的单例模式?其实这也大厂面试的时候可能会问道的面试题,本篇我们来研究下这个问题。 引出...
  • 接上回的单例模式线程是否安全? https://blog.csdn.net/weixin_45262118/article/details/108519818 我们先来谈谈枚举 枚举是JDK1.5推出的新特性,本身也是一个class类 我们先创建一个枚举 public enum EnumTest { ...

空空如也

空空如也

1 2 3 4
收藏数 74
精华内容 29
关键字:

枚举是单例模式吗