单例模式
1. 简介
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
- 单例的构造函数为私有
- 提供一个自身的静态私有成员变量;
- 提供一个公有的静态工厂方法
2. 优缺点
- 优点:
由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。 - 缺点:
由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
3. 实现的几种方式
3.1 饿汉式
类一加载就完成初始化
public class HungerSingleton {
public HungerSingleton(){
}
private static final HungerSingleton singleton=new HungerSingleton();
public static HungerSingleton getInstance() {
return singleton;
}
@Test
public void test(){
HungerSingleton hungerSingleton= HungerSingleton.getInstance();
HungerSingleton hungerSingleton1= HungerSingleton.getInstance();
System.out.println(hungerSingleton.equals(hungerSingleton1));// true
}
}
缺点:太浪费内存空间了
3.2 懒汉式
public class LazySingleton {
private LazySingleton(){
System.out.println(Thread.currentThread().getName()+"ok");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static LazySingleton lazySingleton;
public static LazySingleton getInstance(){
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
@Test
public void test(){
for(int i=0;i<10;i++){
new Thread(()->{
LazySingleton.getInstance();
}).start();
}
}
}
懒汉式,在需要使用的时候才完成加载;但是这个是线程不安全的,多线程的情况下有问题
3.3 懒汉式加锁+双重检测
public class LazySingletonLock {
private LazySingletonLock(){
System.out.println(Thread.currentThread().getName()+"ok");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private volatile static LazySingletonLock lazySingleton;
// private static LazySingletonLock lazySingleton;
public static LazySingletonLock getInstance(){
if (lazySingleton == null) {
synchronized (LazySingleton.class){
if(lazySingleton==null){
lazySingleton = new LazySingletonLock();
}
}
}
return lazySingleton;
}
@Test
public void test(){
for(int i=0;i<10;i++){
new Thread(()->{
LazySingletonLock.getInstance();
}).start();
}
}
}
线程安全、延迟加载、加锁后效率较低
LazySingletonLock实例需要加锁volatile关键字,避免指令重排,否则还是有可能会出错
3.4 静态内部类实现
public class InnerclassSingleton {
private InnerclassSingleton() { }
private static class InnerclassSingletonInstance {
private static final InnerclassSingleton INSTANCE = new InnerclassSingleton();
}
public static InnerclassSingleton getInstance() {
return InnerclassSingletonInstance.INSTANCE;
}
}
使用类装载的机制来保证初始化实例时只有一个线程。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
只有在使用的时候(getInstance())才会实例化。
9.5 反射破坏单例模式
public class LazySingletonLock {
private LazySingletonLock(){
System.out.println(Thread.currentThread().getName()+"ok");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static LazySingletonLock lazySingleton;
public static LazySingletonLock getInstance(){
if (lazySingleton == null) {
synchronized (LazySingleton.class){
if(lazySingleton==null){
lazySingleton = new LazySingletonLock();
}
}
}
return lazySingleton;
}
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazySingletonLock instance = LazySingletonLock.getInstance();
Constructor<LazySingletonLock> declaredConstructor = LazySingletonLock.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);// 可以防伪私有变量
LazySingletonLock instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
}
}
私有变量是不安全的,可以通过反射来破坏单例性
9.6 避免反射破坏
public class LazySingletonLock {
private LazySingletonLock(){
synchronized (LazySingleton.class){
if(lazySingleton!=null){
throw new RuntimeException("不要用反射来破环单例");
}
}
System.out.println(Thread.currentThread().getName()+"ok");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static LazySingletonLock lazySingleton;
public static LazySingletonLock getInstance(){
if (lazySingleton == null) {
synchronized (LazySingleton.class){
if(lazySingleton==null){
lazySingleton = new LazySingletonLock();
}
}
}
return lazySingleton;
}
@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazySingletonLock instance = LazySingletonLock.getInstance();
Constructor<LazySingletonLock> declaredConstructor = LazySingletonLock.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);// 可以防伪私有变量
LazySingletonLock instance1 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1);
// 通过反射创建两个实例,还是会破坏单例
LazySingletonLock instance3 = declaredConstructor.newInstance();
LazySingletonLock instance4 = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(instance1); // 两个实例是不同的
}
}
还是不能避免反射的破话,如果直接从反射获取两个实例,还是会出问题,这时候可以通过一个标志位来判断是否创建了实例;但是这个标识位通过反编译还是可以获取到。
9.7 通过枚举来实现单例
枚举是不能通过反射来创建的。
enum SingletonEnum {
INSTANCE("name","ljh"),
;
private String key;
private String value;
SingletonEnum(String key,String value){
this.key=key;
this.value=value;
}
// 声明单例对象
private SingletonEnum getInstance() {
return INSTANCE;
}
}
public class EnumSingleton {
// 枚举类型是线程安全的,并且只会装载一次
@Test
public void test() throws Exception {
SingletonEnum instance = SingletonEnum.INSTANCE;
SingletonEnum instance1 = SingletonEnum.INSTANCE;
System.out.println(instance);
System.out.println(instance1);
// 尝试反射破解
// 查看输出的class文件发现是无参数构造器, java.lang.NoSuchMethodException:
// 不是期望的异常,用命令行javap反编译这个类发现还是无参数构造器
// 用jad工具反编译,反现有两个参数string,int; ava.lang.IllegalArgumentException: Cannot reflectively create enum objects
// 得到想要的异常。
Constructor<SingletonEnum> declaredConstructor = SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
SingletonEnum singletonEnum = declaredConstructor.newInstance();
SingletonEnum singletonEnum1 = declaredConstructor.newInstance();
System.out.println(singletonEnum);
System.out.println(singletonEnum1);
}
}
- 为什么使用volatile 修饰了singleton 引用还用synchronized 锁?
volatile 只保证了共享变量 singleton 的可见性,但是 singleton = new Singleton(); 这个操作不是原子的,所以需要加锁。 创建实例的三步操作:
- 为 singleton 分配内存空间
- 初始化 singleton
- 将 singleton 指向分配的内存地址
因为JVM会进行指令重排,所以这三步的顺序可能会被打乱。
- 第一次检查singleton 为空后为什么内部还进行第二次检查?
A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。