单例模式
为什么需要单例模式
- 如果整个程序运行时有一个实例就够用了,何必创建出来多个实例浪费内存呢;
- 有些保存数据的实例在程序中只有一份即可,有多份的话可能导致数据不一致问题;
实现
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;
}
}
这种写法相比上两种稍微有点复杂:
- 首先,instance 字段要用 volatile 关键字修饰,volatile 关键字的作用就是当某个线程修改了 instance 字段的内容后,其他线程能立即看到 instance 字段的最新值;
- 单锁锁定的还是类 Singleton;
- 双重校验则是在锁外和锁内均对 instance 是否为 null 做判断,以防止刚开始有多个线程跳过了第一层校验然后又都去初始化 instance 字段的问题;
- 在 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 中也只有一个实例。
以上就是饿汉模式的两种写法,下面说说单例模式的一些注意点吧:
- 序列化和反序列化问题对单例的影响,可以通过修改 readResolve 方法来解决;
- 多个类加载器加载对单例的影响?尴尬,忽然想起来我对类加载器的知识也不大熟练就先不说了吧!
- 通过反射调用单例类的私有构造方法对单例的影响,可以通过修改构造方法,以便第二次创建实例时抛出异常!
参考
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