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

领会单例设计模式(Java版本)_第1张图片

设计模式在软件开发人员中非常流行。设计模式是一种通用软件问题的精妙解决方案。单例模式是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 clazz = SingletonClass.class;
            Constructor 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
.

你可能感兴趣的:(领会单例设计模式(Java版本))