[Java高并发编程](七)单例模式

本博客根据《Java多线程编程核心技术》和《实战高并发程序设计》总结整理

概述

  单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。在java中,这样的行为能带来两大好处:

  • 对于频繁使用的对象,可以省略new操作花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销
  • 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间

单例模式本身与并行没有直接的关系,但是,我们不可避免的会在多线程环境中使用它们。并且,系统中使用单例的地方可能非常频繁,因此我们需要一种高效的单例实现。

立即加载 / ‘饿汉模式’

  立即加载就是使用类的时候已经将对象创建完毕,常见的实现办法就是直接new 实例化。而立即加载从中文看就是“着急,急迫”的意思,所以也叫“饿汉模式”。
  这个实现非常简单,但无疑是一个正确并且良好的实现。

public class Singleton {
    private Singleton(){
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "  " +Singleton.getInstance().hashCode());
            }).start();
        }
    }
}

运行结果如下:
[Java高并发编程](七)单例模式_第1张图片

使用这种方式需要注意的是:

  1. 为了保证系统中不会有人意外创建多余实例,我们把Singleton的构造函数设置为private
  2. instance对象必须是private并且static的。如果不是private,那么instance的安全性无法保证。一个意外就可能使得instance变为null。其次,因为工厂方法getinstance()必须是static的,因此对应的instance也必须是static。
  3. 这个单例性能是非常好的,因为getInstance()方法只是简单的返回instance,并没有任何锁操作,因此在并行程序中,会有良好的表现。

不足之处:
  Singleton实例在什么时候创建是不受控制的,对于静态成员instance,它会在类第一次初始化的时候被创建。这个时刻并不一定是getInstance()方法第一次被调用的时候。也就是说这种单例实现方式不能够精确的控制instance的创建时间,如果这种不足不重要,那么这种单例实现方式是一个不错的选择。

延迟加载 / ‘懒汉模式’

  延迟加载就是在调用get()方法时实例才会被创建,常见的实现办法就是在get()方法中进行new实例化。这样实现的好处是:
  

  • 充分利用了延迟加载,只有在真正需要时创建对象

但是坏处也很明显,在多线程的环境中,会存在线程安全问题。
如下代码所示:

public class Singleton {
    private Singleton(){
    }

    private static Singleton instance;

    public static Singleton getInstance(){
        //延迟加载
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "  " +Singleton.getInstance().hashCode());
            }).start();
        }
    }
}

运行结果如下:
[Java高并发编程](七)单例模式_第2张图片

优化方案一:加锁

  为了防止对象被创建,我们可以使用synchronized关键字进行方法同步,这种实现的好处是,充分利用了延迟加载的特性,只有在真正需要时创建对象,但坏处也很明显,并发环境下加锁,竞争激烈的场合对性能可能产生一定的影响。但总体来说,这是一个非常易于实现和理解的方法。
代码如下:

public class Singleton {
    private Singleton(){
    }

    private static Singleton instance;

    public static synchronized Singleton getInstance(){
        //延迟加载
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

优化方案二:利用双重检查加锁机制

双重检查加锁机制分为如下两重检查:

  1. 在程序每次调用getInstance()方法时先不进行同步,而是在进入该方法后再去检查类实例是否存在,若不存在则进入接下来的同步代码块
  2. 进入同步代码块后将再次检查类实例是否存在,若不存在则创建一个新的实例

这样一来,就只需要在类实例初始化时进行一次同步代码判断即可,而非每次调用getInstance()方法都进行同步判断,大大节省了时间,具体实现如下:

public class Singleton {
    /**
     * 装载时不创建类实例,但需要利用一个类变量去保存后续创建的类实例
     * 添加volatile关键词使其不会被本地线程缓存,保证线程能正确处理
     * 添加static关键词使得该变量能在getInstance()静态方法中使用
     */
    private volatile static Singleton instance = null;

    /**
     * 私有化构造方法,使外部无法通过构造方法构造除instance外的类实例
     * 从而达到单例模式控制类实例数目的目的
     */
    private Singleton() {
    }

    /**
     * 类实例的全局访问方法
     * 添加static关键词使得外部可以通过类名直接调用该方法获取类实例
     * @return 单例类实例
     */
    public static Singleton getInstance() {
        //  第一重检查:如果instance未被初始化,则进入同步代码块
        if (instance == null) {
            //  同步代码块,保证线程安全
            synchronized (Singleton.class) {
                //  第二重检查:如果instance未被初始化,则初始化该类实例
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

问题:
在《实战Java高并发程序设计》中,作者提到,这种方式实现单例模式是一种非常丑陋、复杂的方法,甚至在低版本的JDK中都不能保证正确性。因此不推荐大家使用。

终极优化三:Lazy Initialization Holder Class 模式

这一方案的核心在于Java的类级内部类(即使用static关键字修饰的内部类,否则称之为对象级内部类)以及多线程缺省同步锁,先看看具体实现:

public class Singleton {
    /**
     * 私有化构造方法,使外部无法通过构造方法构造除instance外的类实例
     * 从而达到单例模式控制类实例数目的目的
     */
    private Singleton() {
    }

    /**
     * 类级内部类,用于缓存类实例
     * 该类将在被调用时才会被装载,从而实现了延迟加载
     * 同时由于instance采用静态初始化的方式,因此JVM能保证其线程安全性
     */
    private static class Instance {
        private static Singleton instance = new Singleton();
    }


    /**
     * 类实例的全局访问方法
     * 添加static关键词使得外部可以通过类名直接调用该方法获取类实例
     * @return 单例类实例
     */
    public static Singleton getInstance() {
        return Instance.instance;
    }
}

首先,getInstance()方法中没有锁,这使得在高并发环境下性能优越。其次,只有在getInstance()方法被第一次调用时,Singleton对象才会被创建。因为这种方法巧妙的使用了内部类和类的初始化方式。内部类Singleton被声明为private,这使得我们不可能在外部访问并初始化它。而我们只可能在getInstance()内部对Singleton类进行初始化,利用虚拟机的类初始化机制创建单例。

你可能感兴趣的:(꧁JAVA꧂,Java多线程技术)