1️⃣概念
定义:保证一个类仅有一个实例,并提供一个全局访问点;
类型:创建型;
2️⃣适用场景
想确保任何情况下都绝对只有一个实例;
3️⃣优点
在内存中只有一个实例,减少了内存开销;
可以避免对资源的多重占用;
设置了全局访问点,严格控制访问;
4️⃣缺点
没有接口,扩展困难;
5️⃣重点
①私有构造器
②线程安全
③延迟加载
④序列化和反序列化安全
⑤反射
6️⃣单例模式Coding(懒汉式)
①懒汉式简单版实现
public class LazySingletonV1 {
private static LazySingleton lazySingleton = null;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
这个版本的实现会在多线程环境中出现问题可以看到通过使用IDEA模拟多线程的情况,我们拿到了两个不一样的对象.
②实现线程安全的懒汉式
public class LazySingletonV2 {
private static LazySingleton lazySingleton = null;
private LazySingleton(){}
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
通过使用synchronized同步锁来实现懒汉式单例的线程安全是一种较为普遍的解决方案,但是此方案也有一定的局限; synchronized修饰static方法其实是锁的整个class文件,因为同步锁有上锁和解锁的开销所以此解决方案会存在性能开销过大的问题;
③DoubleCheck双重检查实现懒汉式
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
虽然这种方式兼顾了性能和安全同时也满足懒加载的情况,但是这种情况也有一定的缺陷,首先通过synchronized我们保证了多线程情况下只有一个线程可以创建对象,如果对象已经被创建则直接返回不需要在进行加锁的操作,避免了性能的开销;但是根据java规范intra-thread semantics我们知道单线程在执行操作的时候有可能会出现指令重排序的问题,指令重排序不会影响单线程的结果,如果放在多线程的情况下就会出现问题;如上图所示,在多线程环境下,由于线程0并没有初始化完成对象,但是线程1已经将此对象判断为非空,也就是说线程1拿到的其实是线程0正在进行初始化的对象,在这样的情况下系统就会报异常了;
④通过volatile关键字禁止DoubleCheck双重检查指令重排序的问题;
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
通过volatile关键字我们可以禁止掉指令重排序,从而解决了多线程情况下的指令重排序问题;volatile关键字主要使用的是缓存一致性协议,有兴趣的小伙伴可以深入研究一下,这里不做深入的解释;
⑤通过静态内部类实现对其他线程屏蔽指令重排序
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){}
}
}
这种解决方案实际上是基于类初始化的延迟加载解决方案,jvm在初始化类的时候会获取一个锁,这个锁会同步多个线程对一个类的初始化
7️⃣单例模式Coding(饿汉式)
①饿汉式简单实现
public class HungrySingleton{
private final static HungrySingleton hungrySingleton;
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
优点:类加载时即创建,避免了线程安全问题;
缺点:可能会导致内存的浪费;
②序列化破坏单例模式原理解析及解决方案
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
public class Test {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
从上边的测试中,我们可以看出通过序列化和反序列化我们得到了两个不同的对象,这样就违背了单例的初衷;接下来我们就解决这个问题;
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
针对这个问题,我们就需要去readObject()方法中去看一下了(由于源码调用层次较深,这里不做演示,有兴趣的小伙伴可以自己尝试一下)通过看源码我们了解到底层是通过反射来创建的对象,既然是通过反射来创建的对象那么可能和原对象是不一致,这也就解释了为什么第一次比较的时候为false了;那么为什么加上了readResolve方法就能解决这个问题呢?通过继续看源码我们找到了答案,在反射的时候jdk会确认被反射的类有没有readResolve()方法,如果有则返回true;如果结果为true会通过反射调用被反射类的readResolve()方法,然后readResolve()方法会返回我们创建好的实例对象,这样就实现两个对象比较结果为true的情况了;
③单例模式反射攻击的解决方案
public class Test {
public static void main(String[] args) throws Exception {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();
StaticInnerClassSingleton newInstance = (StaticInnerClassSingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
我们可以看到通过反射我们依然可以得到两个对象,那我们该怎么解决这样的问题呢请往下看
public class HungrySingleton implements Serializable{
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
这样就可以解决这个问题了;但是这种解决方案只适用于非延时加载的单例模式,如果是延时加载的单例模式我们依旧可以通过反射来创建,那么有没有既能保证单例不被序列化破坏又能禁止反射创建的单例模式呢?
8️⃣单例模式的其他实现
①Enum枚举单例
public enum EnumInstance {
INSTANCE{
protected void printTest(){
System.out.println("Enum Print Test");
}
};
protected abstract void printTest();
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
序列化验证
public class Test {
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}
反射验证
public class Test {
public static void main(String[] args) throws Exception {
Class objectClass = EnumInstance.class;
Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance instance = (EnumInstance) constructor.newInstance("测试",666);
}
}
可以看到枚举单例可以完美的解决上述的问题;
②基于容器的单例模式
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> singletonMap = new HashMap<String,Object>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
容器单例模式并不是线程安全的,不过这种单例模式也是有应用场景的当一个程序中单例比较多时,可以使用这样的模式进行统一管理;
③ThreadLocal线程单例
这种单例并不能保证全局唯一,但是可以保证线程唯一
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
= new ThreadLocal<ThreadLocalInstance>(){
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance(){}
public static ThreadLocalInstance getInstance(){
return threadLocalInstanceThreadLocal.get();
}
}
public class T implements Runnable {
@Override
public void run() {
ThreadLocalInstance instance = ThreadLocalInstance.getInstance();
System.out.println(Thread.currentThread().getName()+" "+instance);
}
}
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println("main thread"+ThreadLocalInstance.getInstance());
System.out.println("main thread"+ThreadLocalInstance.getInstance());
System.out.println("main thread"+ThreadLocalInstance.getInstance());
System.out.println("main thread"+ThreadLocalInstance.getInstance());
System.out.println("main thread"+ThreadLocalInstance.getInstance());
System.out.println("main thread"+ThreadLocalInstance.getInstance());
}
}
9️⃣单例模式的应用
①单例模式在JDK中的应用
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
......
}