如何写好Java的单例

目录

1 前言

1.1 什么是单例?

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。
Java单例算是设计模式中比较简单,也是开发者们比较熟练的设计模式了。当要实现一个好的Java单例,也需要考虑许多问题,主要是性能和线程安全问题。

1.2 单例实现方式
Java单例的实现可以分为两大类,懒汉式饿汉式,他们的区别在于:
  • 懒汉式:指全局的单例实例在第一次被使用时构建。
  • 饿汉式:指全局的实例在类装载时构建。

2 编程实现

注意:无论是懒汉式还是饿汉式的实现,我们都需要注意的一点就是,必须把类的构造器改为私有的,这样能够防止被外部的类调用。

2.1 懒汉式
2.1.1 简单实现
/**
* 懒汉式单例
**/
public class Singleton {
    private static Singleton instance;

    //把构造器改为私有的,防止被外部的类调用
    private Singleton(){}

    //这个被外部类调用该类实例的唯一方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种写法在当多线程工作的时候,如果有多个线程同时运行到if (instance == null)时,都判断为null,那么这些线程都会各自创建一个实例。这就就违背了单例的宗旨了。

2.1.2 synchronized版本

对应上面提到的多线程安全问题,我们可以使用synchronized来对访问方法进行加锁,实现线程的同步安全。代码实现如下:

/**
* 懒汉式单例
**/
public class Singleton {
    private static Singleton instance;

    //把构造器改为私有的,防止被外部的类调用
    private Singleton(){}

    //这个被外部类调用该类实例的唯一方法
    //使用synchronized加锁实现线程同步安全
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

从上面代码中可以看到,每次多个线程同时访问getInstance()时,都被synchronized同步锁阻塞,同一时间只允许一个线程访问,这对访问程序的性能造成影响。而且我们可以发现,(instance == null)只在第一次访问时,才会出现这种情况,其他时间都不会有线程的安全问题,所以我们可以对上面的代码优化如下:

/**
* 懒汉式单例
**/
public class Singleton {
    private static Singleton instance;

    //把构造器改为私有的,防止被外部的类调用
    private Singleton(){}

    //这个被外部类调用该类实例的唯一方法
    //使用synchronized加锁实现线程同步安全
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); 
                }
            }
        }
        return instance;
    }
}

我们可以注意到上面的代码中有两次if (instance == null)的判断,这个叫做【双重检查 Double-Check】。

  • 第一个if (instance == null),是为了解决synchronized使用导致的效率问题,只有instance为null的时候,才进入synchronized的代码段,这大大减少了进入阻塞的几率。
  • 第二个if (instance == null),则是跟开始时一样,防止可能出现多个实例的情况。

上面的代码看起来已经非常完美了,实际上,还是会有小概率出现问题的。这弄清楚为什么这里可能出现问题,首先我们需要弄清楚几个概念:原子操作、指令重排。

知识点:什么是原子操作

原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。

知识点:什么是指令重排

在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

了解了原子操作和指令重排的概念之后,我们再继续看上面代码的问题。
主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 singleton 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
  3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。

这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作。

2.1.3 终极版本: volatile

解决方案是:只需要给instance的声明加上volatile关键字,即可解决上面提到的原子操作和指令重排导致的问题。

/**
* 懒汉式单例,最终版
**/
public class Singleton {
    //volatile关键字,解决instance的原子操作和指令重排导致的问题
    private static volatile Singleton instance;

    //把构造器改为私有的,防止被外部的类调用
    private Singleton(){}

    //这个被外部类调用该类实例的唯一方法
    //使用synchronized加锁实现线程同步安全
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障。这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

这样懒汉式单例模式编程的问题就都解决了,接下来我们看看饿汉式单例的编程。

2.2 饿汉式

饿汉式单例的实现如下:

/**
* 饿汉式单例
**/
public class Singleton {
    //在类加载时,就进行初始化
    private static final Singleton INSTANCE = new Singleton();

    //把构造器改为私有的,防止被外部的类调用
    private Single(){}

