什么是单例模式?
单例模式,是一种常用的软件设计模式。通过单例模式可以保证系统中,应用该模式的这个类只有一个实例,即一个类只有一个对象实例。下面讲讲他的四种实现。
1. 饿汉式单例设计模式
饿汉单例设计模式就是使用类的时候已经将对象创建完毕,不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故被称为“饿汉模式”。
代码实现:
package com.hy.practice;
/**
* @author HY
* @ClassName UserService1
* @Description 饿汉式
* @DateTime 2020/12/27 10:54
* Version 1.0
*/
public class UserService1 {
// 1. 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
private UserService1() {
}
// 2. 在该类内部产生一个唯一的实例化对象
private static UserService1 userService1=new UserService1();
// 3. 定义一个静态方法返回这个唯一对象。
public static UserService1 getInstance() {
return userService1;
}
}
测试:
UserService1 instance1 = UserService1.getInstance();
UserService1 instance2 = UserService1.getInstance();
UserService1 instance3= UserService1.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
输出:
2.懒汉式单例设计模式
懒汉单例设计模式就是调用getInstance()方法时实例才被创建,先不急着实例化出对象,等要用的时候才实例化出对象。不着急,故称为“懒汉模式”。
package com.hy.practice;
import sun.security.jca.GetInstance;
/**
* @author HY
* @ClassName UserService2
* @Description 懒汉式
* @DateTime 2020/12/27 10:56
* Version 1.0
*/
public class UserService2 {
// 1. 将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
private UserService2() {
}
// 2. 在该类内部产生一个唯一的实例化对象
private static UserService2 userService2;
// 3. 定义一个静态方法返回这个唯一对象。
public static UserService2 getInstance() {
if (userService2==null){
return userService2=new UserService2();
}
return userService2;
}
}
测试:
UserService2 instance1 = UserService2.getInstance();
UserService2 instance2 = UserService2.getInstance();
UserService2 instance3 = UserService2.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance3);
输出:
但是这种实现方式在多线程的情况下会创建多个实例,无法保证单一实例,开两个线程打debug验证一下:
开两个线程获取实例
new Thread(()->{
UserService2 instance = UserService2.getInstance();
System.out.println(instance);
}).start();
new Thread(()->{
UserService2 instance = UserService2.getInstance();
System.out.println(instance);
}).start();
在getInstance()的方法中打个断点
第一个线程:
进入if:
第一个线程进入if语句中还未new出实例的时候,cpu调度被另一个线程抢走,另一个线程判断为null也会进入if语句:
第二个线程:
进入if:
让程序执行完,可以看到输出了两个不同的对象,这就出问题了:
所以可以在方法上用一个synchronized解决,但是这样做的效率太低了,当有很多个线程调用此方法的时候会造成一个线程执行,其他线程Blocked(锁阻塞)。所以有第二个版本:
3.双重检测锁
双重检查锁是高级版的懒汉式实现。先讲讲他的实现思路:当多个线程调用getInstance()方法时,之前的版本是在方法上加锁,所有没有拿到锁的线程都会锁阻塞,那有没有办法让其他线程也能调用方法?有,只在实例化对象的地方才加锁。
public static UserService3 getInstance() {
if (userService3 == null) {
synchronized (UserService3.class) {
userService3 = new UserService3();
}
}
return userService3;
}
但是仔细想想,这样加锁好像跟没加锁有一样问题啊,只是把实例化对象的步骤锁住了,当前线程在执行完if语句判断后cpu调度被抢走,其他线程判断为空,也会进入if中,只是“排队”new出实例罢了。如图:
所以有了最终版本!再判断一次,这就是双重检测锁。
package com.hy.practice;
/**
* @author HY
* @ClassName UserService3
* @Description TODE
* @DateTime 2020/12/27 15:37
* Version 1.0
*/
public class UserService3 {
//构造私有化
private UserService3() {
}
//维护一个唯一的对象
private static UserService3 userService3;
public static UserService3 getInstance() {
if (userService3==null){
synchronized (UserService3.class) {
//再判断一次
if (userService3==null){
userService3 = new UserService3();
}
}
}
return userService3;
}
}
验证一下
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(UserService3.getInstance());
}).start();
}
结果:太长了我就不贴图了,目测正确。大家可以自己试一下,也可以选择相信我~
4.静态内部类
最优雅的方式,因为静态内部类有以下特点:
- 外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。
- 可以使用外部类的私有构造方法
- 静态内部类只加载一次
所以,我们可以利用静态内部类的类加载特点,实现单例模式。
package com.hy.practice;
import sun.security.jca.GetInstance;
/**
* @author HY
* @ClassName UserService
* @Description 静态内部类
* @DateTime 2020/12/27 10:45
* Version 1.0
*/
public class UserService {
//构造私有化
private UserService() {
}
private static class InnerClass {
//内部类内实例化
static UserService userService=new UserService();
}
public static UserService getInstance(){
return InnerClass.userService;
}
}
测试一下:
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(UserService.getInstance());
}).start();
}
没毛病:
总结
- 第一种饿汉式:优点是线程安全,缺点是无法文现懒加载
- 第二种懒汉式:优点是实现了懒加载,缺点是执行效率低
- 第三种双重检测锁:优点即实现了懒加载、线程又安全、执行效率还比较高,只有第一次执行获取单例对象的方法需要加锁,之后有对象了都不用加锁
- 第四种方式静态内部类:线程又安全、又实现了懒加载、还不用加锁