1、博客内容均出自于咕泡学院架构师第三期
2、架构师系列内容:架构师学习笔记(持续更新)
0、单例模式(Singleton Pattern)
指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局访问点,隐藏其所有的构造方法,这种模式属于创建型模式。
单例的实现主要是通过以下两个步骤:
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
优点:
- 在内存中只有一个对象,节省内存空间;
- 避免频繁的创建销毁对象,可以提高性能;
- 避免对共享资源的多重占用,简化访问;
- 为整个系统提供一个全局访问点;
缺点:
- 不适用于变化频繁的对象;
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
- 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
适用场景:
- 需要生成唯一序列的环境;
- 需要频繁实例化然后销毁的对象;
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
- 方便资源相互通信的环境;
单例模式代码:
- ServletContext
- ServletConfig
- ApplicationContext
- DBPool
单例模式常见写法:
- 饿汉式单例
- 懒汉式单例
- 注册式单例
- ThreadLocal单例
1、饿汉式单例
在单例类首次加载时就创建实例,而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
优点: 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点: 在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
饿汉式单例写法:
package com.jarvisy.demo.pattern.singleton.hungry;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 22:56
* @description :
*/
public class HungrySingleton {
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton() {
}
private static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
package com.jarvisy.demo.pattern.singleton.hungry;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 22:56
* @description :
*/
public class HungryStaticSingleton {
private static final HungryStaticSingleton HUNGRY_SINGLETON;
static {
HUNGRY_SINGLETON = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
private static HungryStaticSingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
2、懒汉式单例
被外部类调用时才创建实例
线程非安全:
缺点: 这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
package com.jarvisy.demo.pattern.singleton.lazy;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:10
* @description :
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy = null;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
这种写法为线程非安全的,验证方法如下:
package com.jarvisy.demo.pattern.singleton;
import com.jarvisy.demo.pattern.singleton.lazy.LazySimpleSingleton;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:37
* @description :
*/
public class ExecutorThread implements Runnable {
@Override
public void run() {
LazySimpleSingleton singleton = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
}
package com.jarvisy.demo.pattern.singleton.lazy;
import com.jarvisy.demo.pattern.singleton.ExecutorThread;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:39
* @description :
*/
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExecutorThread());
Thread t2 = new Thread(new ExecutorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
启动main方法,多跑几次或者打断点调试一下,发现打印的实体可能不是同一个
所以就有了线程安全的写法:
线程安全 :
package com.jarvisy.demo.pattern.singleton.lazy;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:10
* @description :
*/
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy = null;
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance() {
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
此方法在方法上增加了synchronized 虽然说jdk1.6之后对synchronized 性能优化了不少,但是不可能避免的还是存在一定的性能问题,又因为synchronize写在static方法上,可能会造成整个类的死锁。
所以我们又有了 双重检查锁机制(double check singleton)
双重检查锁机制(double check singleton)
使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率
优点:线程安全;延迟加载;效率较高。
package com.jarvisy.demo.pattern.singleton.lazy;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:10
* @description :
*/
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazy == null) {
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
上述代码其实还有一个问题:重排序。
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
- 分配内存给这个对象。
- 初始化对象
- 设置lazy指向刚刚分配的这个内存地址
- 用户初次访问
由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
这样一来,如果进行了重排序,先走了第三步,此时 对象还没有初始化就相当于这个对象没有实际的值。
解决方法:加入volatile可以禁止指令重排。
package com.jarvisy.demo.pattern.singleton.lazy;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:10
* @description :
*/
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazy == null) {
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}
不管是synchronized是加在方法上还是加在代码块上,始终会影响一些性能,所以又有了静态内部类单例。
静态内部类单例
静态内部类单例巧妙的利用了内部类的特性,JVM底层的执行逻辑,完美的避免了线程安全问题。
性能最优的单例
package com.jarvisy.demo.pattern.singleton.lazy;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:10
* @description :
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
}
}
反射,反序列化破坏单例问题
现在我们来看两个问题:
1、虽然我们的构造方法私有了,但是无法逃避反射获取,通过反射破坏单例
代码测试一下:
package com.jarvisy.demo.pattern.singleton.lazy;
import java.lang.reflect.Constructor;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 0:25
* @description :
*/
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try {
//很无聊的情况下,进行破坏
Class<?> clazz = LazyInnerClassSingleton.class;
//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问,强吻,不愿意也要吻
c.setAccessible(true);
//暴力初始化
Object o1 = c.newInstance();
//调用了两次构造方法,相当于new了两次
//犯了原则性问题,
Object o2 = c.newInstance();
System.out.println(o1 == o2);
// Object o2 = c.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时获取的两个对象不是同一个
解决反射获取对象的方法:
在私有构造器里进行判断
package com.jarvisy.demo.pattern.singleton.lazy;
/**
* @author :Jarvisy
* @date :Created in 2020/9/15 23:10
* @description :
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY_INNER_CLASS_SINGLETON != null) {
throw new RuntimeException("不允许构建多个实例");
}
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY_INNER_CLASS_SINGLETON;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY_INNER_CLASS_SINGLETON = new LazyInnerClassSingleton();
}
}
2、序列化反序列好化破坏单例
测试代码:
package com.jarvisy.demo.pattern.singleton.serializable;
import java.io.Serializable;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 0:34
* @description :
*/
//反序列化时导致单例破坏
public class SerializableSingleton implements Serializable {
//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
//内存中状态给永久保存下来了
//反序列化
//讲已经持久化的字节码内容,转换为IO流
//通过IO流的读取,进而将读取的内容转换为Java对象
//在转换过程中会重新创建对象new
public final static SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {
}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
}
package com.jarvisy.demo.pattern.singleton.lazy;
import com.jarvisy.demo.pattern.singleton.serializable.SerializableSingleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 0:34
* @description :
*/
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1 = null;
SerializableSingleton s2 = SerializableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SerializableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决方法:
在单例类里加上
//重写readResolve 方法,只不过是覆盖了反序列化出来的对象,其实还是创建了两次,发生在JVM层面,相对于来说比较安全,之前反序列话的对象会被GC回收
private Object readResolve() {
return INSTANCE;
}
对于一些singleton class,如果你让其implements Serializable,会导致该class不再是singleton。使用ObjectInputStream.readObject()读取进来之后,如果是多次读取,就会创建多个object,下面的代码可以证明这一点,解决的办法之一就是override一个 method,readResolve()
当从流中读取一个对象并准备将其返回给调用者时,调用了readResolve方法.ObjectInputStream检查对象的类是否定义了readResolve方法。如果定义了方法,则调用readResolve方法以允许流中的对象指定要返回的对象。返回的对象应该是与所有用途兼容的类型。如果不兼容,则会在发现类型不匹配时抛出ClassCastException。
readResolve用于替换从流中读取的对象。我见过的唯一用途是强制执行单例;读取对象时,将其替换为单例实例。这确保了没有人可以通过序列化和反序列化单例来创建另一个实例。
具体可以看下面这个帖子:https://zhuanlan.zhihu.com/p/136769959
3、注册式单例
将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。
枚举式单例:
package com.jarvisy.demo.pattern.singleton.register;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 0:54
* @description :
*/
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;
}
}
测试代码:
package com.jarvisy.demo.pattern.singleton.register;
import java.lang.reflect.Constructor;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 0:58
* @description :
*/
public class EnumSingletonTest {
//测试能不能被反序列化破坏
// public static void main(String[] args) {
// try {
// EnumSingleton instance1 = null;
//
// EnumSingleton instance2 = EnumSingleton.getInstance();
// instance2.setData(new Object());
//
// FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
// ObjectOutputStream oos = new ObjectOutputStream(fos);
// oos.writeObject(instance2);
// oos.flush();
// oos.close();
//
// FileInputStream fis = new FileInputStream("EnumSingleton.obj");
// ObjectInputStream ois = new ObjectInputStream(fis);
// instance1 = (EnumSingleton) ois.readObject();
// ois.close();
//
// System.out.println(instance1.getData());
// System.out.println(instance2.getData());
// System.out.println(instance1.getData() == instance2.getData());
//
// }catch (Exception e){
// e.printStackTrace();
// }
// }
// 测试能不能被反射破坏
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("test", 666);
} catch (Exception e) {
e.printStackTrace();
}
}
}
测试结果枚举式单例可避免序列化破坏和反射破坏。
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
容器式单例:
package com.jarvisy.demo.pattern.singleton.register;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:11
* @description :
*/
//Spring中的做法,就是用这种注册式单例
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
synchronized (ioc) {//加锁保证线程安全
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}
发令枪:
package com.jarvisy.demo.pattern.singleton;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:20
* @description :
*/
public class ConcurrentExecutor {
/**
* @param runHandler
* @param executeCount 发起请求总数
* @param concurrentCount 同时并发执行的线程数
* @throws Exception
*/
public static void execute(final RunHandler runHandler, int executeCount, int concurrentCount) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
//控制信号量,此处用于控制并发的线程数
final Semaphore semaphore = new Semaphore(concurrentCount);
//闭锁,可实现计数量递减
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
for (int i = 0; i < executeCount; i++) {
executorService.execute(new Runnable() {
public void run() {
try {
//执行此方法用于获取执行许可,当总计未释放的许可数不超过executeCount时,
//则允许同性,否则线程阻塞等待,知道获取到许可
semaphore.acquire();
runHandler.handler();
//释放许可
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();//线程阻塞,知道闭锁值为0时,阻塞才释放,继续往下执行
executorService.shutdown();
}
public interface RunHandler {
void handler();
}
}
package com.jarvisy.demo.pattern.singleton.register;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:19
* @description :
*/
public class Pojo {
}
测试代码:
package com.jarvisy.demo.pattern.singleton.register;
import com.jarvisy.demo.pattern.singleton.ConcurrentExecutor;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:23
* @description :
*/
public class ContainerSingletonTest {
public static void main(String[] args) {
try {
long start = System.currentTimeMillis();
//测试线程安全
ConcurrentExecutor.execute(new ConcurrentExecutor.RunHandler() {
public void handler() {
Object obj = ContainerSingleton.getInstance("com.jarvisy.demo.pattern.singleton.register.Pojo");
System.out.println(System.currentTimeMillis() + ": " + obj);
}
}, 10, 6);
long end = System.currentTimeMillis();
System.out.println("总耗时:" + (end - start) + " ms.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
容器式单例适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,
4、ThreadLocal 线程单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。
package com.jarvisy.demo.pattern.singleton.threadlocal;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:26
* @description :
*/
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();
}
}
package com.jarvisy.demo.pattern.singleton.threadlocal;
import com.jarvisy.demo.pattern.singleton.ExecutorThread;
/**
* @author :Jarvisy
* @date :Created in 2020/9/16 1:27
* @description :
*/
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExecutorThread());
Thread t2 = new Thread(new ExecutorThread());
t1.start();
t2.start();
System.out.println("End");
}
}