这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战java
单例模式
- 单例模式是一种建立模式,单例类负责本身建立本身的对象而且一个类只有一个实例对象,而且向整个系统提供这个实例.系统能够直接访问这个实例而不须要实例化
- 单例模式的特色:
- 单例类只有一个实例
- 单例类必须本身建立自身的惟一实例
- 单例类必须给其他系统对象提供建立的惟一实例
单例模式的实现方式
- 单例模式要保证一个类只有一个实例,而且提供给全局访问,主要用于解决一个全局使用的类频繁建立和销毁的问题,经过判断系统是否存在这个单例来解决这样的问题,若是有这个单例则返回这个单例,不然就建立这个单例,只要保证构造函数是私有的便可
- 保证一个类只有一个实例: 将该类的构造方法定义为私有方法便可
- 提供全局一个该实例的访问点: 单例类本身建立实例,提供一个静态方法做为实例的访问点便可
- 饿汉和懒汉比较:
- 懒汉: 单例类对象实例懒加载,不会提早建立对象实例,只有在使用对象实例的时候才会建立对象实例
- 饿汉: 在单例对象实例进行声明引用时就进行实例化建立对象实例
- 单例模式除去线程不安全的懒汉,一般有五种实现方式:
- 通常状况下,直接使用饿汉实现单例模式
- 若是明确要求懒加载一般使用静态内部类实现单例模式
- 若是有关于反序列化建立对象会考虑使用枚举实现单例模式
- 静态类Static :
- 静态类在第一次运行时直接初始化,也不须要在延迟加载中使用
- 在不须要维持任何状态,仅仅用于全局访问时,使用静态类的方式更加方便
- 若是须要被继承或者须要维持一些特定状态下的状况,就适合使用单例模式
线程不安全懒汉
线程安全懒汉
双检锁
- 双重检查锁模式: doule checked locking pattern
- 使用同步块加锁的方法
- 会有两次检查instance == null
- 一次在同步块外
- 一次在同步块内
- 由于会有多个线程一块儿进入同步块外的if中
- 若是不在同步块内不进行二次检验就会致使生成多个实例
- 单例模式双检锁Singleton示例
- volatile:
- 对于计算机中的指令而言 ,CPU和编译器为了提高程序的执行效率,一般会按照必定的规则对指令进行优化
- 若是两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序
- 形如instance = new Instance() 方法建立实例执行分为三步:
- 分配对象内存空间: 给新建立的Instance对象分配内存
- 初始化对象: 调用单例类的构造函数来初始化成员变量
- 设置instance指向新建立的对象分配的内存地址,此时instance != null
- 由于上面的初始化对象和设置instance指向新建立的对象分配的内存地址不存在数据上的依赖关系,不管哪一步先执行都不会影响最终结果,因此程序在编译时,顺序就会发生改变:
- 分配对象内存空间
- 设置instance指向新建立对象分配的内存地址
- 初始化对象
- CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 若是不加volatile关键字,若是有多个线程访问getInstance() 方法时,若是恰好发生了指令重排,可能会出现如下状况:
- 当第一个线程获取锁而且进入到第二个if方法后,先分配内存空间,而后instance指向刚刚分配的内存地址,此时instance不等于null. 可是此时instance尚未初始化完成
- 若是此时有另外一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会致使程序NPE异常
- 使用volatile的缘由是禁止指令从新排序:
- 在volatile变量进行赋值操做后会有一个内存隔离
- 读操做不会重排序到内存隔离之中
- 好比在上面操做中,读操做必须在执行完1,2,3或者1,3,2步骤以后才会执行读取到结果,不然不会读取到相关结果
饿汉
- 单例模式饿汉Singleton示例
- 优势:
- 在单例类中,装载类的时候就建立对象实例.由于单例类的实例声明为static的final变量,在第一次加在类到内存中时就会初始化,因此建立实例自己时线程安全的
- 缺点:
- 饿汉模式不是一种懒加载模式,即使客户端没有调用getInstance() 方法,单例类也会在类第一次加载时初始化
- 使用饿汉模式建立单例类实例在某些场景中没法使用:
- 好比由于饿汉建立的实例声明为final变量
- 若是单例类Singleton的实例的建立依赖参数或者配置文件
- 须要在getInstance() 方法以前调用方法为单例类的实例设置参数,此时这种饿汉模式就没法使用
静态内部类
- 单例模式静态内部类Singleton示例
- 使用静态内部类模式建立单例类实例是使用JVM机制保证线程安全:
- 静态单例对象没有做为单例类的成员变量直接实例化,因此当类加载时不会实例化单例类
- 第一次调用getInstance() 方法时将加载静态内部类Nest. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量
- 经过JVM来保证线程安全,确保该成员变量只初始化一次
- 因为getInstance() 方法并无加线程锁,因此对性能没有什么影响
- 静态内部类的优势:
- 静态内部类Nest是私有的,只能经过getInstance() 方法进行访问,因此这是懒加载的
- 读取实例时不会进行同步锁的获取,性能较好
- 静态内部类不依赖JDK版本
枚举
- 单例模式枚举Singleton示例
- 使用枚举方式实现单例的最大特色是很是简单
- 能够经过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单
- 枚举的建立默认就是线程安全的方法,并且能防止反射以及反序列化致使从新建立新的对象
- Enum类内部使用Enum类型断定防止经过反射建立新的对象
- Enum类经过对象的类型和枚举名称将对象进行序列化,而后经过valueOf() 方法匹配枚举名称找到内存中的惟一对象实例,这样能够防止反序列化时建立新的对象
- 懒汉式和饿汉式实现的单例模式破坏 : 不管是经过懒汉式仍是饿汉式实现的单例模式,均可能经过反射和反序列化破坏掉单例的特性,能够建立多个对象
- 反射破坏单例模式: 利用反射,能够强制访问单例类的私有构造器,建立新的对象
public static void main(String[] args) {
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessiable(true);
Singleton newInstance = constructor.newInstance();
Singleton singletonInstance = Singleton.getInstance();
System.out.println(singletonInstance == newInstance);
}
复制代码
- 反序列化破坏单例模式: 经过readObject() 方法读取对象时会返回一个新的对象实例
public static void main(String[] args) {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
Singleton singletonInstance = Singleton.getInstance();
os.writeObject(singleton);
File file = new File("Singleton.file");
ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton)is.readObject();
System.out.println(singletonInstance == newInstance);
}
复制代码