可能是最全的Java单例模式讨论

单例模式
最简单但是也挺困难的。

要保证在一个JVM中只能存在一个实例,要考虑到如下的情况:

  • Java能够使用那些方式构建对象
  • Java在创建对象时多线程并发情况下是否仍然只能创建一个实例

Java创建对象的方法:

  • new 最常用的,直接使用构造器创建。 每new一次都会产生新的实例。所以单例中应该只new一次,当再想用对象时都返回该对象的值
  • Class.newInstance() 该方法会调用public 的无参构造器。
    为了防止这个方式创建,只要把构造器设置为private的就可以了。这是如果再用这个方法创建会报错.同时私有构造器也可以解决四处new的问题。
  • 反射
    Constructor ctt = c.getDeclaredConstructor();
    ctt.setAccessible(true);
    T t1 = ctt.newInstance();
    这样私有构造器也不行了。解决的办法是使用抽象类,这样就会抛出异常了,不能创建了。或者在构造器中加入判断如果是第二次构建就抛出异常。
  • clone
    这个主要由clone()方法的具体行为决定的。如果没有实现Cloneable接口是不用管这个问题的。
  • 反序列化
    反序列化的时候也会打破单例,解决的方式是写一个readResolve。这个方法的规则是在反序列化的时候勇气返回值来代替反序列化的返回值
    还有一个更简单的办法是不要实现Serializable接口,这样序列化的时候就会报错了

先写个验证工具,来验证这个类是否是单例的

public class SingletonTester {
    public static <T> void checkClassNewInstance(Class<T> c){

        try {
            T t1 = c.newInstance();
            T t2 = c.newInstance();
            if(t1 != t2){
                System.out.println("Class.newInstance校验失败,可以创建两个实例");
            }else{
                System.out.println("Class.newInstance校验通过");
            }
        } catch (Exception e) {
            System.out.println("不能用Class.newInstance创建,因此Class.newInstance校验通过");
        }
    }

    public static <T> void checkContructorInstance(Class<T> c){
        try {
            Constructor<T> ctt = c.getDeclaredConstructor();
            ctt.setAccessible(true);
            T t1 = ctt.newInstance();
            T t2 = ctt.newInstance();
            if(t1 != t2){
                System.out.println("ContructorInstance校验失败,可以创建两个实例");
            }else{
                System.out.println("ContructorInstance校验通过");
            }
        } catch (Exception e) {
            System.out.println("不能用反射方式创建,因此ContructorInstance校验通过");
        }
    }

    public static <T> void testSerializable(T t1){
        File objectF = new File("/object");
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new FileOutputStream(objectF));
            out.writeObject(t1);
            out.flush();
            out.close();
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(objectF));
            T t2 = (T) in.readObject();
            in.close();

            if(t1 != t2){
                System.out.println("Serializable校验失败,可以创建两个实例");
            }else{
                System.out.println("Serializable校验通过");
            }
        } catch (Exception e) {
            System.out.println("不能用反序列化方式创建,因此Serializable校验通过");
        } 

    }

    public static void main(String[] args) {
        checkClassNewInstance(Singleton3.class);
        checkContructorInstance(Singleton3.class);
        testSerializable(Singleton3.getInstance());

    }
}

这个工具验证了Class.newInstance攻击,反射攻击,反序列化攻击,能够屏蔽着三种攻击的才是好的单例。

单例1

public class Singleton1{
    private Singleton1() {
    }

    private static Singleton1 instance;

    public static Singleton1 getInstance(){
        if(instance == null){
            instance = new Singleton1();
        }
        return instance;
    }
}

最普通懒汉模式的单例, 私有构造器,静态方法获取实例,获取的时候先判空。
测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
ContructorInstance校验失败,可以创建两个实例
不能用反序列化方式创建,因此Serializable校验通过

这个类因为不能被序列化,因此不会受到反序列化攻击
因为私有构造器避免了Class.newInstance
但是会被反射攻击
另外其不是线程安全的

单例2

public class Singleton2 {
    private static Singleton2 sington = new Singleton2();

    private Singleton2(){};

    public static Singleton2 getInstance(){
        return sington;
    }
}

来个典型的饿汉模式的
测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
ContructorInstance校验失败,可以创建两个实例
不能用反序列化方式创建,因此Serializable校验通过

同样不会有反序列化及Class.newInstance的问题。
并且没有并发的问题。
不过其会在不同的时候也初始化一个实例出来。个人感觉实际上影响不大

单例3

上面的都会有反射攻击的问题。来解决它。

