文章目录
- 设计模式之单例模式
- 定义
- 特点
- 优缺点
- 优点
- 缺点
- 结构与实现
- 结构
- 实现
- 懒汉式单例
- 1. 懒汉式单例(线程不安全)
- 单线程调用
- 多线程调用
- 2. 懒汉式单例(线程安全)
- 3. 懒汉式单例(双重校验锁)
- `volatile`和`synchronized`的区别
- 4. 懒汉式单例(静态内部类 推荐*)
- 饿汉式单例
- 1. 饿汉式单例(静态成员变量)
- 2. 饿汉式单例(静态代码块)
- 3. 饿汉式单例(枚举类 推荐*)
- 总结
- 破坏单例模式
- 序列化反序列化方式破坏
- 序列化反序列化方式破坏的解决方案
- 源码查看
- 反射破坏
- 反射破坏的解决方案
- 实例
- 1. JDK源码中的单例模式
- 2.Spring源码中的单例模式
- 示例
- 结论
- Spring的bean注入是懒汉式单例还是饿汉式单例?
- 扩展
- 多例实现
- `@Scope`有哪些
设计模式之单例模式
定义
一个类只有一个实例,该类提供一个全局的访问点供外部获取该实例。
如windows只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费。
特点
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
优缺点
优点
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
缺点
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
结构与实现
一般地,普通类的构造函数是公有的,外部类可以通过**new 构造函数()
**来生成多个实例。
使用Spring之前就是通过new来进行调用的。
如果将类的构造函数设为私有的,外部类就无法调用该构造函数,更无法生成多个实例。
这时,该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
结构
主要角色有:单例类和访问类。
- 单例类:包含一个实例并且能自行创建这个实例的类。
- 访问类:使用单例的类。
实现
单例模式分为两种:懒汉式和饿汉式。
懒汉式是指:首次使用才会进行该实例对象的创建。
饿汉式是指:类加载时该单实例对象就会被创建。
懒汉式单例
1. 懒汉式单例(线程不安全)
public class Singleton {
//私有构造方法
private Singleton() {
}
//声明变量
private static Singleton instance;
//外部调用方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
单线程调用
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
}
在进行第一次调用时,会去new一个实例,第二次则直接可以获取到上一次new的实例。
输出结果:
com.ssw.config.thread.Singleton@59a6e353
com.ssw.config.thread.Singleton@59a6e353
com.ssw.config.thread.Singleton@59a6e353
上面的代码在单线程情况下是没有问题的,当多个线程并行调用getInstance()
时,就会创建多个实例。
多线程调用
多线程异步调用,肯定不能使用main方法来测试,这里我新写一个接口,然后通过地址进行调用。
新建一个TestService
类,里面有t1、t2、t3方法,分别调用getInstance()
方法,输出实例。
3个方法改为异步执行。
@Service
public class TestService {
private static final Logger log = LoggerFactory.getLogger(TestService.class);
@Async
public void t1() {
log.error("t1实例:{}", Singleton.getInstance());
}
@Async
public void t2() {
log.error("t2实例:{}", Singleton.getInstance());
}
@Async
public void t3() {
log.error("t3实例:{}", Singleton.getInstance());
}
}
控制层,注入TestService
类,写一个地址为/a1
的方法,调用t1、t2、t3。
@Autowired
private TestService testService;
@PostMapping("/a1")
public void a1() {
testService.t1();
testService.t2();
testService.t3();
}
输出结果:
2021-12-30 10:47:09 ERROR c.s.c.b.c.TestService.t1(20): t1实例:com.ssw.config.thread.Singleton@7a15471c
2021-12-30 10:47:09 ERROR c.s.c.b.c.TestService.t3(30): t3实例:com.ssw.config.thread.Singleton@423eaa58
2021-12-30 10:47:09 ERROR c.s.c.b.c.TestService.t2(25): t2实例:com.ssw.config.thread.Singleton@37cb9672
可以看到每个实例都是不一样的。
这属于是并发问题,我本来需要的是一个结果,现在变成了3个结果。
问题:再执行/a1
方法,结果是什么?
2. 懒汉式单例(线程安全)
为了解决上面线程不安全的问题,最简单的方法是将整个getInstance()
方法设置为同步(synchronized
)
public class Singleton {
private static Singleton instance;
//私有构造方法
private Singleton() {
}
//外部调用方法
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
只是在getInstance()
上增加了synchronized
。
重启项目,调用/a1
方法,查看结果:
2021-12-30 10:55:53 ERROR c.s.c.b.c.TestService.t2(25): t2实例:com.ssw.config.thread.Singleton@651dea56
2021-12-30 10:55:53 ERROR c.s.c.b.c.TestService.t1(20): t1实例:com.ssw.config.thread.Singleton@651dea56
2021-12-30 10:55:53 ERROR c.s.c.b.c.TestService.t3(30): t3实例:com.ssw.config.thread.Singleton@651dea56
可以看出,设置为同步就解决多线程会创建多个实例的问题。
3. 懒汉式单例(双重校验锁)
上面增加了synchronized
同步,解决了多线程情况下创建了多个实例的问题,但是效率不高,因为在调用getInstance()
方法时,只能有一个线程调用,其他线程必须等待。
但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。(就是我第一次调用进行创建,后面的调用我不需要再进行同步操作,直接返回结果就好了),没必要让每个线程必须持有锁才能进行操作。
这就引出了双重检验锁。
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。
之所以称其为双重检查锁,因为会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。
为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
//第一次检验
if (instance == null) {
synchronized (Singleton.class) {
//第二次检验
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这段代码看起来很完美,但是instance = new Singleton()
会存在问题。
问题:在多线程情况下可能会出现空指针问题。
这句话在jvm中做了3件事情:
- 给
instance
分配内存 - 调用
Singleton
的构造函数来初始化成员变量 - 将
instance
对象指向分配的内存空间(执行完这步instance
就为非null
了)
在JVM的即时编译器中存在指令重排序的优化,就是说第二步和第三步的顺序不能保证,有可能执行的顺序是1-2-3或者1-3-2。
如果是后者,3执行完毕,2未执行之前,被线程2抢占了,这时instance
已经非null
了(但没有进行初始化),线程2会直接返回instance
。
我们只需要将instance
变量声明成volatile
就可以了。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
//第一次检验
if (instance == null) {
synchronized (Singleton.class) {
//第二次检验
if (instance == null) {
System.out.println("执行new");
instance = new Singleton();
}
}
}
return instance;
}
}
使用volatile
的主要原因是它有一个特性:禁止指令重排优化,保证可见性和有序性。
volatile
和synchronized
的区别
volatile
告诉jvm当前变量在寄存区中的值是不确定的,需要从主内存中读取。synchronized
是锁定当前变量,只有当前线程可以访问该变量,其他线程处于阻塞状态。volatile
仅能使用在变量级别;synchronized
可以使用在变量、方法和类级别中。volatile
仅能实现变量的修改可见性,不能保证原子性;而synchronized
则可以保证变量的修改可见性和原子性volatile
不会造成线程的阻塞;synchronized
可能会造成线程的阻塞。volatile
标记的变量不会被编译器优化;synchronized
标记的变量可以被编译器优化
4. 懒汉式单例(静态内部类 推荐*)
静态内部类单例模式实例由内部类创建,由于JVM
在加载外部类的过程中,不会加载静态内部类,只有内部类的方法/属性被调用时才会被加载,静态属性被static
修饰,保证只被实例化一次,并且严格保证实例化顺序,解决了指令重排序的问题。
public class Singleton {
//私有构造方法
private Singleton() {
}
//定义静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//提供公共访问
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
输出结果:
2021-12-30 11:50:39 ERROR c.s.c.b.c.TestService.t2(25): t2实例:com.ssw.config.thread.Singleton@598414b8
2021-12-30 11:50:39 ERROR c.s.c.b.c.TestService.t3(30): t3实例:com.ssw.config.thread.Singleton@598414b8
2021-12-30 11:50:39 ERROR c.s.c.b.c.TestService.t1(20): t1实例:com.ssw.config.thread.Singleton@598414b8
静态内部类使用了JVM
本身机制保证了线程安全问题;
SingletonHolder
是私有的,除了getInstance()
方法之外没有办法可以访问它,所以它是懒汉式的;
饿汉式单例
1. 饿汉式单例(静态成员变量)
特点是类一旦加载就创建一个单例,保证在调用getInstance
方法之前单例就存在了。
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程也不会出现问题。
但这种方式不属于懒加载,因为只要程序启动,不管是否使用都会进行创建。
2. 饿汉式单例(静态代码块)
public class Singleton {
//私有构造函数
private Singleton() {
}
private static Singleton instance;
//静态代码块
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
}
3. 饿汉式单例(枚举类 推荐*)
创建枚举默认是线程安全的,还能防止反序列化导致重新创建新建的对象。
public enum SingletonEnum {
INSTANCE;
}
调用方:
public static void main(String[] args) {
SingletonEnum singletonEnum = SingletonEnum.INSTANCE;
SingletonEnum singletonEnum1 = SingletonEnum.INSTANCE;
System.out.println(singletonEnum == singletonEnum1);
}
返回结果:
true
总结
-
在不考虑浪费内存空间的情况下,推荐使用饿汉式的枚举方式。
-
在需要时才进行加载时,推荐使用懒汉式的静态内部类方式。
破坏单例模式
破坏单例模式是指:使上面定义的单例类可以创建多个实例,枚举方式除外。
破坏的方式有两种:序列化反序列化破坏和反射破坏。
序列化反序列化方式破坏
示例:
如我们使用静态内部类方式
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {
}
//定义静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//提供公共访问
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
定义将获取的实例写入文件中
/**
* 向文件中写数据(对象)
* @throws Exception
*/
public static void wirte() throws Exception {
//1.获取对象
Singleton singleton = Singleton.getInstance();
//2.创建对象输出流对象
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\a.txt"));
//3.写对象
objectOutputStream.writeObject(singleton);
//4.释放对象
objectOutputStream.close();
}
读取文件中的实例
/**
* 从文件读取数据(对象)
* @throws Exception
*/
public static void read() throws Exception {
//1.创建对象输入流对象
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\a.txt"));
//2.读取对象
Singleton singleton = (Singleton) objectInputStream.readObject();
System.out.println(singleton);
//释放资源
objectInputStream.close();
}
调用方:
先调用写方法,再进行两次读取,看结果是否一致。
public static void main(String[] args) throws Exception {
//1.写文件
wirte();
//2.读文件
read();
read();
}
输出结果:
com.ssw.config.thread.Singleton@239963d8
com.ssw.config.thread.Singleton@3abbfa04
序列化反序列化方式破坏的解决方案
序列化、反序列化方式破坏单例模式的解决方法。
在Singleton
类中添加readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,没有定义则返回新new 出来的对象。
静态内部类增加readResolve()
方法
//当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回。
public Object readResolve() {
return SingletonHolder.INSTANCE;
}
再次进行一次写、两次读操作。
输出结果:
com.ssw.config.thread.Singleton@36f6e879
com.ssw.config.thread.Singleton@36f6e879
问题解决。
源码查看
Singleton singleton = (Singleton) objectInputStream.readObject();
查看readObject()
方法,
readObject0()
方法,tc
=TC_OBJECT
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
readOrdinaryObject()
方法中
如果有readResolve
方法,则执行readResolve
方法。
反射破坏
示例:
还是用静态内部类
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {
}
//定义静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//提供公共访问
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
调用方:
public static void main(String[] args) throws Exception {
//获取字节码对象
Class<Singleton> singletonClass = Singleton.class;
//获取无参构造对象
Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
//取消访问检查
constructor.setAccessible(true);
//创建对象
Singleton s1 = constructor.newInstance();
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2);
}
创建了两个对象,调用结果:
false
反射破坏的解决方案
反射是通过获取无参构造对象,来进行创建对象。
所以静态内部类,需要修改构造方法
增加标识符,,当第一次创建时设置为true,否则抛出运行时异常。
private static boolean flag = false;
//私有构造方法
private Singleton() {
synchronized (Singleton.class) {
//如果为ture,说明已经创建过。抛出运行时异常。
if (flag) {
throw new RuntimeException("不能创建多个对象");
}
//如果为false,第一次创建,设置flag=true
flag = true;
}
}
静态内部类完整代码
public class Singleton implements Serializable {
private static boolean flag = false;
//私有构造方法
private Singleton() {
synchronized (Singleton.class) {
//如果为ture,说明已经创建过。抛出运行时异常。
if (flag) {
throw new RuntimeException("不能创建多个对象");
}
//如果为false,第一次创建,设置flag=true
flag = true;
}
}
//定义静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//提供公共访问
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
调用结果:
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.ssw.core.blog.controller.Test2.main(Test2.java:22)
Caused by: java.lang.RuntimeException: 不能创建多个对象
at com.ssw.config.thread.Singleton.<init>(Singleton.java:17)
... 5 more
实例
1. JDK源码中的单例模式
如java.lang.Runtime
就是使用了饿汉式单例(静态成员变量)
供外部调用方法
public static Runtime getRuntime() {
return currentRuntime;
}
2.Spring源码中的单例模式
在Spring依赖注入Bean实例,默认是单例的。Bean有两种模式,单例(singleton)和多例(prototype)。
- singleton(单例):只有一个共享的实例存在,所有对这个 bean 的请求都会返回唯一的实例。
- prototype(多例):对这个 bean 的每次请求都会创建一个新的 bean 实例,类似于 new。
如@Autowired
,@Resource
,@Service
,@Mapper
,@Component
,默认都属于单例模式。
这里拿@Autowired
举例
示例
新建一个users接口
public interface IUsers {
/**
* 获取信息
* @return
*/
boolean getInfo();
}
新建users接口的实现类
@Service
public class UsersService implements IUsers {
@Override
public boolean getInfo() {
return true;
}
}
控制层调用(为了对比,写了两个控制层)
@RestController
@RequestMapping("users")
public class Users1Controller {
private static final Logger log = LoggerFactory.getLogger(Users1Controller.class);
@Autowired
private IUsers users;
@RequestMapping("a1")
public void a1() {
log.info("a1: users={},返回结果:{}", users, users.getInfo());
IUsers iUsers = new UsersService();
log.info("a1 new 出来的: users={},返回结果:{}", iUsers, iUsers.getInfo());
}
}
@RestController
@RequestMapping("users1")
public class Users2Controller {
private static final Logger log = LoggerFactory.getLogger(Users2Controller.class);
@Autowired
private IUsers users;
@RequestMapping("a2")
public void a2() {
log.info("a2: users={},返回结果:{}", users, users.getInfo());
IUsers iUsers = new UsersService();
log.info("a2 new 出来的: users={},返回结果:{}", iUsers, iUsers.getInfo());
}
}
说明:两个控制层分别进行注入IUsers
接口,并new
一个服务层,日志输出进行比较。
结果
: a1: users=com.demo.service.UsersService@3ae1a7f3,返回结果:true
: a1 new 出来的: users=com.demo.service.UsersService@235bfd56,返回结果:true
: a2: users=com.demo.service.UsersService@3ae1a7f3,返回结果:true
: a2 new 出来的: users=com.demo.service.UsersService@66ee65ad,返回结果:true
结论
- 可以看出多次注入的实例是同一个实例。
- 每次new出来的实例都是新建,所以每个实例都不一样,会造成资源浪费。
Spring的bean注入是懒汉式单例还是饿汉式单例?
扩展
上面说到,在Spring依赖注入Bean实例,默认是单例的。Bean有两种模式,单例(singleton)和多例(prototype)。
- singleton(单例):只有一个共享的实例存在,所有对这个 bean 的请求都会返回唯一的实例。
- prototype(多例):对这个 bean 的每次请求都会创建一个新的 bean 实例,类似于 new。
多例实现
上面的示例,都是使用UsersService
这个bean
,我们只需要增加@Scope
注解就可以将这个bean
变为多例模式。
@Scope("prototype")
@Service
@Scope("prototype")
public class UsersService implements IUsers {
@Override
public boolean getInfo() {
return true;
}
}
其他类不需要修改
测试结果
a1: users=com.demo.service.UsersService@5c8847c1,返回结果:true
a1 new 出来的: users=com.demo.service.UsersService@405b8b1e,返回结果:true
a2: users=com.demo.service.UsersService@6a7b3cfc,返回结果:true
a2 new 出来的: users=com.demo.service.UsersService@b529dd6,返回结果:true
可以看到每个实例都不一样,对这个bean
的每次请求都是创建一个新的实例。
@Scope
有哪些
singleton
单例模式,也就是容器中只存在一个实例,不管怎么获取Bean都是只存在一个实例,singleton类型的bean定义从容器启动到第一次被请求而实例化开始,只要容器不销毁或退出,该类型的bean的单一实例就会一直存活,典型单例模式。prototype
多例模式,spring容器每次都会重新生成一个新的Bean实例给请求方,选择该类型的对象的实例化和属性设置都由容器负责,但准备工作完成后,对象实例给到请求方后,容器不再拥有对该对象的引用,拿到该对象实例的这一方需要对该对象实例后续的生命周期负责(比如说销毁)。request
同一次请求创建一个实例,bean在当前请求内有效,该可以理解成一种特殊的多例模式。当请求结束后,该对象的生命周期即告结束,他们之间相互不干扰。session
代表同一个session创建一个实例,一个session id对应一个session。global session
基于porlet的web程序中才有用,它映射到porlet的global session,普通的servlet中使用了这个模式,容器会将其作为普通的session的scope对待。
最常用的是:单例singleton
和多例prototype
。