你知道单例模式吗?
- 饿汉,一开始就创建好。安全但是占空间。
- 懒汉,为了保证线程安全,可以加粗锁。然后进阶就是volatile+双检测锁。
-
- 双检测锁是为了后续获取锁的线程不再重复创建对象。
- volatile禁止什么指令重排?禁止的是new对象的时候,对象创建过程的指令重排(对象创建过程不是原子的)。
- 说了这两种之后,可以说基于java语言的特性,有 静态内部类的方式,顺便说静态内部类的特性(静态内部类只有使用的时候加载类,基于此特性JVM就可以线程安全地创建单例对象,因此不用加锁了)
- 上面所有的单例模式,都可以通过反射 破坏私有构造,从而创建新对象 破坏单例模式,但用枚举类不会被破坏,因为反射的Constructor.newInstance()源码中规定了 不能通过反射创建枚举对象。
单例模式
1、懒汉单例(线程安全)
懒加载,线程安全但getInstance方法加了锁,效率低
public class Singleton_01 {
//静态变量,因为要在静态方法中调用
private static Singleton_01 instance;
// 私有化,不允许外部调用
// 构造方法属于类 级别
private Singleton_01(){
}
//静态方法
//当一个方法被声明为 synchronized 时,它被称为同步方法。同一时间只能有一个线程进入该方法,其他线程必须等待。该方法的锁是当前对象的实例
public static synchronized Singleton_01 getInstance(){
//如果不为空则直接返回
if (null != instance) return instance;
//否则创建实例返回
instance = new Singleton_01();
return instance;
}
}
这样对获取单例实例方法加锁 能保证线程安全,但 效率低,原因是 无论单例对象是否创建,后续所有的获取单例的多线程方法都必须串行执行(理论上当第一次单例对象创建完成后,后续getInstance读方法允许多线程共享访问无需加锁),这样必然导致效率降低。
2、懒汉单例(线程安全)双检查锁和volatile
- 为了避免上述 保证线程安全时效率低 的问题,采用在方法内部加类锁 synchronized (Singleton_02.class) 而不是 对整个方法加锁
- 而在内部加类锁后,必须双重检测(在锁里面再次用 if 判断 instance 是否),
-
- 锁里面为什么还有 if 双重检测?
- 假如第一个创建单例的线程竞争到了锁,并在锁里面创建了单例对象,这个线程释放锁后第二个线程会进入获取锁,这时需要再次判断,因为这时已经有了单例对象将不会进入if语句内重复创建对象
- 加了粒度更细的锁后,私有的单例变量instance 用volatile 修饰,防止指令重排。为什么?
这里涉及JVM的对象创建的几个步骤(面试八股), 首先 instance = new Singleton_02(); 本身不是原子操作,给引用创建并分配对象 这一步实际如下分为 3 步:
在单线程环境下,这三步是允许指令重排且不会出现问题的,例如1、3、2:分配一个空间,instance指向这个分配的null空间,给分配的空间内初始化对象。
但是在多线程环境下,指令重排可能出现问题。例如,此时线程a执行new 操作1、3、2,执行3后就已经给引用变量指向了内存空间,但空间中未初始化对象(还未执行2) ;这时线程b进来 在锁外面判断 if (instance == null),instance不为null,然后返回了一个还未初始化完全的对象引用。
//TODO 必须加volatile,防止指令重排(狂神JUC课程)
private volatile static Singleton_02 instance;
// 私有化,不允许外部调用
// 构造方法属于类 级别
private Singleton_02(){
synchronized (Singleton_02.class){
if (instance != null){
//抛出异常
throw new RuntimeException("不要试图通过反射破坏单例异常");
}
}
}
//TODO 双重检测锁模式
public static Singleton_02 getInstance(){
if (instance == null){
//加锁
synchronized (Singleton_02.class){
//TODO 锁里面的if有什么用,非常有用
// 假如第一个创建单例的线程竞争到了锁,并在锁里面创建了单例对象,这个线程释放锁后其他线程会进入获取锁,这时需要再次判断,因为这时已经有了单例对象将不会进入if语句内重写创建对象
if (instance == null){
instance = new Singleton_02(); //不是原子操作
/**
* 1分配内存空间
* 2执行构造方法
* 3把对象指向内存空间
*/
}
}
}
return instance;
}
3、懒汉单例(线程不安全)
//线程不安全
//instance == null时,多个线程同时调用getInstance可能会创建多个实例
public class Singleton_01 {
//静态变量,因为要在静态方法中调用
private static Singleton_01 instance;
// 私有化,不允许外部调用
// 构造方法属于类 级别
private Singleton_01(){
}
//静态方法
public static Singleton_01 getInstance(){
//如果不为空则直接返回
if (null != instance) return instance;
//否则创建实例返回
instance = new Singleton_01();
return instance;
}
}
4、饿汉单例
线程安全,但不是懒加载,资源浪费
Java 的类加载机制保证了静态成员instance的初始化只会发生一次
public class Singleton_02 {
private static Singleton_02 instance = new Singleton_02();
public static Singleton_02 getInstance(){
return instance;
}
private Singleton_02(){
}
}
5、使用类的静态内部类(线程安全),推荐,也是懒加载
通过定义一个私有的静态内部类 SingletonHolder,内部类持有一个静态的 Singleton_04 实例 instance
通过静态内部类的方式,可以延迟加载单例对象,并且在多线程环境下保证了线程安全。当 Singleton_04 类被加载时,静态内部类不会被初始化。只有当调用 getInstance() 方法时,才会触发静态内部类的初始化,并创建唯一的 Singleton_04 实例
public class Singleton_04 {
private static class SingletonHolder {
private static Singleton_04 instance = new Singleton_04();
}
private Singleton_04() {
// 私有的构造函数,防止外部直接实例化该类
}
public static Singleton_04 getInstance() {
return SingletonHolder.instance;
}
}
懒加载:当 Singleton_04 类被加载时,静态内部类不会被初始化,只有当调用 getInstance() 方法时,才会触发静态内部类的初始化
线程安全:静态内部类的初始化是线程安全的(JVM保证类的构造方法线程安全),Java 的类加载机制保证了静态成员的初始化只会发生一次
高效的单例对象: 因为getInstance()方法没有加锁,所以实例创建后getInstance()单例对象的获取是高效多线程的。
为什么要用静态类,因为静态类不能被实例化且静态类及其中的成员只有一份。
问题
为什么静态内部类在类加载时不会初始化
在 Java 中,当一个类被加载时,它的静态成员(包括静态变量和静态代码块)会被初始化。然而,静态内部类并不在外部类加载的过程中被立即初始化。
静态内部类的初始化是在首次使用该内部类的时候才会触发,而不是在外部类加载时。这是由于 Java 的类加载机制。当外部类被加载时,静态内部类并不会被加载和初始化,只有在使用内部类的时候才会加载和初始化内部类。
静态内部类在加载和初始化过程中是线程安全的。Java 的类加载机制保证了静态成员的初始化只会发生一次,避免了多线程环境下的竞争条件。
6、枚举类实现单例
上面所有的单例模式,都可以通过反射 破坏私有构造,从而创建新对象 破坏单例模式。
//反射破坏单例模式
public static void main(String[] args) throws Exception{
Singleton_02 instance = Singleton_02.getInstance();
//反射获取空参构造器
Constructor<Singleton_02> declaredConstructor = Singleton_02.class.getDeclaredConstructor(null);
//设置方法的可访问性
declaredConstructor.setAccessible(true);
Singleton_02 instance2 = declaredConstructor.newInstance();
System.out.println(instance==instance2);
}
但用枚举类不会被破坏,因为反射的Constructor.newInstance()源码中规定了 不能通过反射创建枚举对象。
//枚举本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}