单例模式即只有一个实例
单例模式分为饿汉式与懒汉式,饿汉式为程序一启动就将实例创建好,而懒汉式为有需要的时候才创建实例
饿汉式:会提前创建好对象,但是会占用较多内存,如果这样的对象较多的时候会浪费内存,以空间换时间
public static class HungryMan {
// 私有化无参构造器,使得外部不能通过new实例化对象
private HungryMan(){}
// 程序创建时就实例化对象
private static HungryMan hungryMan = new HungryMan();
// 通过对外方法返回对象
public static HungryMan getInstance(){
return hungryMan;
}
}
懒汉式:需要使用的时候才会创建对象,不会浪费内存,但每次都会判断,是以时间换空间
public class LazyMan {
private LazyMan(){}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (null == lazyMan){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
DCL懒汉式的出现是因为传统的懒汉式在多线程下并不安全,接下来,我们层层递进,从传统懒汉式优化至DCL懒汉式
改进1:在多线程下,判断lazyMan是否为空的时候,可能会两个线程判断的都空,所以会导致创建两次,能过加synchronized锁改进
if (null == testService){
synchronized (TestService.class){
if (null == testService) {
testService = new TestService();
}
}
}
改进2:通过new关键字创建实例对象的个动作并不是原子性的,它可能存在以下问题
if (null == testService){
synchronized (TestService.class){
if (null == testService) {
testService = new TestService(); // 不是原子性操作
// 1. 分配内存空间
// 2. 初始化对象
// 3. 将对象指向空间
// 由于指令重排,可能线程A执行的是123,线程B执行的是132
// 此时线程B会认为testService != null,但其实它的空间是没有的
}
}
}
如果想要改进的话,需要在对象前加上volatile,禁止指令重排
关于volatile的特性,可以浏览博文的另一篇贴子:JUC-volatile关键字
private static volatile TestService testService;
至此,通过双重检测,基本可以保证懒汉式在多线程下正常运行,但是,这样的懒汉式仍然不够安全
改进3:我们可以通过反射获取到无参构造器并通过无参构造器创建对象
Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
LazyMan lazyMan1 = declaredConstructors.newInstance();
LazyMan instance = LazyMan.getInstance();
System.out.println(instance == lazyMan1);
// 结果为false
这个时候改进很简单,只需要在无参构造器中加一个判断即可
private LazyMan(){
synchronized (LazyMan.class) {
if (null != lazyMan) {
throw new RuntimeException("非法构造");
}
}
}
改进4:当所有对象都是通过反射创建的,能通过getInstance()方法时无参构造中的判断会失效
Constructor<LazyMan> declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
LazyMan lazyMan1 = declaredConstructors.newInstance();
LazyMan instance = declaredConstructors.newInstance();
System.out.println(instance == lazyMan1);
此时可以加一个标志位,判断是否创建过对象
private static boolean flag = false;
private LazyMan(){
synchronized (LazyMan.class) {
if (!flag){
flag = true;
}else {
throw new RuntimeException("非法构造");
}
}
}
改进5:这个时候,如果将在创建对象前将标志位的值也进行变更,则又可以通过反向破坏单例模式
Constructor declaredConstructors = LazyMan.class.getDeclaredConstructor(null);
declaredConstructors.setAccessible(true);
Field flag1 = LazyMan.class.getDeclaredField("flag");
flag1.setAccessible(true);
LazyMan lazyMan1 = declaredConstructors.newInstance();
flag1.set(lazyMan1,false);
LazyMan instance = declaredConstructors.newInstance();
System.out.println(instance == lazyMan1);
最终解决方案:使用枚举,枚举的本质也是一个类,只不过继承了Enum类,它是自带单例模式的,而且在反射的源码中会检测类的类型,如果是枚举类型会抛出IllegalArgumentException异常
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
欢迎前往博客主页查看更多内容
如果觉得不错,期待您的点赞、收藏、评论、关注
如有错误欢迎指正!