专栏首页Java进阶指南设计模式之单例模式

设计模式之单例模式

单例模式,是特别常见的一种设计模式,因此我们有必要对它的概念和几种常见的写法非常了解,而且这也是面试中常问的知识点。

所谓单例模式,就是所有的请求都用一个对象来处理,如我们常用的Spring默认就是单例的,而多例模式是每一次请求都创建一个新的对象来处理,如structs2中的action。

使用单例模式,可以确保一个类只有一个实例,并且易于外部访问,还可以节省系统资源。如果在系统中,希望某个类的对象只存在一个,就可以使用单例模式。

那怎么确保一个类只有一个实例呢?

我们知道,通常我们会通过new关键字来创建一个新的对象。这个时候类的构造函数是public公有的,你可以随意创建多个类的实例。所以,首先我们需要把构造函数改为private私有的,这样就不能随意new对象了,也就控制了多个实例的随意创建。

然后,定义一个私有的静态属性,来代表类的实例,它只能类内部访问,不允许外部直接访问。

最后,通过一个静态的公有方法,把这个私有静态属性返回出去,这就为系统创建了一个全局唯一的访问点。

以上,就是单例模式的三个要素。总结为:

  1. 私有构造方法
  2. 指向自己实例的私有静态变量
  3. 对外的静态公共访问方法

单例模式分为饿汉式和懒汉式。它们的主要区别就是,实例化对象的时机不同。饿汉式,是在类加载时就会实例化一个对象。懒汉式,则是在真正使用的时候才会实例化对象。

饿汉式单例代码实现:

public class Singleton {

    // 饿汉式单例,直接创建一个私有的静态实例
    private static Singleton singleton = new Singleton();

    //私有构造方法
    private Singleton(){

    }

    //提供一个对外的静态公有方法
    public static Singleton getInstance(){
        return singleton;

    }
}

懒汉式单例代码实现

public class Singleton {

    // 懒汉式单例,类加载时先不创建实例
    private static Singleton singleton = null;

    //私有构造方法
    private Singleton(){

    }

