领会单例设计模式(Java版本)

设计模式在软件开发人员中非常流行。设计模式是一种通用软件问题的精妙解决方案。单例模式是Java创建型设计模式中的一种。

单例模式的目的是什么?

单例类的目的是为了控制对象的创建,限制对象的数量只能是1。单例只允许有一个入口可以创建这个类的实例。

由于只有一个单例实例,所以单例中任何字段的初始化都应该像静态字段一样只发生一次。当我们需要操控一些资源比如数据库连接或者sokets等时,单例就非常有用。

听起来好像是一个非常简单的设计模式,但是当要具体实现的时候,它会带来许多实践问题。怎么实现单例模式在开发者之间一直是一个有争议的话题。在这里我们将讨论怎样创建一个单例类来满足它的意图:

限制类的实例化以确保java虚拟机中只有唯一一个该类的实例存在。

我们用java创建一个单例类,并在不同的条件下测试。

使用Java创建一个单例类

实现单例类最简单的方式就是将该类的构造函数定义为私有方法。

  • 1、饿汉式初始化:

在饿汉模式中,单例类的实例在加载这个类的时候就被创建,这是创建单例类最简单的方法。

将单例类的构造函数定义为私有方法,其他类就不能创建该类的实例。取而代之的是通过我们提供的静态方法入口(通常命名为getInstance())来获取该类实例。

public class SingletonClass {

    private static volatile SingletonClass sSoleInstance = new SingletonClass();

    //私有构造函数
    private SingletonClass(){}

    public static SingletonClass getInstance() {
        return sSoleInstance;
    }
}

这种方法有一个缺点。这里有可能我们不会使用这个实例,但是单例的实例还是会被创建。如果你的单例类在创建中需要建立同数据库的链接或者创建一个socket时,这可能是一个严重的问题。因为这可能会导致内存泄漏。解决办法是当我们需要使用时再创建单例类的实例。这就是所谓的饿汉式初始化。

  • 2、懒汉式初始化:

饿汉式初始化不同,这里我们由getInstance()方法自己初始化单例类的实例。这个方法会检查该类是否已经有创建实例?如果有,那么getInstance()方法会返回已经创建的实例,否则就在JVM中创建一个该类的实例并返回。这种方法就称为懒汉式初始化。

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    private SingletonClass(){}  //private constructor.

    public static SingletonClass getInstance(){
        if (sSoleInstance == null){ //if there is no instance available... create new one
            sSoleInstance = new SingletonClass();
        }

        return sSoleInstance;
    }
}

我们知道在Java中如果两个对象相同,那么他们的哈希值也应该相等。那么我们来测试一下。如果上面的单例实现是正确的,那么下面的测试代码应该返回相同的哈希值。

public class SingletonTester {
   public static void main(String[] args) {
        //Instance 1
        SingletonClass instance1 = SingletonClass.getInstance();

        //Instance 2
        SingletonClass instance2 = SingletonClass.getInstance();

        //now lets check the hash key.
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());  
   }
}

下面是两个实例哈希值的log输出。

我们可以看到两个实例的哈希值是相等的。所以,这意味着以上代码完美的实现了一个单例。是吗????不是。

如果使用Java的反射API呢?

在上面的单例类中,通过使用反射我们可以创建多个实例。如果你不知道Java反射API,Java反射就是在代码运行时检查或者修改类的运行时行为的过程。

我们可以在运行过程中将单例类的构造函数的可见性修改为public,从而使用修改后的构造函数来创建新的实例。运行下面的代码,看看我们的单例是否还能幸存?

public class SingletonTester {
   public static void main(String[] args) {
        //创建第一个实例
        SingletonClass instance1 = SingletonClass.getInstance();
        
        //使用Java反射API创建第二个实例.
        SingletonClass instance2 = null;
        try {
            Class<SingletonClass> clazz = SingletonClass.class;
            Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
            cons.setAccessible(true);
            instance2 = cons.newInstance();
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }

        //现在来检查一下两个实例的哈希值
        System.out.println("Instance 1 hash:" + instance1.hashCode());
        System.out.println("Instance 2 hash:" + instance2.hashCode());
   }
}

这里是两个实例哈希值的log输出。

哈希值并不相等

两个实例的哈希值并不相等。这清晰的说明我们的单例类并不能通过这个测试。

解决方法:

为了阻止因为反射导致的测试失败,如果构造函数已经被调用过还有其他类再次调用时我们必须抛出一个异常。来更新一下SingletonClass.java

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    //私有构造方法
    private SingletonClass(){
       
        //阻止通过反射的API调用.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    } 

    public static SingletonClass getInstance(){
        if (sSoleInstance == null){ //如果还没可用的实例。。。。创建一个
            sSoleInstance = new SingletonClass();
        }

        return sSoleInstance;
    }
}

我们的单例线程安全吗?

如果有两个线程几乎在同时初始化我们的单例类,会发生什么?我们一起来测试一下下面的代码,这段代码中两线程几乎同时创建并且都调用getInstance()方法。

public class SingletonTester {
   public static void main(String[] args) {
        //Thread 1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonClass instance1 = SingletonClass.getInstance();
                System.out.println("Instance 1 hash:" + instance1.hashCode());
            }
        });

        //Thread 2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                SingletonClass instance2 = SingletonClass.getInstance();
                System.out.println("Instance 2 hash:" + instance2.hashCode());
            }
        });

        //start both the threads
        t1.start();
        t2.start();
   }
}

