Java并发编程-单例模式

Posted 扇影无风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程-单例模式相关的知识,希望对你有一定的参考价值。

为什么要使用单例模式

实际开发中,为了节约系统资源,有时需要确保系统中某个类只有唯一的一个实例,当这个唯一实例创建成功后,就无法再创建一个同类型的其它对象,所有的操作都只能基于这个唯一实例。为了确保实例的唯一性,可以通过单例模式实现。

最简单的单例类设计

public class Single {

    // 设置instance为静态变量
    private static Single instance = null;

    // 构造方法私有化
    private Single() {}

    // 静态方法-实例化对象
    public static Single getInstance() {
        if (instance == null) {
            // 实例化对象
            instance = new Single();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 通过getInstance()方法获取实例
        Single instance1 = Single.getInstance();
        // 通过getInstance()方法获取实例
        Single instance2 = Single.getInstance();
        // 判断两个对象是否相同,相同则表示获取的是同一个实例,否则不是
        System.out.println(instance1 == instance2);
    }
}

因为构造函数的私有化,所以在类外无法直接创建新的 Single 对象,但是可以通过 Single.getInstance() 方法来获取实例对象。第一次调用 getInstance() 方法时将创建一个唯一实例,再次调用的时候会返回这个实例,从而确保实例对象的唯一性。

思考

为什么 instance 要设置成静态变量?

饿汉式和懒汉式

饿汉式单例类
public class EagerSingle {

    // 定义静态变量instance,在类加载的时候已经将其实例化,只能在类内部调用
    private static EagerSingle instance = new EagerSingle();

    // 构造函数私有化-只能从类内部访问
    private EagerSingle() {}

    // 定义静态方法-获取instance对象,供类外部访问调用
    public static EagerSingle getInstance() {
        return instance;
    }
}

在类加载时,调用 new EagerInstance() 方法将静态变量 instance 初始化,此时类的私有构造方法会被调用,单例类的唯一实例被创建。

懒汉式单例类
public class LazySingle {

    // 设置instance为静态变量
    private static LazySingle instance = null;

    // 构造函数私有化
    private LazySingle() {}

    // 静态方法-实例化对象
    public static LazySingle getInstance() {
        if (instance == null) {
            // 实例化instance对象
            instance = new LazySingle();
        }
        return instance;
    }
}

懒汉式单例类在第一次调用 getInstance() 方法时创建唯一实例,与饿汉式单例类不同,在类加载的时候并不会进行类的实例化操作,这种技术也成为延迟加载技术,即需要的时候才会加载实例。但是为了避免在多线程环境下多个线程同时调用 getInstance() 方法造成创建多个实例问题,可以使用 sychronized 关键字,如下代码:

public class LazySingle {

    // 设置instance为静态变量
    private static LazySingle instance = null;

    // 构造函数私有化
    private LazySingle() {}

    // 静态方法-实例化对象,使用sychronized关键字,保证同一时刻只可一个线程使用getInstance()方法
    public synchronized static LazySingle getInstance() {
        if (instance == null) {
            // 实例化instance对象
            instance = new LazySingle();
        }
        return instance;
    }
}

该单例类在 getInstance() 方法上使用 sychronized 关键字,保证多线程下同时访问 getInstance() 方法的问题。但是每次调用 getInstance() 方法都需要进行线程锁定判断,高并发下会导致系统性能会大大降低。如何解决呢?因为单例类的目标是保证有且只有一个实例,所以我们只要对 instance = new LazySingle() 代码加锁即可,无需对整个 getInstance() 方法加锁。因此可以对以上代码进行修改,如下代码:

public class LazySingle {

    // 设置instance为静态变量
    private volatile static LazySingle instance = null;

    // 构造函数私有化
    private LazySingle() {}

    // 静态方法-实例化对象,使用sychronized关键字,保证同一时刻只可一个线程使用getInstance()方法
    public static LazySingle getInstance() {
        if (instance == null) {
            synchronized (LazySingle.class) {
                instance = new LazySingle();
            }
        }
        return instance;
    }
}

但是该代码还是存在缺陷,多线程环境下可能还是创建了多个对象,原因如下:

当线程 T1 和线程 T2 在同时调用 getInstance() 方法,此时 instance 为 null,所以都可以通过 instance == null 判断,但是因为 instance = new LazySingle() 代码进行了加锁,所以同时只可以让一个线程进入,如果线程 T1 拿到了锁,则会创建一个实例。此时线程 T2 处于阻塞状态,等待线程 T1 释放锁,线程 T1 执行完毕后释放锁,线程 T2 拿到锁后同样会执行 instance = new LazySingle() 代码创建一个实例,这样就导致了整个单例类创建了两个实例的问题。所以需要对代码进行改进,可以在 instance = new LazySingle() 代码前再进行一个判断,使用 if (instance == null) 语句,这样当线程 T2 拿到锁后会判断是否创建了实例,如果创建则不会再创建了,代码如下:

public class LazySingle {

