简述设计模式——单例模式

单例模式(Singleton Pattern)的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。本质:控制实例的数目。

1、饿汉模式

public class HungrySingletonTest {
    public static void main(String[] args) {
        // 判断两次获取的实例是否相同
        System.out.println(HungrySingleton.getInstance() == HungrySingleton.getInstance());
    }
}

class HungrySingleton {
    // 直接在这里创建对象,只能创建一次
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    // 私有化构造方法,可以在内部控制创建实例的数目
    private HungrySingleton() {
    }
    
    // 定义一个方法来为客户端提供实例
    public static HungrySingleton getInstance() {
        // 直接返回已经创建好的实例
        return hungrySingleton;
    }
}

运行结果:

true

饿汉模式是典型的空间换时间

2、懒汉模式

public class LazySingletonTest {
    public static void main(String[] args) {
        // 判断两次获取的实例是否相同
        System.out.println(LazySingleton.getInstance() == LazySingleton.getInstance());
    }
}

class LazySingleton {
    // 定义一个变量来存储创建好的实例
    private static LazySingleton singleton;

    public static LazySingleton getInstance() {
        // 判断存储实例的变量是否有值
        if (null == singleton) {
            // 没有,就创建一个实例,并把值赋给存储类实例的变量
            singleton = new LazySingleton();
        }
        // 有值就直接使用
        return singleton;
    }
}

运行结果:

true

但是如果我们对上面的代码稍做改动,如下:

public class LazySingletonTest {
    public static void main(String[] args) {
        new Thread(() -> {
            LazySingleton s1 = LazySingleton.getInstance();
            System.out.println("s1:"+s1);
        }).start();

        new Thread(() -> {
            LazySingleton s2 = LazySingleton.getInstance();
            System.out.println("s2:"+s2);
        }).start();
    }
}


class LazySingleton {
    // 定义一个变量来存储创建好的实例
    private static LazySingleton singleton;

    public static LazySingleton getInstance() {
        // 判断存储实例的变量是否有值
        if (null == singleton) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 没有,就创建一个实例,并把值赋给存储类实例的变量
            singleton = new LazySingleton();
        }
        // 有值就直接使用
        return singleton;
    }
}

运行结果:

s1:LazySingleton@61553549
s2:LazySingleton@670db704

以上代码就会出现并发了,会创建两个不同的实例了。
说明懒汉模式是非线程安全的。那如何实现懒汉模式的线程安全呢?只要加上synchronized即可,如下:

public static synchronized LazySingleton getInstance(){...}

3、双重校验加锁

public class LazySingletonTest {
    public static void main(String[] args) {
        new Thread(() -> {
            LazySingleton s1 = LazySingleton.getInstance();
            System.out.println("s1:"+s1);
        }).start();

        new Thread(() -> {
            LazySingleton s2 = LazySingleton.getInstance();
            System.out.println("s2:"+s2);
        }).start();
    }
}


class LazySingleton {
    // 定义一个变量来存储创建好的实例
    private volatile static LazySingleton singleton;

    public static synchronized LazySingleton getInstance() {
        // 判断存储实例的变量是否有值,对实例的变量添加volatile的修饰
        if (null == singleton) {
            // 同步块,线程安全的创建实例
            synchronized (LazySingleton.class){
                // 再次检查实例是否存在,如果不存在才真正的创建实例
                if(null ==  singleton){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 没有,就创建一个实例,并把值赋给存储类实例的变量
                    singleton = new LazySingleton();
                }
            }
        }
        // 有值就直接使用
        return singleton;
    }
}

运行结果:

s1:LazySingleton@670db704
s2:LazySingleton@670db704

volatile关键字保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)。禁止进行指令重排序。(实现有序性)。volatile 只能保证对单次读/写的原子性。

由于volatile关键字可能会屏蔽掉虚拟机的一些不必要的代码优化,所以运行的效率并不是很高。我们建议没有特殊的需要,不要使用“双重检查加锁”机制来实现线程安全的单例。

