栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

单例模式

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

单例模式

单例模式

定义:保证一个类仅有一个实例,并提供一个全局访问点

类型:创建型

使用场景:与定义无异,想在任何时候情况下都只有一个实例。当然,如果是在单机模式下肯定不用过多讨论。一般都是集群模式下,
比如一些共享的计数器,连接池,线程池等。

优点:
  • 内存开销少,只有一个实例。也可以避免对资源的多重浪费
  • 设置全局访问点,严格的控制了访问。换句话说就是没办法去进行new操作,只能调用方法获取。
缺点

-优点及缺点。严格的控制访问导致扩展性差,基本只能靠改代码进行修改。

单例模式设计的重点

  • 私有构造器
  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
  • 反射 -> 防止反射攻击

1. 懒汉模式

通过上图我们能看到执行看似没什么问题,但是仔细一看的话便能发现它是线程不安全的。因为代码比较简单所以是很难触发的。所以我们需要进行一下多线程debug。

设置之后我们分别对线程进行一下debug,手动模拟会出现问题的可能。

      最后果然出现了不同的打印结果。知道了不安全的原因,那么如何解决自然变得很简单,我们只需要在静态方法前面加synchronized关键字即可,但是静态方法加锁就相当于这个类加锁。对于性能自然会不是很高。那么有没有让锁尽量不会起作用,还能延迟加载的方法呢?
自然是有,下面讲解一下双重检查

2. 双重检查模式

public class DoubleCheckLazySingleton {
    //private static DoubleCheckLazySingleton lazySingleton = null;
    private volatile static DoubleCheckLazySingleton lazySingleton = null;


    private DoubleCheckLazySingleton() {
    }

    public synchronized static DoubleCheckLazySingleton getInstance() {

 if (lazySingleton == null) {
     synchronized (DoubleCheckLazySingleton.class) {
  if (lazySingleton == null) {
      lazySingleton = new DoubleCheckLazySingleton();
      //因为指令重排可能会有一个隐患
      //1 - 分配内存給这个对象
      //3 - lazySingleton 指向刚分配的内存地址
      //2 - 初始化对象
      //-----------------------------
      //3 - lazySingleton 指向刚分配的内存地址

  }

     }
 }
 return lazySingleton;
    }

    public static void main(String[] args) {
 //只是为了快速用而已,实际的话不建议这么创建线程池
 ExecutorService executorService = Executors.newFixedThreadPool(3);

 for (int i = 0; i < 2; i++) {
    executorService.execute(DoubleCheckLazySingleton::getInstance);
 }
 executorService.shutdown();
    }
}

      通过代码可以看到我是把没有加volatile的代码注释掉了。原因下面会进行讲解。

      通过图片我们能看到因为指令重排的原因,创建一个对象的指令可能会被重排序。如果出现上图的情况,那么就会导致程序报错。那么我们解决问题的方法无非两种:
1 禁止重排序
2 线程0的重排序,对其他线程不可见。

     其实加volatile关键字就是方法 1。volatile通过加入内存屏障和禁止重排序优化来实现可见性。这个应该是线程安全性相关的知识,因为今天主要是说单例模式,所以简单说一下:volatile写操作的时候会将本地内存的共享变量刷新到主内存,而读操作会从主内存中去读共享变量。

3. 静态内部类模式

public class StaticInnerSingleton {

    private static class InnerClass{

 private static StaticInnerSingleton staticInnerSingleton = new StaticInnerSingleton();
    }

    public static StaticInnerSingleton getInstance(){
 return InnerClass.staticInnerSingleton;
    }


    //私有构造方法
    private StaticInnerSingleton() {
    }
}

内部静态类模式就是上面2的解决方式。线程的重排序,对其他线程不可见。

(深入理解java虚拟机 p226)虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确的加锁、同步、如果多个线程去同时初始化一个类,那么只有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程的方法执行完毕。


类似于上图。只有一个线程是可以获取锁的,那么即使线程0去重排序,对于线程1也是不可见的。

4. 饿汉模式
饿汉比较简单就不详细说了。优点线程安全。缺点不是延迟加载,如果不用,会造成一定的开销。

