Java的单例模式


单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

实现方法:

  1. 懒汉式
  2. 饿汉式
  3. 静态内部类

饿汉式

public class LazyMan{
	// 1. 私有化构造函数
	private LazyMan(){}
	// 2. 静态对象
	private static LazyMan lman = new LazyMan();
	public static LazyMan getInstance(){
		return lman;
	}

思考:反射介入之后怎么办?
回答:应该在 private LazyMan(){} 上下功夫了。

懒汉式

public class LazyMan{
	// 1. 私有化构造函数
	private LazyMan(){}
	// 2. 静态对象
	private static LazyMan lman;
//------------------------------------------------------------------------------------------
	// 3.1 单线程之下获取实例方法
	public static LazyMan getInstance(){
		// 4. 判空
		if(lman == null)
		{
			lman = new LazyMan();
		}
		return lman;
	}
//------------------------------------------------------------------------------------------
	// 3.2 获取实例方法
	public static LazyMan getInstance_multi_thread(){
		// 4. 双重检测锁  DCL懒汉式  :  Notice 3
		if(lman == null){
			synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
				if(lman == null){// 假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
					lman = new LazyMan(); // 不是原子性的操作  : Notice 1
					//> 1. 分配内存空间
					//> 2. 执行构造方法
					//> 3. 将对象指向分配的空间
				}
			}
		}
		return lman;   // 123是一般的顺序,但是当进行指令重排之后,顺序可能是132
		// 那么就存在一种情况 Thread1:拿到锁,已经执行了 1、3步骤。
		// 这时候Thread 2 调用该方法发现 lman != null,则直接返回了。
		// 但其实Thread1只是进行了1、3步骤还没有完成2步骤。所以Thread 2拿了一个未被初始化的lman。
		// 怎么解决??? ---> 防止其指令重排
	}
//------------------------------------------------------------------------------------------
	// 3.3 获取实例方法
	// 2. 静态对象
	private volatile static  LazyMan lman; // 使用volatile进行修饰 : Notice 2
	public static LazyMan getInstance_multi_thread(){
		// 4. 双重检测锁  DCL懒汉式
		if(lman == null){ // 4.1 减少线程对同步锁的竞争
			synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
				if(lman == null){// 保证单例
					lman = new LazyMan(); // 不是原子性的操作
				}
			}
		}
		return lman;
	} // 仍然不安全,因为存在反射
//------------------------------------------------------------------------------------------
// 人家用反射咋办??  待增加
	// 3.4 获取实例方法
	// 2. 静态对象
	private volatile static  LazyMan lman; // 使用volatile进行修饰 
	public static LazyMan getInstance_multi_thread(){
		// 4. 双重检测锁  DCL懒汉式
		if(lman == null){
			synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
				if(lman == null){// 
					lman = new LazyMan(); // 不是原子性的操作
				}
			}
		}
		return lman;
	}
}

Notice 1:

Object obj = new Object(); // 不是原子性操作

  1. 分配内存空间
  2. 执行构造方法
  3. 将对象指向分配的空间
    期望的顺序是:123; 但是JVM为了高效,有的时候会进行指令重排,所以可能的顺序是 132

Notice 2:

volatile的优势:

  1. 可见性(直接使用主内存(这儿的知识点是:主内存和工作内存))
  2. 防止指令重排

Notice 3:

  1. if (lman == null) :多线程不安全
  2. if (lman == null) + synchronized/lock :多线程不安全,同一时刻可能会有很多线程通过 if (lman == null)条件。后面就排队创建对象了
  3. synchronized/lock + if (lman == null) :多线程安全,但线程之间会争夺锁资源。

例子:1000个线程竞争,第一个线程获取锁后就创建对象;其他999个线程陆陆续续进来后发现对象已经创建了(发牢骚说:这不白等了吗,单例就只需要创建一次对象,我还进来干嘛)

  1. if (lman == null) + synchronized/lock + if (lman == null) :多线程安全,第一个if (lman == null) 减少对同步锁的竞争,提高效率。

DCL_为什么是双重检测
懒汉式单例模式–解决线程安全最完整版详解

反射介入

