技术分享|Java单例设计模式初探

2018-01-23 17:05:04

设计模式在软件开发者中十分受欢迎。设计模式是对于常见软件问题的良好解决方案。单例模式是 Java 中创建型设计模式的一种。

单例模式的目的是什么?

单例类的目的是控制对象创建,约束对象的数量有且只有一个。单例模式只允许有一个入口来创建类实例。

因为只有一个单例类实例,任何单例类的实例都将只会产生一个类,就像静态域一样。当你需要控制资源的时候,例如在数据库连接或者使用 sockets ,单例模式是非常有用的。

这看起来是一个很简单的设计模式,但是当我们真正去实现的时候,会带来许多的实现问题。单例模式的实现在开发者当中总是存在一定争议。现在,西安中软卓越的技术老师带领大家讨论一下如何创建一个单例类以完成下列目的:

技术分享|Java单例设计模式初探.jpg

限制类的实例并且保证在 JVM 中只存在一个类实例。

让我们在 Java 中创建单例类并在不同的情况下进行测试。

创建单例类

为了实现单例类,最简单方法是把构造器变为 private。有两种初始化方法。

饿汉式

饿汉式初始化,单例类的实例在类加载时被创建,这是创建单例类最简单的方法。

 

通过将构造器声明为 private ,不允许其他类来创建单例类实例。取而代之的是,创建一个静态方法(通常命名为 getInstance )来提供创建类实例的唯一入口。

 

public class SingletonClass {

 

    private static volatile SingletonClass sSoleInstance = new SingletonClass();

    //private constructor.

    private SingletonClass(){}

    public static SingletonClass getInstance() {

        return sSoleInstance;

    }

}

这种方法有一个缺陷,即使在程序没有使用到它的时候,实例已经被创建了。当你创建数据库连接或者 socket 时,这可能成为一个相当大的问题,会导致内存泄漏问题。解决方法是当需要的时候再创建实例,我们称之为懒汉式初始化。

懒汉式

与饿汉式相反,你在 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 中,如果两个对象是相同的,那么他们的 hashCode 也是相同的。让我们测试一下,如果上面的单例类都正确实现,那么将会返回同样的哈希。

 

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());  

       }

}

下面是输出日志:

15:04:341 I/System.out: Instance 1 hash:247127865

15:04:342 I/System.out: Instance 2 hash:247127865

可以看到两个实例拥有同样的 hashCode。所以,这就意味着上面的代码创建了完美的单例类,是吗?不。

让单例类反射安全

在上面的单例类中,通过反射可以创建不止一个实例。 Java Reflection 是一个在运行时检测或者修改类的运行时行为的过程。通过在运行时修改构造器的可见性并通过构造器创建实例可以产生新的单例类实例。运行下面的代码,单例类还存在吗?

public class SingletonTester {

       public static void main(String[] args) {

        //Create the 1st instance

        SingletonClass instance1 = SingletonClass.getInstance();

        //Create 2nd instance using Java Reflection 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();

        }

        //now lets check the hash key.

        System.out.println("Instance 1 hash:" + instance1.hashCode());

        System.out.println("Instance 2 hash:" + instance2.hashCode());

       }

}

下面是输出日志:

15:21:48.216 I/System.out: Instance 1 hash:51110277

15:21:48.216 I/System.out: Instance 2 hash:212057050

每一个实例都有不同的 hashCode。显然这个单例类无法通过测试。

解决方案:

为了预防反射导致的单例失败,当构造器已经初始化并且其他类再次初始化时,抛出一个运行时异常。让我们更新 SingletonClass.java 。

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(){

        if (sSoleInstance == null){ //if there is no instance available... create new one

            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();

       }

}

如果你多次运行这些代码,有时你会发现不同的线程创建了不同的实例。

16:16:24.148 I/System.out: Instance 1 hash:247127865

16:16:24.148 I/System.out: Instance 2 hash:267260104

这说明了你的单例类不是线程安全的。所有的线程同时调用 getInstance() 方法, sSoleInstance == null 条件对所有线程返回值,所以两个不同的实例被创建。这打破了单例原则。

解决方案

同步 getInstance() 方法

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() 方法之后,第二个线程必须等到第一个线程执行完 getInstance() 方法之后才能执行,这就保证了线程安全。

但是,这个方法同样有一些缺点:

锁的开销导致运行变慢