public class HungrySingleton {
    private final static HungrySingleton hungrySIngleton = new HungrySingleton();

    public HungrySingleton() {
    }

    public HungrySingleton getHungrySIngleton() {
 return hungrySIngleton;
    }
}

序列化以及反射对单例模式的影响
     因为后续会讲解枚举类型的单例,因为天然特性的原因。所以这里先讲一下序列化以及反射对单例模式的影响

序列化
    private final static HungrySingleton hungrySIngleton = new HungrySingleton();

    public HungrySingleton() {
    }

   public static HungrySingleton getHungrySIngleton() {
 return hungrySIngleton;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {

 HungrySingleton hungrySingleton = HungrySingleton.getHungrySIngleton();
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
 oos.writeObject(hungrySingleton);

 File file = new File("singleton");
 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));

 HungrySingleton hungrySingletonTwo = (HungrySingleton) objectInputStream.readObject();

 System.out.println(hungrySingleton);
 System.out.println(hungrySingletonTwo);
 System.out.println(hungrySingleton.equals(hungrySingletonTwo));

    }
}

//打印结果
com.example.demo.singleton.HungrySingleton@1fb3ebeb
com.example.demo.singleton.HungrySingleton@5010be6
false

我们可以看出来这就违背了我们单例模式的初衷,因为我的得到了不一样的对象。解决方案也很简单我们只需要加一个方法就可以搞定了。

     我们可以看到readResolve并不是灰色的,因为我的主题如果这个方法没有被调用的时候显示的是灰色的。那么为什么加一个readResolve就可以了呢,他有事在哪调用的?那接下来我们得看源码才能知道了。

 HungrySingleton hungrySingletonTwo = (HungrySingleton) objectInputStream.readObject();

因为源码太多,我就不贴太多图了,主要的地方我再贴图。
点击readObject这个方法 -> 进去可以看到readObject0()这个方法->进去之后发现里面有一个switch 找到TC_OBJECT然后我们进入readOrdinaryObjectreadOrdinaryObject这个方法-> 因为源码太多,我就不贴太多图了,主要的地方我在贴图。
点击readObject这个方法 -> 进去可以看到readObject0()这个方法->进去之后发现里面有一个switch 找到TC_OBJECT然后我们进入readOrdinaryObject这个方法。在里面找到

obj = desc.isInstantiable() ? desc.newInstance() : null;

通过这行代码,我们看到了obj,然后看了一下obj最后会返回,那么就说明obj没啥可看的,我们的重点在于这个判断。点击进入isInstantiable方法

  
    boolean isInstantiable() {
 requireInitialized();
 return (cons != null);
    }
    cons是一个构造器点进去没有什么有效信息,那么只能看上方注解了。如果
    serializable/externalizable在运行的时候被实例化就会返回true。

可以看到返回true之后 desc.newInstance()通过反射拿到一个新的对象肯定会和原来不一样。虽然现在知道了我们会新获得一个对象,但是还没有解决我们最初的疑问,所以我们接着往后看。

 if (obj != null &&
     handles.lookupException(passHandle) == null &&
     desc.hasReadResolveMethod())
 {
     Object rep = desc.invokeReadResolve(obj);
     if (unshared && rep.getClass().isArray()) {
  rep = cloneArray(rep);
     }
     if (rep != obj) {
  // Filter the replacement object
  if (rep != null) {
      if (rep.getClass().isArray()) {
   filterCheck(rep.getClass(), Array.getLength(rep));
      } else {
   filterCheck(rep.getClass(), -1);
      }
  }
  handles.setObject(passHandle, obj = rep);
     }
 }

     通过上面代码我们看到if里面有一个hasReadResolveMethod()方法,看名字我们也猜出来这到底是干啥的。进入if里面之后看到 Object rep = desc.invokeReadResolve(obj);点击进入发现里面是通过反射拿到我们类里面声明的readResolve方法,至此我们也知道了readResolve是在哪被调用了。但是这还有个不好的地方就是每次都会有新的对象被生成,只不过后期调用readResolve方法被替换了而已。

反射攻击
public class HungrySingleton {
    private final static HungrySingleton hungrySIngleton = new HungrySingleton();