    // 设置instance为静态变量,使用volatile关键字保证instance共享变量的内存可见性
    private volatile static LazySingle instance = null;

    // 构造函数私有化
    private LazySingle() {}

    // 静态方法-实例化对象,使用sychronized关键字,保证同一时刻只可一个线程使用getInstance()方法
    public static LazySingle getInstance() {
        if (instance == null) {
            synchronized (LazySingle.class) {
                if (instance == null) {
                    instance = new LazySingle();
                }
            }
        }
        return instance;
    }
}

需要注意,使用该方式需要使用 volatile 关键字修改 instance 变量,这样可以保证线程 T1 对 instance 变量的修改对线程 T2 立即可见,否则线程 T2 执行 instance == null 是为 true 的。

饿汉式与懒汉式比较

饿汉式单例类:在类加载的时候就将自己实例化,它的优点在于无需考虑多线程环境下的访问问题,可以很好的确保实例的唯一性,从调用速度和反应时间上看,饿汉式单例类会优于懒汉式单例类,因为实例一开始就被创建了。但是无论系统在运行的时候是否需要该对象都会在类加载的时候创建,因此从资源利用效率看饿汉式单例类不及懒汉式单例类。而且系统加载的时候就要创建对象,所以加载时间会比懒汉式要长。

懒汉式单例类:在第一次调用时创建实例,无需一直占用系统资源,实现了延迟加载,但是必须要处理多线程的访问问题,即多个线程同时调用创建实例的方法可能导致创建了多个实例的问题。所以需要使用锁机制来控制,这样会影响系统性能。

更好的实现方法

饿汉式单例类会在类加载的时候创建实例,不能实现延迟加载,所以不管用不用单例都会一直占据内存。懒汉式单例类使用锁机制保证线程安全,影响系统性能。那么如何将二者优点结合并克服缺点?使用 Initialization Demand Holder (IoDH) 技术。代码如下:

public class BestSingle {

    // 构造函数私有化
    private BestSingle() {}

    // 定义一个静态内部类,其中创建一个BestSingle实例
    public static class HolderClass {
        private static BestSingle instance = new BestSingle();
    }

    // 静态方法,返回instance对象
    public static BestSingle getInstance() {
        return HolderClass.instance;
    }

    public static void main(String[] args) {
        BestSingle instance1, instance2;
        instance1 = BestSingle.getInstance();
        instance2 = BestSingle.getInstance();
        System.out.println(instance1 == instance2);
    }
}

在单例类中加入一个静态内部类,在内部类中创建单例,再将该单例对象通过 getInstance() 方法返回给外部使用。由于静态单例对象并没有作为 BestSingle 的成员变量直接实例化,因为类加载的时候不会创建实例,第一次调用 getInstance() 方法会加载静态内部类 HolderClass,在该类内部定义了一个静态变量 intance,此时会初始化这个变量,由于 getInstance() 方法没有使用任何锁机制,对性能不会造成任何影响。所以通过这种方式即可以实现延迟加载,并且在保证线程安全的基础上不影响系统性能。所以该方法是最好的单例模式实现方法

单例模式适用场景

1、系统只需要一个实例对象,比如系统要求提供一个唯一的序列号生成器或资源管理器,或者考虑资源消耗太大只允许创建一个对象。

2、用户调用单例类的的实例只允许使用一个公共访问点,除了该公共访问点,不能通过其它路径访问该实例。

更多问题

如何对单例模式进行改造,使得系统中某个类的对象可以有有限多个?例如二例、三例等。(改造后的类可以称为多例类)

以上是关于Java并发编程-单例模式的主要内容,如果未能解决你的问题,请参考以下文章

JUC并发编程 --单例模式

JUC并发编程07:单例模式CAS算法和原子引用

并发-单例模式

Java高并发编程

JAVA OOP 编程-常用设计模式

马士兵java高并发编程三