    //真正使用时才创建类的实例
    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

稍有经验的程序员就发现了,以上懒汉式单例的实现方式,在单线程下是没有问题的。但是,如果在多线程中使用,就会发现它们返回的实例有可能不是同一个。我们可以通过代码来验证一下。创建十个线程,分别启动,线程内去获得类的实例,把实例的 hashcode 打印出来,只要相同则认为是同一个实例;若不同,则说明创建了多个实例。

public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new MyThread().start();
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.hashCode());
    }
}
/**
运行多次,就会发现,hashcode会出现不同值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/

所以,以上懒汉式的实现方式是线程不安全的。那饿汉式呢?你可以手动测试一下,会发现不管运行多少次,返回的hashcode都是相同的。因此,认为饿汉式单例是线程安全的。

那为什么饿汉式就是线程安全的呢?这是因为,饿汉式单例在类加载时,就创建了类的实例,也就是说在线程去访问单例对象之前就已经创建好实例了。而一个类在整个生命周期中只会被加载一次。因此,也就可以保证实例只有一个。所以说,饿汉式单例天生就是线程安全的。(可以了解一下类加载机制)

既然懒汉式单例不是线程安全的,那么我们就需要去改造一下,让它在多线程环境下也能正常工作。以下介绍几种常见的写法:

1) 使用synchronized方法

实现非常简单,只需要在方法上加一个synchronized关键字即可

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    //使用synchronized修饰方法,即可保证线程安全
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

这种方式,虽然可以保证线程安全,但是同步方法的作用域太大,锁的粒度比较粗,因此,执行效率就比较低。

2) synchronized 同步块

既然,同步整个方法的作用域大,那我缩小范围,在方法里边,只同步创建实例的那一小部分代码块不就可以了吗(因为方法较简单,所以锁代码块和锁方法没什么明显区别)。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //synchronized只修饰方法内部的部分代码块
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

这种方法,本质上和第一种没什么区别,因此,效率提升不大,可以忽略不计。

3) 双重检测(double check)

可以看到,以上的第二种方法只要调用getInstance方法,就会走到同步代码块里。因此,会对效率产生影响。其实,我们完全可以先判断实例是否已经存在。若已经存在,则说明已经创建好实例了,也就不需要走同步代码块了;若不存在即为空,才进入同步代码块,这样可以提高执行效率。因此,就有以下双重检测了:

public class Singleton {

    //注意,此变量需要用volatile修饰以防止指令重排序
    private static volatile Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //进入方法内,先判断实例是否为空,以确定是否需要进入同步代码块
        if(singleton == null){
            synchronized (Singleton.class){
                //进入同步代码块时也需要判断实例是否为空
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

需要注意的一点是,此方式中,静态实例变量需要用volatile修饰。因为,new Singleton() 是一个非原子性操作,其流程为:

a.给 singleton 实例分配内存空间
b.调用Singleton类的构造函数创建实例
c.将 singleton 实例指向分配的内存空间,这时认为singleton实例不为空

正常顺序为 a->b->c,但是,jvm为了优化编译程序,有时候会进行指令重排序。就会出现执行顺序为 a->c->b。这在多线程中就会表现为,线程1执行了new对象操作,然后发生了指令重排序,会导致singleton实例已经指向了分配的内存空间(c),但是实际上,实例还没创建完成呢(b)。

这个时候,线程2就会认为实例不为空,判断 if(singleton == null)为false,于是不走同步代码块,直接返回singleton实例(此时拿到的是未实例化的对象),因此,就会导致线程2的对象不可用而使用时报错。

4)使用静态内部类

思考一下,由于类加载是按需加载,并且只加载一次,所以能保证线程安全,这也是为什么说饿汉式单例是天生线程安全的。同样的道理,我们是不是也可以通过定义一个静态内部类来保证类属性只被加载一次呢。

public class Singleton {

    private Singleton(){

    }

    //静态内部类
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        //调用内部类的属性,获取单例对象
        return Holder.singleton;
    }
}

而且,JVM在加载外部类的时候,不会加载静态内部类,只有在内部类的方法或属性(此处即指singleton实例)被调用时才会加载,因此不会造成空间的浪费。

5)使用枚举类

因为枚举类是线程安全的,并且只会加载一次,所以利用这个特性,可以通过枚举类来实现单例。

public class Singleton {

    private Singleton(){

    }

    //定义一个枚举类
    private enum SingletonEnum {
        //创建一个枚举实例
        INSTANCE;

        private Singleton singleton;

        //在枚举类的构造方法内实例化单例类
        SingletonEnum(){
            singleton = new Singleton();
        }

        private Singleton getInstance(){
            return singleton;
        }
    }

    public static Singleton getInstance(){
        //获取singleton实例
        return SingletonEnum.INSTANCE.getInstance();
    }
}

本文分享自微信公众号 - 烟雨星空(mistyskys),作者:烟雨星空

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-01-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 设计模式之单例模式讲解设计模式之单例模式讲解

    Singleton是一种创建型模式,指某个类采用Singleton模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点

    yukong
  • 设计模式之单例模式

    会存在并发问题,因为调用newInstance()方法时没有加锁,导致会并发执行,如图:

    tanoak
  • 设计模式之单例模式

      无论什么开发中,设计模式都起着关键的作用,其中比较常用的当属单例了,所谓单例,就是让一个类在项目中只存在一个对象,即使用到这个类的地方很多,也只存在一个对象...

    codingblock
  • 设计模式之——单例模式

    单例模式是在面试中是最容易被考到的设计模式,这是因为单例模式是设计模式中最简单的,几行代码就能搞定(现场手写代码);同时单例模式又有多种实现方式,涉及到线程安全...

    Yano_nankai
  • 设计模式之单例模式

    爱撒谎的男孩
  • 设计模式之单例模式

    单例模式是GOF 23个设计模式中最简单的模式了,它提供了一种创建唯一对象的最佳实现,注意此处的简单只是表述和意图很简单,但是实现起来,尤其是实现一个优美的单例...

    Edison.Ma
  • 设计模式之单例模式

    单例模式(Singleton Pattern)限制系统中某一个类只能有一个唯一的实例。很多时候系统对类的需求就只是一个全局对象,有些资源比较重,加载创建耗时,适...

    Dylan Liu
  • 设计模式之单例模式

    在学习设计模式时 , 我们尽量掌握每种模式的场景 , 意义 ,以及思维模式的转化 ,而不是死记硬背代码

    时间静止不是简史
  • 设计模式之单例模式

    在前面中,我们知道如果一个bean需要被加载,首先需要获取资源的位置,然后根据资源位置获取xml文件,然后将其变成document,然后根据document对元...

    路行的亚洲
  • 设计模式之单例模式

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明...

    bering
  • 设计模式之单例模式

      Java Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。 使用Singleton的好处还在于可以节省内存,因为...

    雨落凋殇
  • 设计模式之单例模式

    通过上面的例子,我们实现了单例模式,无论我们怎样实例化类,都只能实例化一次类,大大的节省里系统资源的创建和销毁的开销

    北溟有鱼QAQ
  • 设计模式之单例模式

    单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内...

    用户1205080
  • 设计模式之单例模式

    单例模式(Sigleton Parttern)是23种设计模式中最简单也是最常见的一种设计模式,单例模式确保了一个类只有一个实例,由于内存中只有一个...

    小诸葛
  • 【设计模式】之单例模式

    单例模式是23种设计模式中最简单、最常见的一种,也是各个公司面试题中必考的设计模式之一,是程序猿必备掌握的。

    xcbeyond
  • 设计模式之单例模式

    小弟最近在研究设计模式,准备边学边发博客,与众多大佬们交流学习,希望各位能够指出不足之处(废话不多说了,直接开花)** **

    止术
  • 设计模式 之 单例模式

        Ensure a class has only one instance,and provide a global point of access to...

    Jacob丶
  • 设计模式之单例模式

    Ensure a class has only one instance, and provide a global point of access to it...

    beginor
  • 设计模式之单例模式

    单例模式是创建对象最简单的方式。单例模式的定义 是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    一粒小麦

扫码关注云+社区

领取腾讯云代金券