单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
实现方法:
- 懒汉式
- 饿汉式
- 静态内部类
饿汉式
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();
// 不是原子性操作
- 分配内存空间
- 执行构造方法
- 将对象指向分配的空间
期望的顺序是:123; 但是JVM为了高效,有的时候会进行指令重排,所以可能的顺序是 132
Notice 2:
volatile的优势:
- 可见性(直接使用主内存(这儿的知识点是:主内存和工作内存))
- 防止指令重排
Notice 3:
if (lman == null)
:多线程不安全if (lman == null) + synchronized/lock
:多线程不安全,同一时刻可能会有很多线程通过if (lman == null)
条件。后面就排队创建对象了synchronized/lock + if (lman == null)
:多线程安全,但线程之间会争夺锁资源。
例子:1000个线程竞争,第一个线程获取锁后就创建对象;其他999个线程陆陆续续进来后发现对象已经创建了(发牢骚说:这不白等了吗,单例就只需要创建一次对象,我还进来干嘛)
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的值。。。
单例又被破解了。。。。
咋办????? 枚举类 :
序列化介入
cnblogs - 通过反射机制、序列化反序列化破解单例模式
静态内部类
public class Holder
{
private Holder(){}
public static Holder getInstance(){
return innerClass.HOLDER;
}
public static class InnerClass
{
private static final Holder = new Holder();
}
}