单例设计模式的定义
在内存中只有一个对象实例
使用套路
- 构造方法私有化
- 使用静态方法,供外部获取对象的实例
1.饿汉式
HungrySingleton.java
public class HungrySingleton {
private static HungrySingleton mInstance = new HungrySingleton();
/**
* 构造方法私有化
*/
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return mInstance;
}
}
特点: 在类装载的时候,就已经创建实例,而且保证了线程的安全。(使用空间来节约时间,无论有没有使用到该对象,内存中都存在对象的实例)。
2.1懒汉式(存在线程安全问题)
LazySingleton.java
public class LazySingleton {
private static LazySingleton mInstance;
/**
* 构造方法私有化
*/
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == mInstance) {
mInstance = new LazySingleton();
}
return mInstance;
}
}
特点: 在对象被使用的时候才创建实例,但是存在线程安全问题。(使用时间来节约空间,每次进来都要判断实例是否为null,这个判断有一定的开销)
线程安全问题: 单线程时当然不存在任何问题,但是有多个线程进行调用时,就会存在并发问题。如下面一段代码,使用Set数据结构来存放单例的对象。
Set的特点:
-
它不允许出现重复元素;
-
不保证集合中元素的顺序
-
允许包含值为null的元素,但最多只能有一个null元素。
TestDemo.java
public class TestDemo {
public static Set<LazySingleton> singles = new HashSet<>();
public static void main(String[] args) {
SingletonRunnable runnable = new SingletonRunnable();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
new Thread(runnable).start();
...
//等待线程完成
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(singles);
}
private static class SingletonRunnable implements Runnable {
@Override
public void run() {
LazySingleton s = LazySingleton.getInstance();
singles.add(s);
}
}
}
运行的结果:
懒汉线程安全1.png 懒汉线程安全2.png如图所示,图1 的情况是在多次运行的时候出现的,图2 的情况是正常的情况。说明,LazySingleton.java出现了线程并发访问导致的线程安全问题。所以在使用时要加上 synchronized 同步锁进行解决。
2.2懒汉式(解决线程安全问题)
LazySingleton.java
public class LazySingleton {
private static LazySingleton mInstance;
/**
* 构造方法私有化
*/
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (null == mInstance) {
mInstance = new LazySingleton();
}
return mInstance;
}
}
特点: 多线程并发访问的时候,每个线程获取实例都要进行同步锁的判断,效率较低。
2.3懒汉式(解决线程安全问题,效率较高)
LazySingleton.java
public class LazySingleton {
private static LazySingleton mInstance;
/**
* 构造方法私有化
*/
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == mInstance) {
synchronized (LazySingleton.class) {
if (null == mInstance) {
mInstance = new LazySingleton();
}
}
}
return mInstance;
}
}
特点: 当 mInstance 不为空的时候则无须加上同步锁,保证的效率;当 mInstance 为空的时候加上同步锁,并且再次判空。
疑问: 为什么要进行两次判空呢?
只进行单次判空,并且执行 TestDemo.java 的情况如下:
public class LazySingleton {
private static LazySingleton mInstance;
/**
* 构造方法私有化
*/
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == mInstance) {
synchronized (LazySingleton.class) {
mInstance = new LazySingleton();
}
}
return mInstance;
}
}
运行结果:
懒汉线程安全3.png 懒汉线程安全4.png很明显如果少了第二层的 if (null == mInstance) 其实就与没有加 synchronized 关键字的代码是相似的。假设有线程1和线程2两个线程,一同走到了第一层的 null == mInstance ,一同进入的 if 里,在 synchronized (LazySingleton.class) 前排队,虽然线程安全,但是线程1和线程2都会执行 mInstance = new LazySingleton() 对象就不是唯一的了。
2.4懒汉式(加入volatile关键字)
并发编程有三个特性:
- 原子性
- 可见性
- 有序性
volatile:
- 可以提供线程共享变量的可见性(体现了并发编程可见性)
- 禁止指令重排序(体现了并发编程的有序性)
首先,我们先理解一下 mInstance = new LazySingleton();在实例化对象的时候执行了下面三个步骤:
- 给mInstance分配内存
- 使用LazySingleton的构造函数初始化成员变量
- 将mInstance对象指向分配的内存空间(执行完mInstance就为不等于null)
可以看出new对象的操作并非原子操作,所以编译器和处理器会对指令进行重排序。正常的顺应该为1->2->3,但是重排序之后,可能会将顺序变为1->3->2。假设按1->3->2的步骤进行,在线程1的中走到了3这个时候 mInstance != null 了,只是没有执行构造方法还未完成实例化。同时线程2 调用getInstance() 的时候就会返回 mInstance 对象,从而造成不必要的错误。但是如果加入了 volatile 就可以禁止指令重排序,不会出现上面的那种情况。
LazySingleton.java
public class LazySingleton {
private static volatile LazySingleton mInstance;
/**
* 构造方法私有化
*/
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (null == mInstance) {
synchronized (LazySingleton.class) {
if (null == mInstance) {
mInstance = new LazySingleton();
}
}
}
return mInstance;
}
}
3静态内部类(推荐写法)
Singleton.java
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.mInstance;
}
private static class SingletonHolder {
private static final Singleton mInstance = new Singleton();
}
}
特点:这样做既保证了线程安全,也提高了效率,是一个高性能的懒加载。推荐使用该方法来实现单例。
验证例子:
静态内部类1.png 静态内部类2.png如上图所示,实例化对象是在getInstance之后的。所以,通过静态内部类的使用,让JVM来保证线程的安全,减少了锁增加的耗时,并且是一个懒加载的模式。
网友评论