Java设计模式之单例模式

单例模式,是特别常见的一种设计模式,所以咱们有必要对它的概念和几种常见的写法很是了解,并且这也是面试中常问的知识点。程序员

所谓单例模式,就是全部的请求都用一个对象来处理,如咱们经常使用的Spring默认就是单例的,而多例模式是每一次请求都建立一个新的对象来处理,如structs2中的action。面试

使用单例模式,能够确保一个类只有一个实例,而且易于外部访问,还能够节省系统资源。若是在系统中,但愿某个类的对象只存在一个,就可使用单例模式。设计模式

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

咱们知道,一般咱们会经过new关键字来建立一个新的对象。这个时候类的构造函数是public公有的,你能够随意建立多个类的实例。因此,首先咱们须要把构造函数改成private私有的,这样就不能随意new对象了,也就控制了多个实例的随意建立。多线程

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

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

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

  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();
    }
}
相关文章
相关标签/搜索