    //这个被外部类调用该类实例的唯一方法
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

对于一个饿汉式单例的写法来说,它基本上是完美的了。

所以它的缺点也就只是饿汉式单例本身的缺点所在了——由于INSTANCE的初始化是在类加载时进行的,而类的加载是由ClassLoader来做的,所以开发者本来对于它初始化的时机就很难去准确把握:

可能由于初始化的太早,造成资源的浪费

如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。

当然,如果所需的单例占用的资源很少,并且也不依赖于其他数据,那么这种实现方式也是很好的。

3 一些其他的实现方式
3.1 Effective Java 1 —— 静态内部类

《Effective Java》一书的第一版中推荐了一个中写法:

/**
* Effective Java 第一版推荐写法
**/
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种写法非常巧妙:

对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。

同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。

简直是神乎其技。

3.2 Effective Java 2 —— 枚举

《Effective Java》的作者在这本书的第二版又推荐了另外一种方法,来直接看代码:

/**
* Effective Java 第二版推荐写法
**/
public enum SingleInstance {
    INSTANCE;
    public void fun1() { 
        // do something
    }
}

// 使用
SingleInstance.INSTANCE.fun1();

看到了么?这是一个枚举类型……连class都不用了,极简。

由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。

4 总结

上面主要讲述Java的单例设计模式的各自使用方案,在开发过程中,我们可以根据需求选择合适的实现方式。

下面是一些有用的参考资料:
深入浅出单实例SINGLETON设计模式:
http://coolshell.cn/articles/265.html
Java并发编程:volatile关键字解析:
http://www.cnblogs.com/dolphin0520/p/3920373.html
为什么volatile不能保证原子性而Atomic可以?:
http://www.cnblogs.com/Mainz/p/3556430.html
类在什么时候加载和初始化?
http://www.importnew.com/6579.html

### 单例模式的实现方式 单例模式是一种常见的软件设计模式,其目的是确保一个类在任何情况下只有一个实,并提供一个全局访问点供外部获取这个唯一的实。这种模式特别适用于那些具有全局状态的场合,如配置管理器、线程池、缓存、对话管理等。 #### 懒汉式(非线程安全) 懒汉式是最基本的实现方式,其特点是延迟加载,即在第一次调用 `getInstance()` 方法时才创建实。这种方式在线程环境下是可行的,但在多线程环境下可能会导致多个实被创建。 ```java public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` 这种方式在多线程环境下是不安全的,可能会导致多个线程同时进入 `if (instance == null)` 判断,从而创建多个实。 #### 懒汉式(线程安全) 为了保证线程安全,可以在 `getInstance()` 方法上加上 `synchronized` 关键字,确保在多线程环境下只有一个线程可以执行该方法。 ```java public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` 这种方式虽然线程安全,但每次调用 `getInstance()` 方法时都需要加锁,性能较差。 #### 饿汉式 饿汉式是单例模式的一种常见实现方式,其特点是类加载时就初始化实,因此是线程安全的。这种方式适用于对性能要求不高的场景。 ```java public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } ``` 这种方式在类加载时就完成了初始化,因此不会出现多线程并发问题,但缺点是不能延迟加载。 #### 双重检查锁定(Double-Checked Locking) 双重检查锁定是一种高效的实现方式,它结合了懒汉式和饿汉式的优点,既保证了线程安全,又实现了延迟加载。 ```java 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; } } ``` 这种方式通过 `volatile` 关键字确保了多线程环境下的可见性,并通过双重检查锁定避免了不必要的同步。 #### 枚举实现 枚举类实现单例模式是极力推荐的实现模式,因为枚举类型是线程安全的,并且只会装载一次。设计者充分地利用了枚举的这个特性来实现单例模式,枚举的法非常简,而且枚举类型是所有实现中唯一一种不会被破坏的实现模式。 ```java public enum Singleton { INSTANCE; public void doSomething() { System.out.println("Singleton instance is working."); } } ``` 枚举实现的优点在于其天然的线程安全性和防止反射攻击的能力。 #### 静态内部类实现 静态内部类实现单例模式是一种较为推荐的方式,它利用了类加载机制来保证线程安全,并且实现了延迟加载。 ```java public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } } ``` 这种方式利用了类加载机制来保证线程安全,并且只有在第一次调用 `getInstance()` 方法时才会加载内部类,从而实现了延迟加载。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值