如果你运行这段代码很多次,你会发现有时两个线程创建了不同的实例:

哈希值并不相等

这意味着我们的单例是线程不安全的。如果两个线程同时调用我们的getInstance()方法,那么sSoleInstance == null条件对两个线程都成立,所以会创建同一个类的两个实例。这破坏了单例规则。

解决方法:

1.将getInstance()方法定义为synchronized:
我们将getInstance()方法定义为synchronized

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    //private constructor.
    private SingletonClass(){
       
        //Prevent form the reflection api.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    } 

    public synchronized static SingletonClass getInstance(){
        if (sSoleInstance == null){ //if there is no instance available... create new one
            sSoleInstance = new SingletonClass();
        }

        return sSoleInstance;
    }
}

getInstance()方法定义为synchronized,那么第二个线程就必须等待第一个线程的getInstance()方法运行完。这种方式能够达到线程安全的目的,但是使用这种方法有一些缺点:

  • 频繁的锁导致性能低下
  • 一旦实例初始化完成,没有必要再进行同步操作

2、双重检查锁方法:
如果我们使用双重检查锁方法来创建单例类则可以解决这个问题。在这种方法中,我们仅仅将实例为null条件下的代码块同步执行。所以只有在sSoleInstance为null的情况下同步代码块才会执行,这样一旦实例变量初始化成功就不会出现不必要的同步操作。

public class SingletonClass {

    private static SingletonClass sSoleInstance;

    //private constructor.
    private SingletonClass(){

        //Prevent form the reflection api.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    public static SingletonClass getInstance() {
        //Double check locking pattern
        if (sSoleInstance == null) { //Check for the first time
          
            synchronized (SingletonClass.class) {   //Check for the second time.
              //if there is no instance available... create new one
              if (sSoleInstance == null) sSoleInstance = new SingletonClass();
            }
        }

        return sSoleInstance;
    }
}

3、使用volatile
这个方法表面上看起来很完美,因为你只需要执行一次同步代码块,但是在你将sSoleInstance变量定义为volatile之前测试仍然会失败。

如果没有volatile修饰符,就可能会出现另一个线程可以访问到处于半初始化状态的_instance变量,但是使用了volatile类型的变量,它能保证:对 volatile 变量sSoleInstance的写操作,不允许和它之前的读写操作打乱顺序;对 volatile 变量sSoleInstance的读操作,不允许和它之后的读写乱序。

    public class SingletonClass {
    
        private static volatile SingletonClass sSoleInstance;
    
        //private constructor.
        private SingletonClass(){
    
            //Prevent form the reflection api.
            if (sSoleInstance != null){
                throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
            }
        }
    
        public static SingletonClass getInstance() {
            //Double check locking pattern
            if (sSoleInstance == null) { //Check for the first time
              
                synchronized (SingletonClass.class) {   //Check for the second time.
                  //if there is no instance available... create new one
                  if (sSoleInstance == null) sSoleInstance = new SingletonClass();
                }
            }
    
            return sSoleInstance;
        }
    }

现在我们的单例类是线程安全的。保证单例线程安全是非常重要的,尤其是在Android应用这样的多线程应用环境。

保证单例序列化安全:

在分布式系统中,我们有时需要在单例类中实现Serializable接口。通过实现Serializable可以将它的一些状态存储在文件系统中以供后续使用。让我们来测试一下我们的单例类在序列化和反序列化以后是否能够只有一个实例?

public class SingletonTester {
   public static void main(String[] args) {
      
      try {
            SingletonClass instance1 = SingletonClass.getInstance();
            ObjectOutput out = null;

            out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
            out.writeObject(instance1);
            out.close();

            //deserialize from file to object
            ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
            SingletonClass instance2 = (SingletonClass) in.readObject();
            in.close();

            System.out.println("instance1 hashCode=" + instance1.hashCode());
            System.out.println("instance2 hashCode=" + instance2.hashCode());

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
   }
}
哈希值不相等

我们可以看到两个实例的哈希值并不相等。很显然还是违反了单例原则。上面描述的序列化单例问题是因为当我们需要反序列化一个单例时,会创建一个新的实例。

为了防止创建新的实例,我们必须提供readResolve()方法的实现。readResolve()取代了从数据流中读取对象。这样就能保证其他类无法通过序列化和反序列化来创建新的实例。

public class SingletonClass implements Serializable {

    private static volatile SingletonClass sSoleInstance;

    //private constructor.
    private SingletonClass(){

        //Prevent form the reflection api.
        if (sSoleInstance != null){
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    public static SingletonClass getInstance() {
        if (sSoleInstance == null) { //if there is no instance available... create new one
            synchronized (SingletonClass.class) {
                if (sSoleInstance == null) sSoleInstance = new SingletonClass();
            }
        }

        return sSoleInstance;
    }

    //Make singleton from serialize and deserialize operation.
    protected SingletonClass readResolve() {
        return getInstance();
    }
}

结论:

行文至此,我们已经创建了一个线程安全、反射安全的单例类。但是这个单例类仍然不是一个完美的单例。我们还可以通过克隆或者多个类加载来创建多个单例类的实例,从而破坏单例规则。但是在多数应用中,上面的单例实现能够完美的工作。

本文译自Digesting Singleton Design Pattern in Java
.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容