单例模式笔记-设计模式学习

188 阅读5分钟

单例模式分析

  • 一个单例类只能有一个实例
  • 单例类必须自行创建这个实例
  • 单例类必须保证全局其他对象都能唯一访问到它

单例模式的对象职责

  • 保证一个类只有一个实例
  • 为该实例提供一个全局访问节点

为什么使用单例模式

系统资源有限

  • 控制某些共享资源(例如,数据库或文件)的访问权限
  • 同时读写同一个超大的AI模型文件,或使用外部进程式服务

需要表示为全局唯一的对象

  • 系统要求提供一个唯一的序列号生成器

使用单例模式的优势

  • 对有限资源的合理利用,保护有限的资源,防止资源重复竟抢
  • 更高内聚的代码组件,能提升代码复用性
  • 具备全局唯一访问点的权限控制,方便按照统一规则管控权限
  • 从负载均衡角度考虑,可以轻松地将 Singleton 扩展成两个、三个或更多实例

由于封装了基数问题,所以在适当的时候可以自由更改实例的数量

使用单例模式的劣势

  • 作为全局变量使用时,引用的对象越多,代码修改影响的范围也越大
  • 作为全局变量时,在全局变量中使用状态变量时,会造成加/解锁的性能损耗
  • 即便能扩展多实例,但耦合性依然很高,因为隐蔽了不同对象之间的调用关系
  • 不支持有参数的构造函数

常见的单例模式应用和使用的解决方案

编码实现

饿汉式初始化

饿汉式初始化区别于字面意思中饿汉对食物(资源)的迫切需求,它在开始时无论是否需要/使用这个资源,在类加载时将其初始化。更像是悲观的、贪婪的,在使用之前准备好所需资源。

饿汉式初始化带来的弊端也很明显,程序起初并不需要这些资源。当初始化资源过多时,这些无用资源将会占用系统大量空间,不利于系统运行效率。

package com.designpatternsdemo.singleton;

/**
 * <h1>饿汉式单例模式</h1>
 *
 * @author L
 */
public class HungrySingleton{

	private static HungrySingleton hungrySingleton = new HungrySingleton();

	private HungrySingleton(){};

	public static HungrySingleton getHungrySingleton(){
		return hungrySingleton;
	}
}

懒汉式初始化

懒汉式和饿汉式的区别在于资源初始化时间的不同,饿汉式在程序运行时初始化资源。懒汉式在即将调用时初始化资源。

package com.designpatternsdemo.singleton;

/**
 * <h1>懒汉式初始化</h1>
 *
 * @author L
 */
public class LazyInitialization{
	private static LazyInitialization lazyInitialization;

	private LazyInitialization(){
	}
	
	public static LazyInitialization getLazyInitialization(){
		if(lazyInitialization == null){
			//首次调用初始化变量
			lazyInitialization = new LazyInitialization();
		}
		return lazyInitialization;
	}
}

编写完成后,我们通过多线程测试其是否线程安全

package com.designpatternsdemo.singleton;

/**
 * <h1>主程序类</h1>
 *
 * @author L
 */
public class Main{

	public static void main(String[] args){
		Thread thread1 = new Thread(() -> System.out.println(LazyInitialization.getLazyInitialization())
		);

		Thread thread2 = new Thread(() -> System.out.println(LazyInitialization.getLazyInitialization())
		);

		Thread thread3 = new Thread(() -> {
			try{
				//将thread3睡眠200毫秒,等待上一线程初始化完成
				Thread.sleep(200);
				System.out.println(LazyInitialization.getLazyInitialization());
			} catch(InterruptedException e){
				throw new RuntimeException(e);
			}
		});

		thread1.start();
		thread2.start();

		thread3.start();
	}

}
/*
控制台打印信息:
已连接到目标 VM, 地址: ''127.0.0.1:11500',传输: '套接字''
com.designpatternsdemo.singleton.LazyInitialization@b16e737
com.designpatternsdemo.singleton.LazyInitialization@6334f86d
com.designpatternsdemo.singleton.LazyInitialization@6334f86d
与目标 VM 断开连接, 地址为: ''127.0.0.1:11500',传输: '套接字''
*/

我们通过控制台打印信息可以看到,在 thread1thread2 两个线程同时访问变量时,分别拿到了不同的变量值。

这是因为在创建变量时

public static LazyInitialization getLazyInitialization(){
		if(lazyInitialization == null){
			//首次调用初始化变量
			lazyInitialization = new LazyInitialization();
		}
		return lazyInitialization;
	}

第一个线程在来到 if 时进入创建变量,当还未创建完成时,lazyInitialization 此时依旧为空,但是 thread2 已经进入 if 判断,此时就有了两次赋值,且 thread1 拿到的数值被后续线程覆盖,成为错误数据。

以下为改进代码:

package com.designpatternsdemo.singleton;

/**
 * <h1>懒汉式初始化</h1>
 *
 * @author L
 */