    public HungrySingleton() {
    }

    public static HungrySingleton getHungrySIngleton() {
 return hungrySIngleton;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

 Class jhwclass = HungrySingleton.class;
 Constructor constructor = jhwclass.getDeclaredConstructor();
 //放开私有权限
 constructor.setAccessible(true);
 HungrySingleton hungrySingleton = HungrySingleton.getHungrySIngleton();
 HungrySingleton hungrySingletonTwo = (HungrySingleton) constructor.newInstance();


 System.out.println(hungrySingleton);
 System.out.println(hungrySingletonTwo);
 System.out.println(hungrySingleton.equals(hungrySingletonTwo));


    }
}
打印结果
com.example.demo.singleton.HungrySingleton@13221655
com.example.demo.singleton.HungrySingleton@2f2c9b19
false

在构造方法加上防御代码

静态内部类也可以用上面的方法。原因是两者都是在类加载的时候,实例就会生成。而懒汉加载就不能用了,因为无法确定哪个线程去进行加载,即使加了以一些防御性质的代码也不能保证,例如声明一个变量去当开关,还是可能会被反射进行更改。可以参考一下下面的代码。

public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private static boolean flag = true;

    private LazySingleton() {
 if (flag) {
     flag = false;
 } else {
     throw new RuntimeException("报错了,不能反射");
 }
    }

    public synchronized static LazySingleton getInstance() {

 if (lazySingleton == null) {
     lazySingleton = new LazySingleton();
 }
 System.out.println(Thread.currentThread().getName() + "--lazySingleton:" + lazySingleton);
 return lazySingleton;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
 Class jhwclass = LazySingleton.class;
 Constructor a = jhwclass.getDeclaredConstructor();
 a.setAccessible(true);
 LazySingleton lazySingleton = LazySingleton.getInstance();

 Field aa = lazySingleton.getClass().getDeclaredField("flag");
 aa.setAccessible(true);
 aa.set(lazySingleton, true);
 LazySingleton lazySingletonTwo = (LazySingleton) a.newInstance();
    }
}

5. 枚举模式

public enum EnumInstance {

    one;
    private String data;

    public String getData() {
 return data;
    }

    public void setData(String data) {
 this.data = data;
    }

    public static EnumInstance getInstance() {
 return one;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
 EnumInstance enumInstance = EnumInstance.getInstance();
 enumInstance.setData("jhw");

 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
 oos.writeObject(enumInstance);

 File file = new File("singleton");
 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));

 EnumInstance enumInstanceTwo = (EnumInstance) objectInputStream.readObject();

 Class jhwclass = EnumInstance.class;
 //Constructor aa = jhwclass.getDeclaredConstructor();
 Constructor aaa = jhwclass.getDeclaredConstructor(String.class, int.class);

 //aa.setAccessible(true);
 aaa.setAccessible(true);

 // EnumInstance enumInstanceTrd = (EnumInstance) aa.newInstance();
 EnumInstance enumInstanceTrd = (EnumInstance) aaa.newInstance("jj", 1);

 System.out.println(enumInstance.getData());
 System.out.println(enumInstanceTwo.getData());
 System.out.println(enumInstance.getData().equals(enumInstanceTwo.getData()));

    }
}

//打印结果 
jhw
jhw
true

//反射错误1
Exception in thread "main" java.lang.NoSuchMethodException: com.example.demo.singleton.EnumInstance.()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.example.demo.singleton.EnumInstance.main(EnumInstance.java:48)

//反射错误2
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.example.demo.singleton.EnumInstance.main(EnumInstance.java:55)

     通过上面的代码我们可以看到,通过反射拿到的也是同一个对象。源码跟上面一样,readEnum方法中->readStrirng方法。因为枚举类里面的名字是唯一的,那么拿到的常量肯定也是唯一的。
     而Enum这个类也并没有无参的构造方法并且枚举类还不允许进行反射调用,上面的两个错误打印就是很好的说明。通过一些反编译的工具我们看一下enum,其中内部一些声明比如final,静态块是其优雅实现单例模式的基石。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/238530.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号