Java 设计模式 之 单例模式(Singleton)

目录

单例模式(Singleton)

最简单的单例类

加锁版单例类

内部类单例类


单例模式(Singleton)

1、单例对象(Singleton)是一种常用的设计模式,在 Java 应用中,单例对象能保证在一个 JVM 中,该对象只有一个实例存在。

2、这样的模式有几个好处:

1)某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

2)省去了 new 操作符,降低了系统内存的使用频率,减轻 GC 压力。

3)多线程中进行数据共享,有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

3、需要清楚的是单例模式作为一种模式,其实现方式并不是规定死的,可以是任意的,只要能达到期望的效果即可。也就是说实际情况中可能大家写的各有不同,但是大同小异。

最简单的单例类

1、下面开始写一个简单的单例类:

package main.singleton;

import java.util.logging.Logger;
/**
 * Created by Administrator on 2019/4/8 0008.
 * trade:交易,实现单例模式
 */
public class Trade {
    //私有静态实例,防止被引用。此处赋值为 null,目的是实现延迟加载
    private static Trade instance;

    //私有构造方法,防止被实例化
    private Trade() {

    }

    //静态工程方法,创建实例
    public static Trade getInstance() {
        if (instance == null) {
            instance = new Trade();
            Logger.getAnonymousLogger().info("创建 Trade 实例..." + Thread.currentThread().getName());
        }
        return instance;
    }
}

4、测试如下:

package main.test;

import main.singleton.Trade;
/**
 * Created by Administrator on 2019/4/8 0008.
 */
public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    System.out.println("--------------" + Thread.currentThread().getName());
                    Trade.getInstance();
                }
            }.start();
        }
    }
}

Java 设计模式 之 单例模式(Singleton)

演示的是 10 个线程同时创建 Trade 实例,结果是只有第一个操作的线程会创建一次对象,后面的不会再创建。

加锁版单例类

1、上面的 Trade 类基本可以满足要求,但是像这样毫无线程安全保护的类,如果把它放入多线程的环境下,仍然有可能会出现问题。首先会想到对 getInstance 方法加 synchronized 关键字。

package main.singleton;

import java.util.logging.Logger;

/**
 * Created by Administrator on 2019/4/8 0008.
 * trade:交易,实现单例模式
 */
public class Trade {
    //私有静态实例,防止被引用。此处赋值为 null,目的是实现延迟加载
    private static Trade instance;

    //私有构造方法,防止被实例化
    private Trade() {

    }

    //静态工程方法,创建实例,添加了类锁
    public synchronized static Trade getInstance() {
        if (instance == null) {
            instance = new Trade();
            Logger.getAnonymousLogger().info("创建 Trade 实例..." + Thread.currentThread().getName());
        }
        return instance;
    }
}

2、上面这样添加 synchronized 关键字后,每次调用 getInstance()方法,都要对对象上锁,事实上只有在第一次创建对象的时候需要加锁,之后就不需要了,所以可以改进如下:

package main.singleton;

import java.util.logging.Logger;

/**
 * Created by Administrator on 2019/4/8 0008.
 * trade:交易,实现单例模式
 */
public class Trade {
    //私有静态实例,防止被引用。此处赋值为 null,目的是实现延迟加载
    private static Trade instance = null;

    //私有构造方法,防止被实例化
    private Trade() {

    }

    //静态工程方法,创建实例
    public static Trade getInstance() {
        if (instance == null) {
            /**
             * 创建实例时添加类锁
             * 这样当第一次 instance 为 null时,添加类锁,后续不再需要加锁
             */
            synchronized (Trade.class) {
                if (instance == null) {
                    instance = new Trade();
                    Logger.getAnonymousLogger().info("创建 Trade 实例..." + Thread.currentThread().getName());
                }
            }
        }
        return instance;
    }
}

3、因为只需要在创建实例的时候进行同步,所以只要将创建实例和 getInstance() 方法分开,单独为创建实例加 synchronized 关键字也是可以的,如下所示:

package main.singleton;

import java.util.logging.Logger;

/**
 * Created by Administrator on 2019/4/8 0008.
 * trade:交易,实现单例模式
 */
public class Trade {
    //私有静态实例,防止被引用。此处赋值为 null,目的是实现延迟加载
    private static Trade instance = null;

    //私有构造方法,防止被实例化
    private Trade() {
    }

    //同步创建实例,添加了类锁
    private synchronized static void syncInit() {
        if (instance == null) {
            instance = new Trade();
            Logger.getAnonymousLogger().info("创建 Trade 实例..." + Thread.currentThread().getName());
        }
    }

    //静态工程方法,创建实例
    public static Trade getInstance() {
        if (instance == null) {
            syncInit();
        }
        return instance;
    }
}

实现方式并不是唯一的,但都是大同小异,除了使用 synchronized 关键字,比如也可以使用 Lock 接口。

内部类单例类

1、加了 synchronized 关键字,只有在 instance 为 null 时,为其创建对象,同时加锁,性能有一定的提升。但是仍然可能存在问题的。

2、Java 指令中创建对象和赋值操作其实是分开进行的,也就是说 instance = new Trade(); 语句是分两步执行的。而 JVM 并不保证这两个操作的先后顺序,也就是说有可能 JVM 会先为新的 Trade 实例分配空间,然后直接赋值给 instance 成员,然后再去初始化这个 Singleton 实例。这样有可能会出错,以 A、B 两个线程为例:

1)A、B 线程同时进入了第一个 if (instance == null) { 判断 
2)A 首先进入 synchronized ,由于 instance 为 null,所以它执行 instance = new Trade();
3)由于 JVM 内部的优化机制,JVM 先划出了一些分配给 Trade 实例的空白内存,并赋值给 instance 成员(注意此时JVM没有开始初始化这个实例),然后 A 离开了synchronized。
4)B 进入 synchronized ,由于 instance 此时不是null,因此它马上离开了synchronized 并将结果返回给调用该方法的程序。 
5)此时 B 线程打算使用 Trade 实例,却发现它没有被初始化,于是错误发生了。

3、可以使用内部类的方式解决如下:

package main.singleton;

/**
 * Created by Administrator on 2019/4/8 0008.
 * trade:交易,实现单例模式
 */
public class Trade {

    //私有构造方法,防止被实例化
    private Trade() {
    }

    //此处使用一个内部类来维护单例
    //static 修饰的成员变量随着类的加载而加载,在类加载时进行唯一一次的初始化
    private static class TradeFactory {
        private static Trade instance = new Trade();
    }

    //静态工程方法,创建实例
    public static Trade getInstance() {
        return TradeFactory.instance;
    }
}

1)单例模式使用内部类来维护单例的实现,JVM 内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的,这样当第一次调用 getInstance 的时候,JVM 能够保证 instance 只被创建一次,并且会保证把赋值给 instance 的内存初始化完毕。

2)该方式也只会在第一次调用的时候使用互斥机制。