public class LazyInitialization{
	private static LazyInitialization lazyInitialization;

	private LazyInitialization(){
	}

	public static synchronized LazyInitialization getLazyInitialization(){
		if(lazyInitialization == null){
			//首次调用初始化变量
			lazyInitialization = new LazyInitialization();
		}
		return lazyInitialization;
	}
}

/*
已连接到目标 VM, 地址: ''127.0.0.1:13636',传输: '套接字''
com.designpatternsdemo.singleton.LazyInitialization@1297b437
com.designpatternsdemo.singleton.LazyInitialization@1297b437
com.designpatternsdemo.singleton.LazyInitialization@1297b437
与目标 VM 断开连接, 地址为: ''127.0.0.1:13636',传输: '套接字''
*/

双重校验锁模式

经过高并发测试可以发现,由于synchronized的互斥性每次只能由一个线程获取值,多个线程同时获取时,访问速度大大降低。经过需求分析,只在初始化赋值时多线程访问才会出现问题,初始化后获取变量值时,是不必要对变量设置访问限制的。于是就有了双重校验锁模式,即能保证线程安全,又可以保证系统访问效率

package com.designpatternsdemo.singleton;

/**
 * <h1>双重校验锁模式</h1>
 * DCL,即 double-checked locking
 *
 * @author L
 */
public class DCL{

	private static DCL dcl;

	private DCL(){
	}

	public static DCL getDcl(){
		if(dcl == null){
			synchronized(DCL.class){
				if(dcl == null){
					//首次调用初始化变量
					dcl = new DCL();
				}
			}
		}
		return dcl;
	}
}

使用ThreadLocal实现

实现代码:

package com.designpatternsdemo.singleton;

/**
 * <h1>使用ThreadLocal实现</h1>
 *
 * @author L
 */
public class ThreadLocalAccomplish{
	private static boolean signal;

	public static ThreadLocal<ThreadLocalAccomplish> accomplishThreadLocal = ThreadLocal.withInitial(() -> new ThreadLocalAccomplish(true));

	/*
	public static ThreadLocal<ThreadLocalAccomplish> accomplishThreadLocal = new ThreadLocal<ThreadLocalAccomplish>(){
		@Override
		protected ThreadLocalAccomplish initialValue (){
			return new ThreadLocalAccomplish();
		}
	};
	*/
	private ThreadLocalAccomplish(){
	}

	private ThreadLocalAccomplish(boolean signal){
		ThreadLocalAccomplish.signal = signal;
		//初始化时使test+1
		//ThreadLocalAccomplish.signal=!ThreadLocalAccomplish.signal;
	}

	public static ThreadLocalAccomplish getAccomplishThreadLocal(){
		return accomplishThreadLocal.get();
	}

	public static Boolean getSignal(){
		return signal;
	}

	public static void setSignal(Boolean signal){
		ThreadLocalAccomplish.signal = signal;
	}
}

主程序代码:

package com.designpatternsdemo.singleton;

/**
 * <h1>主程序类</h1>
 *
 * @author L
 */
public class Main{

	public static void main(String[] args){
		Thread thread1 = new Thread(() -> {
			System.out.println("thread1:" + ThreadLocalAccomplish.getAccomplishThreadLocal());
			System.out.println("thread1:" + ThreadLocalAccomplish.getSignal());
		});

		Thread thread2 = new Thread(() -> {
			System.out.println("thread2:" + ThreadLocalAccomplish.getAccomplishThreadLocal());
			System.out.println("thread2:" + ThreadLocalAccomplish.getSignal());
			ThreadLocalAccomplish.setSignal(false);
			System.out.println("thread2:" + ThreadLocalAccomplish.getSignal());
		});

		Thread thread3 = new Thread(() -> {
			try{
				Thread.sleep(200);
			} catch(InterruptedException e){
				throw new RuntimeException(e);
			}
			System.out.println("thread3:" + ThreadLocalAccomplish.getAccomplishThreadLocal());
			System.out.println("thread3:" + ThreadLocalAccomplish.getSignal());
		});

		thread1.start();
		thread2.start();
		thread3.start();
	}
}
/*
已连接到目标 VM, 地址: ''127.0.0.1:4080',传输: '套接字''
thread1:com.designpatternsdemo.singleton.ThreadLocalAccomplish@669a3b49
thread1:true
thread2:com.designpatternsdemo.singleton.ThreadLocalAccomplish@38e66a6e
thread2:true
thread2:false
thread3:com.designpatternsdemo.singleton.ThreadLocalAccomplish@36b46f86
thread3:true
与目标 VM 断开连接, 地址为: ''127.0.0.1:4080',传输: '套接字''

进程已结束,退出代码0
*/

可以发现ThreadLocal为每个变量分别提供了独立的变量副本,线程之间可以做到同时访问互不影响。从而解决了多线程之间访问数据冲突的问题。同时也省去了线程之间的同步操作。