单例模式

为什么需要单例模式

  1. 如果整个程序运行时有一个实例就够用了,何必创建出来多个实例浪费内存呢;
  2. 有些保存数据的实例在程序中只有一份即可,有多份的话可能导致数据不一致问题;

实现

1. 非线程安全的懒汉模式

/**
 * Created by clearbug on 2018/3/10.
 *
 * singleton, [n] 独生子, 独身, (扑克牌)单张。在这里即表示“单例”的意思
 */
public class Singleton {

    private static Singleton instance;
    
    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

优点:

  • 简单易懂;

缺点:

  • 获取实例的方法 getInstance 线程不安全,多线程环境下可能导致创建了多个实例;

2. 线程安全的懒汉模式(单锁)

/**
 * Created by clearbug on 2018/3/10.
 *
 * singleton, [n] 独生子, 独身, (扑克牌)单张。在这里即表示“单例”的意思
 */
public class Singleton {

    private static Singleton instance;

    // 构造器私有化
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}

这里保证线程安全的方法很简单:就是在获取实例的方法 getInstance 上使用 synchronized 加锁!这样虽然是保证了线程安全,但是每个线程来获取实例是都要经过“获取锁”、“释放锁”两个步骤,无疑效率大打折扣!那么,怎么做到既能保证线程安全又可以尽可能的提高效率呢?这就要说到 DCL(double-checked locking),双重校验锁机制了。

3. 线程安全的懒汉模式(单锁,双重校验)

/**
 * Created by clearbug on 2018/3/10.
 *
 * singleton, [n] 独生子, 独身, (扑克牌)单张。在这里即表示“单例”的意思
 */
public class Singleton {

    private static volatile Singleton instance;

    // 构造器私有化
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

这种写法相比上两种稍微有点复杂:

  1. 首先,instance 字段要用 volatile 关键字修饰,volatile 关键字的作用就是当某个线程修改了 instance 字段的内容后,其他线程能立即看到 instance 字段的最新值;
  2. 单锁锁定的还是类 Singleton;
  3. 双重校验则是在锁外和锁内均对 instance 是否为 null 做判断,以防止刚开始有多个线程跳过了第一层校验然后又都去初始化 instance 字段的问题;
  4. 在 1.4 及更早版本的 Java 中,JVM 对于 volatile 关键字的实现会导致双重检查加锁的失败;

4. 线程安全的懒汉模式(静态类内部加载)

/**
 * Created by clearbug on 2018/3/10.
 *
 * singleton, [n] 独生子, 独身, (扑克牌)单张。在这里即表示“单例”的意思
 */
public class Singleton {

    private static class SingletonHolder {
        public SingletonHolder() {
            System.out.println("SingletonHolder constructor method.");
        }

        private static Singleton instance = new Singleton();
    }

    private Singleton() {
        System.out.println("2. Singleton constructor method.");
    }

    public static Singleton getInstance() {
        System.out.println("1. Singleton getInstance method.");
        return SingletonHolder.instance;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Singleton.getInstance());
    }

}

运行结果:

1. Singleton getInstance method.
2. Singleton constructor method.
Singleton@60e53b93

instance 字段被 SingletonHolder 类包装了,并且只是在 Singleton 调用 getInstance 方法时才进行了初始化!并且这种方式也是线程安全的,为啥说它线程安全呢?我的理解是类中的静态字段、静态代码区的代码都是由 JVM 自身调度执行的,所以不存在用户多线程竞争问题!
以上就是懒汉模式的四种写法了,下面接着说饿汉模式。

5. 线程安全的饿汉模式(静态字段初始化)

/**
 * Created by clearbug on 2018/3/10.
 *
 * singleton, [n] 独生子, 独身, (扑克牌)单张。在这里即表示“单例”的意思
 */
public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
        System.out.println("2. Singleton constructor method.");
    }

    public static Singleton getInstance() {
        System.out.println("1. Singleton getInstance method.");
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
    }

}

运行结果:

2. Singleton constructor method.
1. Singleton getInstance method.
Singleton@60e53b93

instance 字段在 JVM 加载类 Singleton 时就已经执行了初始化过程,所以是线程安全的!只是如果程序运行期根本没有用到这个实例的话,会造成内存浪费!

6. 线程安全的饿汉模式(枚举类)

/**
 * Created by clearbug on 2018/3/10.
 *
 * singleton, [n] 独生子, 独身, (扑克牌)单张。在这里即表示“单例”的意思
 */
public enum  Singleton {

    INSTANCE;

    public void otherMethod() {
        System.out.println("otherMethod");
    }

    public static void main(String[] args) {
        Singleton.INSTANCE.otherMethod();
    }

}

这种方法我之前没咋听说过,看下面参考博客里面 cielosun 这位老铁说:“Effective Java作者Josh Bloch 提倡的方式,在我看来简直是来自神的写法”,听起来很牛逼的样子!
这种方法也是线程安全的,我的理解是枚举类的字段也是 JVM 在加载该类时初始化的,所以也不存在用户多线程竞争问题!而且,由于枚举类的特性,就算是反序列化之后运行的 JVM 中也只有一个实例。
以上就是饿汉模式的两种写法,下面说说单例模式的一些注意点吧:

  1. 序列化和反序列化问题对单例的影响,可以通过修改 readResolve 方法来解决;
  2. 多个类加载器加载对单例的影响?尴尬,忽然想起来我对类加载器的知识也不大熟练就先不说了吧!
  3. 通过反射调用单例类的私有构造方法对单例的影响,可以通过修改构造方法,以便第二次创建实例时抛出异常!

参考

https://www.cnblogs.com/cielosun/p/6582333.html
http://www.blogjava.net/kenzhh/archive/2016/03/28/357824.html
http://www.importnew.com/18872.html

posted @ 2018-03-10 22:15  optor  阅读(323)  评论(0编辑  收藏  举报