实例变量初始化之后的同步操作时不必要的双检查锁

使用 双检查锁 方法创建实例可以克服上面的问题。

这这种方法中,当实例为空时,在同步代码块中创建单例类,这样只有当 sSoleInstance 为空时,同步代码块才会执行,避免了不必要的同步操作。

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;

    }

}

使用 volatile 关键字

表面上看,这个方法看起来很完美,你只需要付出一次静态代码块的代价。但是除非你使用 volatile 关键字,否则单例仍然会被打破。

没有 volatile 修饰符,另一个线程可能在变量 sSoleInstance 正在初始化尚未完成时引用它。但是通过 volatile 的保证 happens-before 关系,所有对于 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;

    }

}

现在上面的单例类是线程安全的。在多线程应用环境中(比如安卓应用)保证单例类的线程安全是必需的。

 

让单例类序列化安全

在分布式系统中,有些情况下你需要在单例类中实现 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();

        }

   }

}

16:16:24.148 I/System.out: Instance 1 hash:247127865

16:16:24.148 I/System.out: Instance 2 hash:267260104

可以看到实例的 hashCode 是不同的,违反了单例原则。序列化单例类之后,当我们反序列化时,会创建一个新的类实例。为了预防另一个实例的产生,你需要提供 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();

    }

}

结论

在文章的最后,你可以创建线程,反射和序列化安全的单例类,但这仍然不是完美的单例,你可以使用克隆或者多个类加载器来创建不止一个实例。但是对于大多数应用,上面的实现方法已经可以很好的工作了。

作为一名开发人员,技术是立身的根本,是拴马桩的尊严;对于一个做教育机构,口碑如水是生命之源,是长盛不衰的根本。13年历经风雨依然昂首前行,未来的西安中软卓越同样会恪守初心,以技术立身,培养IT精英,捍卫教育本质。

 

本文由中软卓越(西安)汇集整理,转载请注明作者及出处。

如需学习java、UI设计、软件测试、Python、大数据、嵌入式、Linxu云计算,请点击咨询,加入我们让你的未来不再迷茫。

关于我们

【中软卓越】-中软国际旗下高端教育品牌,是中软国际有限公司投资的大型人才服务机构,是中软国际人才战略的核心组成部分之一,承担集团发展过程中人才储备和培养的任务;专注IT培训37年,国内高端IT培训品牌,教育部指定官方IT人才培训机构。专注java培训、UI设计培训、软件测试培训、Python培训、大数据培训、嵌入式培训、Linxu云计算培训等培训课程。217家合作院校,880家合作企业,真实项目实战,素质拓展,职业规划。零首付,100%保障就业,先就业,后付款。

最牛逼的毕业学员入职名企享受高薪就业。880家合作企业,年培育学员人数逾6000人,毕业学员就职于中软国际、百度、腾讯、阿里巴巴、IBM、华为科技、中兴通讯、软通动力等知名企业。

还有什么疑问?我们全面为你解答!为您提供一对一专人服务,请点击下方咨询

  • 卓越资讯
  • 猜你想看
  • 常见问题

    Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。[详细课程]

    软件开发是根据用户要求建造出软件系统或者系统中的软件部分的过程;软件开发是一项包括需求捕捉、需求分析、设计、实现和测试的系统工程。[详细课程]

    软件测试是在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程。[详细课程]

    Python是一种面向对象的解释型计算机程序设计语言,语法简洁清晰,特色之一是强制用空白符作为语句缩进,它常被昵称为胶水语言,能够把用其他语言制作的各种模块。[详细课程]

    UI设计分为实体UI和虚拟UI,互联网说的UI设计是虚拟UI,一般是指对软件的人机交互、操作逻辑、界面美观的整体设计。[详细课程]

    大数据,又称巨量资料,指的是所涉及的数据资料量规模巨大到无法通过人脑甚至主流软件工具,在合理时间内达到撷取、管理、处理、并整理成为帮助企业经营决策更积极目的的资讯。[详细课程]

    云计算是一种按使用量付费的模式,这种模式提供可用的、便捷的、按需的网络访问, 进入可配置的计算资源共享池(资源包括网络,服务器,存储,应用软件,服务),这些资源能够被快速提供,只需投入很少的管理工作,或与服务供应商进行很少的交互。[详细课程]

    西安市长安北路8号高速大厦三楼

    电话:029-61876930