设计模式——单例模式

单例模式

实际开发中,使用 Spring 的 Bean 工厂保证单例。

饿汉式

类加载到内存,就实例化一个单例,[JVM 保证线程安全](因为一个类 class 只在内存中加载一次)

优点:简单实用(推荐)

缺点:不管有没有用到,[类加载](就算是 Class.forName( ) 只将类加载到内存,静态变量也会一同初始化。)时就已经实例化(但是不用的话为什么要加载。。)

public class User {

    private static final User user = new User();

    // 私有化构造方法(别人不能 new)
    private User() {

    }

    // 对外暴露一个获取 User 对象的静态方法
    public static User getInstance() {
        return user;
    }

}

当然了,也可以用静态代码块的方式来初始化:

private static final User user;
static {
    user = new User();
}

懒汉式

只有真正使用时,才进行实例化。

优点:达到了按需初始化的目的

缺点:带来了线程安全问题

public class User {

    private static final User user;
    
    private User() {

    }
    
    public static User getInstance() {
        if (user == null) {	// 当多个线程同时到达,因为判断时延,会产生多次初始化。
            user = new User();
        }
        return user;
    }

}

解决线程安全问题:

  • synchronized 方法锁:将实例化方法上锁,每次只给一个线程调用。

    缺点:不管对象存不存在,都需要进行:判断、检查锁状态、获得锁、释放锁,浪费性能。

    public static synchronized User getInstance() {
        if (user == null) {
            user = new User();
        }
        return user;
    }
    
  • synchronized 对象锁:目的是为了可以在上锁之前进行判断([双判断](双重检查锁定(Double-Checked Locking))),限流。

    public static User getInstance() {
        if (user == null) {
            synchronized (User.class) {
                if (user == null) {
                    user = new User();
                }
            }
        }
        return user;
    }
    

引入 volatile 关键字

new 一个对象有几个步骤:

  1. 看 class 对象是否加载,如果没有就先加载 class 对象
  2. 分配内存空间,
  3. 调用构造函数初始化实例
  4. 将实例对象指向刚分配的内存地址

而 CPU 为了优化程序,可能会进行指令重排序:颠倒步骤 3、4,导致实例内存还没分配,就被使用了。

加上 volatile 关键字,就保证new 不会被指令重排序。(被 volatile 关键字修饰的变量是被禁止重排序的)

为静态变量加上 volatile 关键字:

private static final volatile User user;

参考:单例模式中的volatile关键字

静态内部类式

加载外部类时,不会加载内部类。只有在调用 getInsance() 时,才会加载内部类 UserManager。

相比饿汉式,静态内部类给静态变量贴了一层保护,利用资深特性,实现了懒加载。(比较推荐)

public class User {

    private User() {

    }

    private static class UserManager {
        private static User user = new User();
    }

    public static User getInstance() {
        return UserManager.user;
    }

}

枚举单例

对于以上方案,都存在一个问题:就算构造方法私有化了,依然可以通过反射得到实例对象。(还要防 25 仔?)

所以,Java 创始人之一 Joshua Bloch 提出了 “最完美” 的方式:[单元素枚举](“单元素的枚举类型已经成为实现 Singleton 的最佳方法”)。

枚举天生单例,它在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。

普通类通过反射实现反序列化,**枚举类的反序列化是通过 java.lang.Enum#valueOf() 来根据名字查找枚举对象。**所以,枚举不会发生由于反序列化导致的单例破坏问题。

优点:不仅可以解决线程同步,还可以防止反序列化

缺点:豆芽杀手

public enum User {

    INSTANCE;	// User 的一个实例

    public void eat() {
        System.out.println("吃");
    }
    
}
public static void main(String[] args) {
    User.INSTANCE.eat();
}

还看见另一种写法:

public class User {

    private User() {

    }

    // 枚举只有一个,所以其属性 user 也只有一个。
    enum UserManager {
        INSTANCE;
        private User user;

        private UserManager() {
            this.user = new User();
        }

        public User getUser() {
            return user;
        }
    }

    public static User getInstance() {
        return UserManager.INSTANCE.getUser();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值