4、类级内部类

有没有一种方案,既可以实现延迟加载,又可以实现线程安全呢?这种方案就是综合使用了Java的类级内部类和多线程缺省同步锁的只是,很巧妙地同时实现延迟加载和线程安全。

public class InnerclassSingletonTest {
    public static void main(String[] args) {
        System.out.println(InnerclassSingleton.getInstance() == InnerclassSingleton.getInstance());
    }
}

class InnerclassSingleton {

    private static class InnerclassSingletonHolder {
        private static InnerclassSingleton singleton = new InnerclassSingleton();
    }

    private InnerclassSingleton() {
    }

    public static InnerclassSingleton getInstance() {
        return InnerclassSingletonHolder.singleton;
    }
}

运行结果:

true

类级内部类是怎么实现延迟加载和线程安全的呢?
类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。

现在我们对上面的代码做如下修改:

public static void main(String[] args) throws Exception{
        Constructor<InnerclassSingleton> declaredConstructor = InnerclassSingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        InnerclassSingleton s1 = declaredConstructor.newInstance();
        InnerclassSingleton s2 = declaredConstructor.newInstance();
        System.out.println(s1 == s2);
    }

运行结果:

false

运行结果两个实例并不是是一个对象。这里实际就是单例模式受到反射的攻击。当然,我们还可以通过序列化实现Serializable。一个串行化的对象在每次返串行化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入readResolve 方法。

5、单元素枚举实现单例

public enum EnumSingleton {
    INSTANCE;

    private void method() {
        System.out.println("hello world!");
    }

    public static void main(String[] args) {
        System.out.println(EnumSingleton.INSTANCE == EnumSingleton.INSTANCE);
        EnumSingleton.INSTANCE.method();
    }
}

运行结果:

true
hello world!
  1. java的枚举类型实质上是功能齐全的类
  2. java枚举类型的基本思想是通过公有的静态final域为每个枚举常量导出实例的类。
  3. 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举。

使用枚举来实现控制会更加简洁,而且无偿地提供了序列化的机制,并由JVM从根本上提供保障,防止多次实例化,更是简洁、高效、安全的实现单例的方式。
单元素枚举实现单例,怎么防止反射攻击的呢?我们可以看下通过getDeclaredConstructor获取到的构建器会发现并没有我们所需的无参构造器,只有参数为(String.class,int.class)构造器。每个枚举对象拥有两个唯一的属性:String name 和 int ordinal,name就是我们在声明枚举变量是的名字(比如INSTANCE),ordinal就是声明的顺序(比如INSTANCE是第一个声明的,所以为0)。源码如下:

/**
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

单元素枚举实现单例,怎么防止反序列化攻击呢?我们看下枚举抽象类的valueOf方法

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

再看enumConstantDirectory方法

Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }

我们再看getEnumConstantsSharded方法

T[] getEnumConstantsShared() {
        if (enumConstants == null) {
            if (!isEnum()) return null;
            try {
                final Method values = getMethod("values");
                java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                        public Void run() {
                                values.setAccessible(true);
                                return null;
                            }
                        });
                @SuppressWarnings("unchecked")
                T[] temporaryConstants = (T[])values.invoke(null);
                enumConstants = temporaryConstants;
            }
            // These can happen when users concoct enum-like classes
            // that don't comply with the enum spec.
            catch (InvocationTargetException | NoSuchMethodException |
                   IllegalAccessException ex) { return null; }
        }
        return enumConstants;
    }

getEnumConstantsShared()方法获取枚举类的values()方法,然后得到枚举类所创建的所有枚举对象。
之前提到过,每个枚举对象都有一个唯一的name属性。序列化只是将name属性序列化,在反序列化的时候,通过创建一个Map(key,value),搭建起name和与之对应的对象之间的联系,然后通过索引key来获得枚举对象。

总结

建议在如下情况时,选用单例模式。
当需要控制一个类的实例只能有一个,并且客户只能从一个全局访问点访问它时,可以采用单例模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值