可能是最全的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-11-05 09:34:05

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

java单例模式【csdn-炸死特】

概念: java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例.饿汉式单例.登记式单例三种. 单例模式有一下特点: 1.单例类只能有一个实例. 2.单例类必须自己自己创建自己的唯一实例. 3.单例类必须给所有其他对象提供这一实例. 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例.在计算机系统中,线程池.缓存.日志对象.对话框.打印机.显卡的驱动程序对象常被设计成单例.这些应用都或多或少具有资源管理器的功能.每台计算机可以有若干个打印机,但只能有一个Printe

【深入】java 单例模式(转)

[深入]java 单例模式 关于单例模式的文章,其实网上早就已经泛滥了.但一个小小的单例,里面却是有着许多的变化.网上的文章大多也是提到了其中的一个或几个点,很少有比较全面且脉络清晰的文章,于是,我便萌生了写这篇文章的念头.企图把这个单例说透,说深入.但愿我不会做的太差. 首先来看一个典型的实现: 1 /** 2 * 基础的单例模式,Lazy模式,非线程安全 3 * 优点:lazy,初次使用时实例化单例,避免资源浪费 4 * 缺点:1.lazy,如果实例初始化非常耗时,初始使用时,可能造成性能问

JAVA单例模式的实践

单例模式是JAVA设计模式中最常用.最重要的设计模式之一. 最简单的写法是: public class TestSingleton { private static String ourInstance = null; //私有化构造器 private TestSingleton() { } //提供静态方法给外部访问 public static String getOurInstance(){ if(ourInstance == null){ ourInstance="单例模式赋值成功!!!&

Java 单例模式探讨

以下是我再次研究单例(Java 单例模式缺点)时在网上收集的资料,相信你们看完就对单例完全掌握了 Java单例模式应该是看起来以及用起来简单的一种设计模式,但是就实现方式以及原理来说,也并不浅显哦. 总结一下我所知道的单例模式实现方式: 1.预先加载法 Java代码 class S1 { private S1() { System.out.println("ok1"); } private static S1 instance = new S1(); public static S1

【转】10个关于java单例模式的面试问题

10 interview question on Singleton Pattern in Java Question starts with What is Singleton class? Have you used Singleton before? Singleton is a class which has only one instance in whole application and provides a getInstance() method to access the s

最全面的Java多线程用法解析

最全面的java多线程用法解析,如果你对Java的多线程机制并没有深入的研究,那么本文可以帮助你更透彻地理解Java多线程的原理以及使用方法. 1.创建线程 在Java中创建线程有两种方法:使用Thread类和使用Runnable接口.在使用Runnable接口时需要建立一个Thread实例.因此,无论是通过Thread类还是Runnable接口建立线程,都必须建立Thread类或它的子类的实例.Thread构造函数: public Thread( ); public Thread(Runnab

你所知道的Java单例模式并不是单例模式

当我们搜索单例模式的时候,能看到很多例子,什么懒汉式.饿汉式,大概如下: public class Singleton { private static Singleton instance=null; private Singleton(){ System.out.println("Singleton..init..."); } public static Singleton getInstance(){ if(instance==null){ instance=new Single

深入Java单例模式(转)

深入Java单例模式 源自 http://devbean.blog.51cto.com/448512/203501 在GoF的23种设计模式中,单例模式是比较简单的一种.然而,有时候越是简单的东西越容易出现问题.下面就单例设计模式详细的探讨一下. 所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在.就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛,比如保存全局数据,实现全局性的操作等. 1. 最简单的实现 首先,能够想到的最简单的实现是,把类

java单例模式案例

</pre>单例模式是一种常见的设计模式,一般有三种方式,单例模式具有如下特点:<p></p><p><span style="white-space:pre">1.在类的属性中,自己初始化一个静态的私有的类实例<span style="white-space:pre"></span>2.将自己的构造方法写成private的方式,拒绝其他类或者操作再次使用构造函数将单例类再次实例化&l