以下是以前总结的 C++ 版的;软件开发经常使用设计模式—单例模式总结(c++版),对比发现 Java 实现的单例模式和 C++ 的在线程安全上仍是有些区别的。html
概念很少说,没意思,我本身总结就是:java
有这样一个类,该类在生命周期内有且只能有一个实例,该类必须本身建立本身的这个惟一实例,该类必须给全部其余对象提供这一实例(提供全局访问点),这样的类就叫单例类。c++
简单的说就是知足三个条件:设计模式
一、生命周期内有且只能有一个实例安全
二、本身提供这个独一无二的实例多线程
三、该实例必须是能全局访问的并发
进一步,单例类,最好能实现懒加载,随用随生成,而不是初始化的时候就生成,提升启动速度和优化内存。ide
还有应该考虑并发环境下的场景,多线程的单例模式实现有什么难点,回答这个问题,必须先知道Java的内存模型,参考:JVM学习(3)——总结Java内存模型工具
考虑黑客会作反序列化的攻击post
考虑黑客会作反射的攻击,由于反射能够访问私有方法
。。。
若是程序确认没有多线程的使用场景,彻底能够简单一些写。
public class NoThreadSafeLazySingleton { private static NoThreadSafeLazySingleton lazySingleton = null; private NoThreadSafeLazySingleton() { } public static NoThreadSafeLazySingleton getLazySingleton() { if (lazySingleton == null) { lazySingleton = new NoThreadSafeLazySingleton(); } return lazySingleton; } }
很简单,可是只适用于单线程环境
原理也很简单,没什么可说的,以下示例代码:
public class ThreadSafeLazySingleton { private static volatile ThreadSafeLazySingleton lazySingleton = null; private ThreadSafeLazySingleton() { } public static ThreadSafeLazySingleton getLazySingleton() { if (lazySingleton == null) { synchronized (ThreadSafeLazySingleton.class) { if (lazySingleton == null) { lazySingleton = new ThreadSafeLazySingleton(); } } } return lazySingleton; } }
主要是注意 volatile 关键字的使用,不然这种所谓双重检查的线程安全的单例是有 bug 的。参考:JVM学习(3)——总结Java内存模型
在某些状况中,JVM 隐含了同步操做,这些状况下就不用本身再来进行同步控制了。这些状况包括:
由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
访问final字段时
在建立线程以前建立对象时
线程能够看见它将要处理的对象时
在静态内部类里去建立本类(外部类)的对象,这样只要不使用这个静态内部类,那就不建立对象实例,从而同时实现延迟加载和线程安全。
public class Person { private String name; private Integer age; private Person() { } private Person(String name, Integer age) { this.name = name; this.age = age; } // 在静态内部类里去建立本类(外部类)的对象 public static Person getInstance() { return Holder.instatnce; } // 静态内部类至关于外部类 Person 的 static 域,它的对象与外部类对象间不存在依赖关系,所以可直接建立。 // 由于静态内部类至关于其外部类 Person 的静态成员,因此在第一次被使用的时候才被会装载,且只装载一次。 private static class Holder { // 内部类的对象实例 instatnce ,是绑定在外部 Person 对象实例中的 // 静态内部类中能够定义静态方法,在静态方法中只可以引用外部类中的静态成员方法或者成员变量,好比 new Person // 使用静态初始化器来实现线程安全的单例类,它由 JVM 来保证线程安全性。 private static final Person instatnce = new Person("John", 31); } }
静态内部类至关于外部类 Person 的 static 域(静态成员),它的对象与外部类对象间不存在依赖关系,所以可直接建立。
既然,静态内部类至关于其外部类 Person 的静态成员,因此在第一次被使用的时候才被会装载,且只装载一次,实现了懒加载和单例。
并且,使用静态初始化器来实现单例类,是线程安全的,由于由 JVM 来保证线程安全性
客户端调用
Person person = Person.getInstance();
该方案实现了,线程安全的单例 + 懒加载的单例,可是并不能防反序列化攻击,须要额外的加以约束。
其实这个 case 不必说太多,知道就行,由于哪里就这么巧,一个能序列化的类(实现了Serializable/Externalizable接口的类),就偏偏是单例的呢?
看下面例子,把 Person 类改造为能序列化的类,而后用反序列攻击单例
public class SerializationTest { public static void main(String[] args) throws IOException, ClassNotFoundException { Person person = Person.getInstance(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person")); objectOutputStream.writeObject(person); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("person")); Person person1 = (Person) objectInputStream.readObject(); System.out.println(person == person1); // false } }
比较两个 person 实例地址,是 false,说明生成了两个对象,违背了单例类的初衷,那么为了能在序列化过程仍能保持单例的特性,能够在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象
public Object readResolve() { return Holder.instatnce; }
原理是当从 I/O 流中读取对象时,ObjectInputStream 类里有 readResolve() 方法,该方法会被自动调用,期间通过种种逻辑,最后会调用到可序列化类里的 readResolve()方法,这样能够用 readResolve() 中返回的单例对象直接替换在反序列化过程当中建立的对象,实现单例特性。
也就是说,不管如何,反序列化都会额外建立对象,只不过使用 readResolve() 方法能够替换之。
具体有关Java 对象的序列化能够参考笔记:深刻理解Java对象序列化
直接看例子,作法很简单,经过 Java 的反射机制,看看能不能拿到单例类的私有构造器,而且改变构造器的访问属性
public class ReflectTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException { Person person = Person.getInstance(); Class clazz = Class.forName("com.dashuai.D13Singleton.Person"); Constructor constructor = clazz.getDeclaredConstructor(); // constructor.setAccessible(true); Person person1 = (Person) constructor.newInstance(); System.out.println(person == person1); // false } }
运行抛出了异常:
可是,若是把注释的行打开,就不会出错,且打印 false。
网上有一些解决方案,好比在构造器里加判断,若是二次调用就抛出异常,其实也没从根本上解决问题。
目前公认的最佳方案,代码极少,线程安全,防止反射和序列化攻击
public enum EnumSingleton { ENUM_SINGLETON; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } ////////////////////////调用 EnumSingleton.ENUM_SINGLETON.setName("dashuai"); System.out.println(EnumSingleton.ENUM_SINGLETON.getName());
全部的变量都是单例的。至于为何,能够经过反编译工具查看枚举的源码。能够安装 idea 的 jad 插件,会发现就是按照单例模式设计的。
享元模式是对象级别的, 也就是说在多个使用到这个对象的地方都只须要使用这一个对象便可知足要求。
单例模式是类级别的, 就是说这个类必须只能实例化出来一个对象。
能够这么说, 单例是享元的一种特例, 设计模式不用拘泥于具体代码, 代码实现可能有n多种方式, 而单例能够看作是享元的实现方式中的一种, 他比享元模式更加严格的控制了对象的惟一性
一、单例类只能有一个实例。
二、单例类必须本身建立本身的惟一实例。
三、单例类必须给全部其余对象提供这一实例。
java.lang.runtime getRuntime,代码也很简单。