public class Singleton3 {
    private static Singleton3 sington = new Singleton3();
    private static int COUNT = 0;
    private Singleton3(){
        if(++COUNT > 1){
            throw new RuntimeException("can not be construt more than once");
        }
    };

    public static Singleton3 getInstance(){
        return sington;
    }
}

测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
不能用反序列化方式创建,因此Serializable校验通过

通过加入计数器来解决,这样虽然解决了反射攻击,但是却不是线程安全的,另外引入了新的变量也不优雅。下面换个方式:

单例4

public abstract class Singleton4 {
    private static class SingletonHolder{
        private static final Singleton4 INSTANCE = new Singleton4() {
        };
    }

    private Singleton4(){};

    public static Singleton4 getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

这个推荐使用

  • 用抽象类解决了反射攻击
  • 用类加载的线程安全性解决了并发
  • 用内部类实现了lazyloader的目的
  • 没有实现clone
  • 没有实现Serializable接口不会有反序列化的问题

单例5

下面说下不用内部类的懒汉模式

public class Singleton5 {
    private static Singleton5 sington = null;

    private Singleton5(){};

    public static Singleton5 getInstance(){
        if(sington == null){    // 1
            synchronized (Singleton5.class) {
                if(sington == null){ // 2
                    sington = new Singleton5();
                }
            }
        }
        return sington;
    }
}

如果没有 //1 的检查,那么所有的getInstance()都会进入锁争夺,会影响性能,因此加入了检查。
此外其会被反射攻击

单例6

上面的会有线程安全问题,是由于JVM的重排序机制引起的:
重排序:
JVM在编译的时候会保证单线程模式下的结果是正确的,但是其中代码的顺序可能会进行重排序,或者乱序,主要是为了更好的利用多cpu资源(乱序), 以及更好的利用寄存器,。
比如1 a = 1; b = 2; a=3;三个语句,如果b执行的时候可能会占用a的寄存器位置,JVM可能会把a=3语句提到b=2前面,减少寄存器置换次数。
比如上面的 instance = new Singleton5()这部分代码的伪字节码为:
1. memory = allocate() // 分配内存
2. init(memory) // 初始化对象
3. instance = memory // 实例指向刚才初始化的内存地址。
4. 第一次访问instance
在JVM的时候有可能2.3的位置进行了重新排序,因为JVM只保证构造器执行完之后的结果是正确的,但是执行顺序可能会有变化。 这个时候并发调用getInstance的时候就有可能出现如下的情况:

时间 线程A 线程B
t1 A1:分配对象的内存空间
t2 A3:设置instance指向内存空间
t3 B://1 处判断instance是否为空
t4 B:由于instance不为null,线程B将返回instance引用的对象
t5 B:instance没有经过初始化,可能会有未知问题
t6 A2:初始化对象
t7 A:这是对象才是被初始化的

为了解决这个问题,我们可以从两个方向考虑:制止重排序,或者使重排序对其他线程不可见。

制止重排序的方式单例
使用JDK1.5之后提供的volatile关键字。这个关键字的意义在于保证变量的可见性。保证变量的改变肯定会回写主内存,并且关闭java -server模式下的一些优化,比如重排序:

public abstract class Singleton6 {
    private static volatile Singleton6 sington = null;

    private Singleton6(){};

    public static Singleton6 getInstance(){
        if(sington == null){    // 1
            synchronized (Singleton6.class) {
                if(sington == null){ // 2
                    sington =  new Singleton6(){};;
                }
            }
        }
        return sington;
    }
}

还可以,但是代码有些长,不如Singleton4

单例7

使重排序对其他线程不可见的单例

public abstract class Singleton7 {
    private static Singleton7 sington = null;

    private Singleton7(){};

    public static Singleton7 getInstance(){
        if(sington == null){    // 1
            synchronized (Singleton7.class) {
                if(sington == null){ // 2
                    Singleton7 temp = new Singleton7(){};
                    sington = temp;
                }
            }
        }
        return sington;
    }
}

另外单例4页是这样的,重排序对其他的线程是不可见的

单例8

如果有必要序列化,那么就需要实现Serializable接口,下面说下这种情况如何解决反序列化攻击的问题

public abstract class Singleton8 implements Serializable{
    private static class SingletonHolder{
        private static final Singleton8 INSTANCE = new Singleton8() {
        };
    }

    private Singleton8(){};

    public static Singleton8 getInstance(){
        return SingletonHolder.INSTANCE;
    }

