是什么?
在Java中,单例模式是一种常用的设计模式,用于确保一个类只有一个实例,并提供一个全局访问点。然而,当多个线程同时访问单例对象时,可能会引发线程安全性的问题。
饿汉单例模式实现
类被加载的时候就立即初始化并创建唯一实例
public class StarvingSingleton {
private static final StarvingSingleton starvingSingleton = new StarvingSingleton();
private StarvingSingleton(){ }
public static StarvingSingleton getInstance(){
return starvingSingleton;
}
}
成员变量设置为static确保唯一性,设置为private防止以StarvingSingleton.starvingSingleton的方式获取实例,设置为final确保成员变量一经初始化就无法被改变。构造方法私有,客户端就无法用new来创建对象
随后提供一个唯一可以访问成员变量的方法,使客户端可以获取早已初始化的实例
这样我们在其它类中只能通过调用StarvingSingleton.getInstance()方法来获取实例,StarvingSingleton.starvingSingleton和构造函数来new对象的方法已经被堵死了。
public class SingletonDemo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(StarvingSingleton.getInstance());
System.out.println(StarvingSingleton.getInstance());
}
}
输出结果表明,两次调用StarvingSingleton.getInstance()方法获取的实例是同一个,也是唯一的实例
懒汉单例模式实现
被客户端首次调用的时候才创建唯一实例
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//第一次检测
if (instance==null){
//同步
synchronized (LazyDoubleCheckSingleton.class){
if (instance == null){
//memory = allocate(); //1.分配对象内存空间
//instance(memory); //2.初始化对象
//instance = memory; //3.设置instance指向刚分配的内存地址,此时instance!=null
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
volatile 关键字,用于声明变量。当一个变量被 volatile 修饰时,表明这个变量是易变的,在多线程环境下,每次访问该变量都会直接从内存中读取,而不会使用线程的本地缓存。volatile 的主要作用是确保变量的可见性和禁止指令重排序。
可见性:当一个变量被声明为 volatile 时,对这个变量的修改会立即被其他线程所感知,即使是在多个线程之间的操作也能保证可见性。
禁止指令重排序:volatile 关键字还可以禁止指令重排序,这意味着在 volatile 变量的赋值操作后面会有一个内存屏障,这可以防止代码的执行顺序被重新排序。
volatile 不能保证原子性。如果一个操作涉及到了多个变量的读取和写入,并且需要保证原子性,那么就需要使用 synchronized 或者 Lock 等同步机制。
在多线程编程中,当多个线程共享一个变量时,如果其中一个线程修改了这个变量,其他线程可能无法立即感知到这个变量的变化,这就可能导致数据不一致的问题。使用 volatile 关键字可以解决这个问题,确保变量的可见性,从而避免了这类问题的发生。
同样的懒汉的类构造函数设置为私有的,防止外部调用构造函数创建实例,定义LazyDoubleCheckSingleton类型的成员变量作为单例返回给客户端,但是该单例在类加载的时候并不初始化,而是第一次调用才初始化,同时提供了唯一获取单例的方法getInstance()
两次if判断确保线程安全,synchronized给该类上一个同步锁,一个线程通过第一个if检查后上锁,其他线程只能等待锁的释放。
因为初始对象,分配空间这两步会被程序执行器任意分配执行顺序,导致只分配了空间没初始化就因为被其他线程访问,跳过上锁的内容返回了没有初始化的instance。所以使用volatile修饰,保证顺序执行。
然而,private可以被反射突破,如饿汉模式通过反射
public class SingletonDemo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(StarvingSingleton.getInstance());
Class clazz = StarvingSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(StarvingSingleton.getInstance());
}
}
这里,两次输出的实例并不是同一个,懒汉模式亦是如此
public class SingletonDemo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(LazyDoubleCheckSingleton.getInstance());
Class clazz = LazyDoubleCheckSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(LazyDoubleCheckSingleton.getInstance());
}
}
想要不被反射突破,改写饿汉模式,使用枚举类型
public class EnumStarvingSingleton {
private EnumStarvingSingleton(){}
public static EnumStarvingSingleton getInstance(){
return ContainerHolder.HOLDER.instance;
}
private enum ContainerHolder{
HOLDER;
private EnumStarvingSingleton instance;
ContainerHolder(){
instance = new EnumStarvingSingleton();
}
}
}
这样就不会被反射攻破
public class SingletonDemo {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(EnumStarvingSingleton.getInstance());
Class clazz = EnumStarvingSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
EnumStarvingSingleton enumStarvingSingleton = (EnumStarvingSingleton)constructor.newInstance();
System.out.println(enumStarvingSingleton.getInstance());
}
}