public class LazyMan{
	// 1. 私有化构造函数
	private LazyMan(){
		// 1.2 中增加了同步检测
		synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
			if(lman != null){// 
				throw new RuntimeException("不要试图使用反射的Constructor来构造对象!");
			}
		}
	}
	// 2. 静态对象
	private static LazyMan lman;
	// 3.4 获取实例方法
	// 2. 静态对象
	private volatile static  LazyMan lman; // 使用volatile进行修饰 
	public static LazyMan getInstance_multi_thread(){
		// 4. 双重检测锁  DCL懒汉式
		if(lman == null){
			synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
				if(lman == null){// 
					lman = new LazyMan(); // 不是原子性的操作
				}
			}
		}
		return lman;
	}

	public static void main(String[] args)
	{
		LazyMan instance = LazyMan.getInstance_multi_thread(); // 先创建一个对象
		Constructor<LazyMan> declareCons = LazyMan.class.getDeclareConstructor();
		declareCons.setAccessible(true);
		// 这儿会出现异常, 由于私有构造函数 1.2 的检测代码
		LazyMan instance2 = declareCons.newInstance(); // 直接调用构造函数建立对象
	}

	public static void main2(String[] args)
	{
		// LazyMan instance = LazyMan.getInstance_multi_thread(); // 先创建一个对象
		Constructor<LazyMan> declareCons = LazyMan.class.getDeclareConstructor();
		declareCons.setAccessible(true);
		// 但是如果注释 LazyMan调用getInstance_multi_thread() 的代码,全部直接使用反射来构造;这样就可以成功
		LazyMan instance2 = declareCons.newInstance(); // 直接调用构造函数建立对象
		LazyMan instance3 = declareCons.newInstance(); // 直接调用构造函数建立对象
		// 因为如果不通过getInstance_multi_thread()方法创建对象(就是说lman还未被赋值)
		// 则,private volatile static LazyMan lman; lman对象永远是null的,而我们的检测都是查看 lman 是否为空
		// 这种问题如何解决?
	}
}

因为如果不通过getInstance_multi_thread()方法创建对象, private volatile static LazyMan lman; lman对象永远是null的,而我们的检测都是查看 lman 是否为空。
这种问题如何解决? 见下:

public class LazyMan{
	private static boolean Flag = false;// 增加标志位
	// 1. 私有化构造函数
	private LazyMan(){
		// 1.2 中增加了同步检测
		synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
			if(Flag == false)  //  增加标志位,防止别人使用反射的构造器直接创建
				Flag == true;
			else{
				if(lman != null){// 
					throw new RuntimeException("不要试图使用反射的Constructor来构造对象!");
				}
			}
		}
	}
	// 2. 静态对象
	private static LazyMan lman;
	// 3.4 获取实例方法
	// 2. 静态对象
	private volatile static  LazyMan lman; // 使用volatile进行修饰 
	public static LazyMan getInstance_multi_thread(){
		// 4. 双重检测锁  DCL懒汉式
		if(lman == null){
			synchronized(LazyMan.class){// 5. 加锁,保证进去之后没有其他线程进来
				if(lman == null){// 
					lman = new LazyMan(); // 不是原子性的操作
				}
			}
		}
		return lman;
	}

	public static void main(String[] args)
	{
		Constructor<LazyMan> declareCons = LazyMan.class.getDeclareConstructor();
		declareCons.setAccessible(true);
		// 这儿会出现成功了
		LazyMan instance2 = declareCons.newInstance(); // 直接调用构造函数建立对象
		LazyMan instance3 = declareCons.newInstance(); // 直接调用构造函数建立对象
	}
}

private static boolean Flag = false; : 增加标志位,防止通过反射直接调用构造函数建立对象

问题又来了,人家通过反射拿到 Flag 字段的信息呢?然后修改 Flag的值。。。
单例又被破解了。。。。
咋办????? 枚举类

我的另外一篇文章—关于Java枚举

在这里插入图片描述

序列化介入

cnblogs - 通过反射机制、序列化反序列化破解单例模式

静态内部类

public class Holder
{
	private Holder(){}

	public static Holder getInstance(){
		return innerClass.HOLDER;
	}

	public static class InnerClass
	{
		private static final Holder = new Holder();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值