    public Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
Serializable校验通过

这个主要在于方法readResolve, 其返回结果会用来代替反序列化的结果

单例9

枚举单例,effectiveJava中推荐的
最后一个了。就是使用枚举单例了。可以看一下,是极好用的

public enum SingleEnum {
    INSTANCE;
}

测试结果:

不能用Class.newInstance创建,因此Class.newInstance校验通过
不能用反射方式创建,因此ContructorInstance校验通过
Serializable校验通过

它也成功的避免了各种可能存在的问题:

  • 用抽象类解决了反射攻击
  • 用类加载的线程安全性解决了并发
    其类加载部分的代码:
public abstract class Enum{
    private Enum{}
    private static Enum INSTANCE = null;
    static{
        INSTANCE = new Enum(){};
    }
}
  • 用静态方法初始化保证了线程安全,会在类加载的时候初始化
  • 没有实现clone
  • 不会有反序列化的问题, 这个使用javap 仍然没有看到类似于readObject的源代码,应该是jdk内部生成字节码的时候做了某些操作。

好了,综上,尽量用枚举单例,或者是Holder单例吧

时间: 2024-09-30 02:40:24

可能是最全的Java单例模式讨论的相关文章

调试-java单例模式传参问题

问题描述 java单例模式传参问题 用sshexec连接linux ConnBean connInfo = new ConnBean(remoteAddress, userName, password); SSHExec sshExec = SSHExec.getInstance(connInfo); 假设每次传过来的conninfo参数是变的,但是debug查看每次都是第一次的参数. private static SSHExec ssh; public static SSHExec getIn

java单例模式写法的问题, 请大神!

问题描述 java单例模式写法的问题, 请大神! 第一种写法: public class LoginService{ private static LoginService instance; public static LoginService getInstance() { if (instance == null) { synchronized (LoginService.class) { if (instance == null) { instance = new LoginServic

Java单例模式实例简述_java

本文实例讲述了Java的单例模式,是Java程序设计中非常重要的概念.分享给大家供大家参考.具体分析如下: 所谓单子模式就是在整个应用过程中只向外界提供唯一的一份实例,也就是说在应用时只有一个实例,这样也就不用反反复复的创建实例了.那么根据他的要求,看下面一个最简单的单例模式的代码: public class Singleton { private static Singleton single = new Singleton(); private Singleton(){ } public s

实例解析Java单例模式编程中对抽象工厂模式的运用_java

定义:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类. 类型:创建类模式 类图: 抽象工厂模式与工厂方法模式的区别        抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象.他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构:而抽象工厂模式则是针对的多个产品等级结构.在编程中,通常一个产品结构,表现为一个接口或者抽象类,也就是说,工厂方法模式提供的所有产品都是衍生自同一个接口或抽象类,而抽象工厂模式所提供的产品则是衍生自不同的

Java单例模式、饥饿模式代码实例_java

class MyThreadScopeData {       // 单例     private MyThreadScopeData() {     }       // 提供获取实例方法     public static synchronized MyThreadScopeData getThreadInstance() {         // 从当前线程范围内数据集中获取实例对象         MyThreadScopeData instance = map.get();      

轻松掌握Java单例模式_java

单例模式是23中设计模式中最简单的设计模式,在企业开发中也应用的特别多.单例模式的优点是:项目中有且仅有一个实例. 特点:构造器私有化,对象私有化,只提供一个对外访问的接口. 应用场景:     1.系统需要共享资源:比如日志系统,spring的资源管理器等     2.为了控制资源的使用:比如线程池 企业级开发和常见框架中的常见应用:     J2EE中的servlet,Spring中的资源管理器(即beans),数据库连接池,线程池,日志系统,网站计数器等 单例模式分类: 1.饿汉模式:饿汉

Java 单例模式的实现资料整理_java

Java单例模式的实现,对java 单例模式的几种实现方法进行了整理: 单例模式好多书上都是这么写的: public class SingleTon1 { private SingleTon1(){ } private static SingleTon1 instance = null; public static SingleTon1 getInstance(){ if(instance == null){ instance = new SingleTon1(); } return insta

Java单例模式实现的几种方式_java

Java单例模式实现的几种方式 单例模式好多书上都是这么写的: public class SingleTon1 { private SingleTon1(){ } private static SingleTon1 instance = null; public static SingleTon1 getInstance(){ if(instance == null){ instance = new SingleTon1(); } return instance; } } 但是实际开发中是不会这

java单例模式学习示例_java

单例模式有一下特点:1.单例类只能有一个实例.2.单例类必须自己自己创建自己的唯一实例.3.单例类必须给所有其他对象提供这一实例.单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器的功能.每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中.每台计算机可以有若干通信端口,系统应当集中管理这些通信端