单例模式
- 1.饿汉式(线程安全)
- 2.懒汉式(线程不安全)
- 3.懒汉式(线程安全)
- 4. 双重校验
- 5. 静态内部类
- 6. 反射对于单例的破坏
- 7. 序列化对于单例的破坏
- 8.枚举(推荐方式)
1.饿汉式(线程安全)
在类加载期间初始化静态实例,保证 instance 实例的创建是线程安全的 ( 实例在类加载时实例化,有JVM保证线程安全).
特点: 不支持延迟加载实例(懒加载) , 此中方式类加载比较慢,但是获取实例对象比较快
问题: 该对象足够大的话,而一直没有使用就会造成内存的浪费。
public class Singleton_01 {
//1. 私有构造方法
private Singleton_01(){
}
//2. 在本类中创建私有静态的全局对象
private static Singleton_01 instance = new Singleton_01();
//3. 提供一个全局访问点,供外部获取单例对象
public static Singleton_01 getInstance(){
return instance;
}
}
2.懒汉式(线程不安全)
此种方式的单例实现了懒加载,只有调用getInstance方法时 才创建对象.但是如果是多线程情况,会出现线程安全问题.
public class Singleton_02 {
//1. 私有构造方法
private Singleton_02(){
}
//2. 在本类中创建私有静态的全局对象
private static Singleton_02 instance;
//3. 通过判断对象是否被初始化,来选择是否创建对象
public static Singleton_02 getInstance(){
if(instance == null){
instance = new Singleton_02();
}
return instance;
}
}
3.懒汉式(线程安全)
原理: 使用同步锁 synchronized
锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建
-
即,
getInstance()
方法块只能运行在1个线程中 -
若该段代码已在1个线程中运行,另外1个线程试图运行该块代码,则 会被阻塞而一直等待
-
而在这个线程安全的方法里我们实现了单例的创建,保证了多线程模式下 单例对象的唯一性
public class Singleton_03 {
//1. 私有构造方法
private Singleton_03(){
}
//2. 在本类中创建私有静态的全局对象
private static Singleton_03 instance;
//3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性
public static synchronized Singleton_03 getInstance(){
if(instance == null){
instance = new Singleton_03();
}
return instance;
}
}
4. 双重校验
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。实现步骤:
- 在声明变量时使用了 volatile 关键字,其作用有两个:
保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但 是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
- 将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被重复实例化 同时在调用getInstance()方法时不进行同步锁,效率高。
public class Singleton_04 {
//使用 volatile保证变量的可见性
private volatile static Singleton_04 instance = null;
private Singleton_04(){
}
//对外提供静态方法获取对象
public static Singleton_04 getInstance(){
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null){
synchronized (Singleton_04.class){
//抢到锁之后再次进行判断是否为null
if(instance == null){
instance = new Singleton_04();
}
}
}
return instance;
}
}
在双重检查锁模式中为什么需要使用 volatile 关键字?
在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
- 第一步是给 singleton 分配内存空间;
- 第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
- 第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错.
详细流程如下图所示:
线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,由于线程 1 被重排序,所以执行了新建实例的第三步,也就是把 singleton 指向之前分配出来的内存地址,在这第三步执行之后,singleton 对象便不再是 null。
这时线程 2 进入 getInstance 方法,判断 singleton 对象不是 null,紧接着线程 2 就返回 singleton 对象并使用,由于没有初始化,所以报错了。最后,线程 1 “姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。
使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。
5. 静态内部类
- 原理
根据 静态内部类 的特性(外部类的加载不影响内部类),同时解决了按需加载、线程安全的问题,同时实现简洁
- 在静态内部类里创建单例,在装载该内部类时才会去创建单例
- 线程安全:类是由
JVM
加载,而JVM
只会加载1遍,保证只有1个单例
public class Singleton_05 {
private static class SingletonHandler{
private static Singleton_05 instance = new Singleton_05();
}
private Singleton_05(){}
public static Singleton_05 getInstance(){
return SingletonHandler.instance;
}
}
6. 反射对于单例的破坏
反射技术过于强大,它可以通过setAccessible()
来修改构造器,字段,方法的可见性。单例模式的构造方法是私有的,如果将其可见性设为public
,那么将无法控制对象的创建。
public class Test_Reflect {
public static void main(String[] args) {
try {
//反射中,欲获取一个类或者调用某个类的方法,首先要获取到该类的Class 对象。
Class<Singleton_05> clazz = Singleton_05.class;
//getDeclaredXxx: 不受权限控制的获取类的成员.
Constructor c = clazz.getDeclaredConstructor(null);
//设置为true,就可以对类中的私有成员进行操作了
c.setAccessible(true);
Object instance1 = c.newInstance();
Object instance2 = c.newInstance();
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决方法之一: 在单例类的构造方法中 添加判断 instance != null
时,直接抛出异常
public class Singleton_05 {
private static class SingletonHandler{
private static Singleton_05 instance = new Singleton_05();
}
private Singleton_05(){
if(SingletonHandler.instance != null){
throw new RuntimeException("不允许非法访问!");
}
}
public static Singleton_05 getInstance(){
return SingletonHandler.instance;
}
}
7. 序列化对于单例的破坏
public class Test_Serializable {
@Test
public void test() throws Exception{
//序列化对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile.obj"));
oos.writeObject(Singleton.getInstance());
//序列化对象输入流
File file = new File("tempFile.obj");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton Singleton = (Singleton) ois.readObject();
System.out.println(Singleton);
System.out.println(Singleton.getInstance());
//判断是否是同一个对象
System.out.println(Singleton.getInstance() == Singleton);//false
}
}
输出结构为false,说明:
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性
。
解决方案:
/**
* 解决方案:只要在Singleton类中定义readResolve就可以解决该问题
* 程序会判断是否有readResolve方法,如果存在就在执行该方法,如果不存在--就创建一个对象
*/
private Object readResolve() {
return singleton;
}
枚举单例方式是<<Effective Java>>
作者推荐的使用方式,这种方式
8.枚举(推荐方式)
枚举单例方式是<<Effective Java>>
作者推荐的使用方式,这种方式
public enum Singleton_06{
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static Singleton_06 getInstance(){
return INSTANCE;
}
}
1: 为什么枚举类可以阻止反射的破坏?
真正原因是: 反射方法中不予许使用反射创建枚举对象
2: 为什么枚举类可以阻止序列化的破坏?
Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找
枚举对象。
比如说,序列化的时候只将INSTANCE
这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会
和之前被序列化的对象实例相同。