【设计模式系列(二)】彻底搞懂单例模式
文章中涉及到的代码,可到这里来拿:https://gitee.com/daijiyong/DesignPattern
概念:单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点
关键点是:单例类中有一个静态的变量,私有化构造方法,提供唯一全局调用方法,并选择一个时机进行初始化
属于创建型模式
它主要是为了解决:一个全局使用的类频繁地创建与销毁所造成的资源开销
单例模式比较简单,最复杂的地方在于如何保证多线程、序列化等情况下
仍然保证单例实例的唯一性
使用场景:
配置文件,如ServletContext、ServletConfig、ApplicationContext、数据库连接池
要求生产唯一序列号
WEB 中的计数器,不用每次刷新都在数据库中同步一次,可以用单例先缓存起来
1. 饿汉式
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
在类加载的时候实例就初始化了
所以基于 classloader 机制很好的避免了多线程情况下的同步问题
还有一种写法是这样的
public class HungrySingleton {
private static HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
类加载顺序:
先静态后动态、先上后下、先属性后方法
所以这种写法也能满足在类加载的时候就能初始化的要求
懒汉式的优点是:执行效率高,性能高(没有加锁)
缺点:不过不是明确需要初始化这个实例,存在内存浪费的情况
2. 懒汉式单例
为了解决饿汉式中存在的内存浪费的情况
我们可以采用懒汉式
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
懒汉式在类加载的时候不会初始化单例
只有被被外部调用的时候才会创建
下面我们测试一下多线程情况下,这种单例的实现方式能否安全
写一个实现了Runable接口的类
在run方法中调用单例模式的实例
/**
* @author daijiyong
*/
public class TestLazySingletonThread implements Runnable {
public void run() {
System.out.println(Thread.currentThread().getName() + LazySimpleSingleton.getInstance());
}
}
编写主函数,写三个线程进行测试
/**
* @author daijiyong
*/
public class Test {
public static void main(String[] args) {
Thread thread1 = new Thread(new TestLazySingletonThread());
Thread thread2 = new Thread(new TestLazySingletonThread());
Thread thread3 = new Thread(new TestLazySingletonThread());
thread1.start();
thread2.start();
thread3.start();
}
}
显而易见,多线程下并没有保证线程安全
创建了多个单例的对象
其中线程1用的是一个实例
线程0和线程2用的另外一个实例
为什么会出现这个情况呢?
首先我们得知道,Java中线程类的start()方法,并不是会立马执行当前线程
仅仅是告知cpu,你需要执行当前线程,具体什么时候执行,看心情
所以虽然我们是按照thread1、thread2、thread3的顺序执行的start()方法,但是执行顺序却并一定是这样的
而且他们三个的执行顺序可能在不同的时间点,也是不一样的,完全随缘
所以,即便打印出来的两个线程的实例是一样的,也不代表这个单例只被创建了一次
也有可能是创建了两个实例,但是在返回结果之前,第二个实例已经创建完了,将第一个实例覆盖了
怎么优化?第一个想到的应该就是加锁
public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance() {
if (instance == null) {
instance = new LazySimpleSingleton();
}
return instance;
}
}
这样随便执行,就不会出现线程不安全的问题了
但是当我这打了一个断点,查看三个进程执行情况的时候发现了下面的问题
线程0是执行runing状态,但是其他两个线程是监听monitor状态
当执行的线程特别多的时候,会就导致有大量的线程处于等待监听状态
synchronized关键字,是解决了线程安全问题
但是导致在同一个时间只能有一个调用,性能会极速下降
为了解决这个,我们还有一个办法
双重检验方法
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
//是否要线程阻塞
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//是否要创建实例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
第一个检验是为了判断,是否已经初始化了单例
如果已经创建了,则不进行线程阻塞,直接返回
第二个检验是为了判断,在没有初始化的情况下,需要初始化,此时是否因为线程阻塞的原因,已经初始化了
如果已经初始化,则直接返回,如果没有,则进行线程初始化
除了双重检验,还有一个更好的方法来实现懒汉模式
静态内部类
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton() {
//防止通过Java反射机制创建实例
if (LazyHolder.INSTANCE != null) {
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
这个看起来是饿汉式的写法
但是其实是懒汉式的
这个主要是利用了java类加载的时候,默认是不会加载内部类的机制
只有在调用使用内部类的时候才会对内部类实现初始化
classloader 机制保证了初始化 instance 时只有一个线程
这样就很好的解决了线程不安全的问题
优雅、高级、装*
3. 枚举式单例
public enum EnumSingleton {
/**
* 实例
*/
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法
它更简洁,利用枚举类自身特点
不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
为什么呢?
我们可以看一下源码
从Java的源码可以看到
Java在底层就禁止了通过反射的方法创建枚举对象
所以在原生层面就解决了反射机制破坏单例模式的问题
那枚举类是如何保证线程安全的呢?
这个问题问的好,我们在看看源码
由此可知,枚举类在加载的时候
会将每一个枚举类的实例元素放到一个Map当中
这个操作在程序启动、类加载的时候就完成了
之后每次取对象,都是从map中拿的
所以绝对线程安全
但是枚举方式跟饿汉方式一样
是存在内存浪费的情况的
为了解决这个问题,还有一种写法
4. 容器式单例
public class ContainerSingleton {
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(16);
private ContainerSingleton() {
}
public static Object getInstance(String className) {
if (!ioc.containsKey(className)) {
try {
synchronized (ioc) {
Object instance = Class.forName(className).newInstance();
ioc.put(className, instance);
}
} catch (Exception e) {
e.printStackTrace();
}
}
return ioc.get(className);
}
}
将每一个实例都缓存到统一的容器当中,使用唯一标识获取实例
当容器中不存在时,则对容器加锁并创建,之后返回
如果已经存在,则直接返回
使用这种方法可以完美的解决多线程和反射带来的问题
5. ThreadLocal单例
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton() {
}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
ThreadLocal单例能够保证在一个线程内部的全局唯一,天生线程安全
跨线程的时候,保证不是同一个单例实例
这种实现方式的应用场景非常清晰了
用在多线程中,保证在一个线程中的单例实现
6. 总结
单例模式
优点:
减少内存开销
避免对资源的多重占用
设置全局访问点,严格控制访问
缺点:
没有接口,扩展困难,如果要扩展,只能修改代码,违背了开闭原则
文/戴先生@2020年7月2日
---end---
更多精彩推荐