单例模式
单例模式
单例模式,属于创建类型的一种常见的软件设计模式。通过单例模式可以确保一个类在当前进程中只有一个实例。当然,根据需要,也可能时一个线程中的单例,比如线程上下文内使用同一个实例。
单例模式一共有两种方式:饿汉式,懒汉式
饿汉式
所谓饿汉式就是在类加载的时候就进行初始化目标对象,以后去获取该的单例对象时就直接获取即可。
1 | class Singleton implements Serializable { |
- 需要设置为private,防止被别的类改动,破坏单例,但是private并不能防止通过反射破坏单例。
- 需要添加 final ,防止子类中可能会使用什么方法去给 instance赋值,来破坏单例
- 如果 Singleton 实现了序列化接口, 需要实现readResovle()方法,返回单例对象,来防止反序列化破坏单例。反序列化的时候,如果发现readResovle()返回了一个对象,则会直接将该对象当成反序列化的结果。
饿汉式的好处就是实现起来简单,不需要考虑并发问题。但是由于在类加载的时候就创建了对象,如果在程序运行的过程种没有任何线程去获取该对象(该对象没有被使用),就会浪费内存。
懒汉式
懒汉式主要使用的就是一种懒加载的思想,在程序需要去获取该对象的时候再去创建。
实现一(无法保证单例)不可用
1 | class Singleton { |
这种实现过于简单,会出现问题,如果多个线程同时去获取Singleton,如果一个线程执行到if (instance == null) 还没来得及往下执行,另一个线程也进行了这个判断,发现 instance 为空,也去创建了一个实例。这样就会出现多个实例。
实现二(加锁)
解决实现一,最简单的思路就是加锁,让在同一个时间内,只能有一个线程执行获取对象的方法
1 | class Singleton implements Serializable { |
只需要加上synchronized,可以保证线程安全。这种方法虽然保证只有一个实例,但是在第一次创建对象以后,其实加锁是没有必要的。但是每次获取对象时都要获取锁,降低了性能低下
实现三(double-check locking)
1 | class Singleton implements Serializable { |
如果检测到instance为null才去竞争。如果已经初始化,就不需要去竞争锁了,直接返回对象即可。但是这种方法并非完美。因为指令重排,会导致有序性出现问题。比如instance = new Singleton()
的指令如下:
1 | 0: new #3 //1. 加载类(如果需要); 2.堆空间开辟一块内存) |
java虚拟机可能会对其进行优化,进行指令重排。指令 4 和 7 可能会发生重排序,即:先执行 7 然后执行 4,先给静态变量赋值,然后调用构造器构造该对象。
假设有两个线程,线程一和线程二
一开时instance为空,线程一调用getInstance方法,判断instance为空,进入到synchronized代码块
线程一执行new,dup,putstatic。此时还没有执行invokespecial。就已经给静态变量instance赋值了
这个时候线程二也调用了getInstance,判断instance不为空,直接返回了该对象,但是此时instance其实还未被初始化。
解决方法:只需要给instance加一个volatile修饰即可,会再读取instance之前加读屏障(防止读屏障之后的指令重排序到读屏障前),在给instance赋值以后加入写屏障(防止写屏障之前的指令重排序到写屏障后)。
1 | class Singleton implements Serializable { |