一、基础概念
1、定义:保证一个类只有一个实例,并提供一个全局访问点
2、类型:创建型
3、适用场景
- 想确保任何场景下都只有一个实例 ,比如线程池、数据库连接池
- 全局信息类,比如网站访问次数,我们希望所有访问记录都记录在对象A上,这时候就要使得这个类是单例
- 无状态工具类,比如日志工具类,不管在哪里使用,只需要它帮助我们记录日志信息这时候我们只需要一个实力对象就可以了
4、优点:
- 在内存里只有一个实例,减少了内存开销(需要频繁的创建或销毁时,或者说这种创建销毁的行为无法优化)
- 避免对资源的多重占用
- 设置全局访问点,严格控制访问
5、缺点:
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难
- 单例类的职责过重,在一定程度上违背了“单一职责原则”
6、重点
- 私有构造器,禁止从外部调用构造函数创建对象
- 线程安全
- 延迟加载,使用时候才创建
- 序列化和反序列化的问题
- 反射(防御反射攻击)
7、懒汉式和饿汉式的区别
- 线程安全
- 懒汉式本身是非线程安全的
- 饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题
- 资源加载和性能
- 饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成
- 懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟
8、UML
二、懒汉式及多线程
懒汉式是延迟加载的,并没有在类加载时就被初始化,如下所示:
//懒汉式
private static LazySingleton lazySingleton = null;
//饿汉式
private static HungrySingleton hungrySingleton = new HungrySingleton();
1、基本步骤
//创建懒汉类
public class LazySingleton {
//声明一个静态的要被单例的对象
private static LazySingleton lazySingleton = null;
//懒汉式比较懒,在初始化时是没有创建的,而是做了延迟加载
private LazySingleton() {
}
//获取LazySingleton对象的方法
public static LazySingleton getInstance() {
//如果对象是空的就new一个,否则就直接返回
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
//这种方式只能用于单线程,它是线程不安全的,
// 一个线程到达了lazySingleton = new LazySingleton();而另一个线程到达了if判断那一行...这样会导致多个对象产生
}
//实现Runnable接口来实现多线程
public class T implements Runnable {
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " " + instance);
}
}
public class Test {
public static void main(String[] args) {
// LazySingleton instance = LazySingleton.getInstance(); //单线程的方式
//多线程的方式,run的话得到的对象都是一样的,这边要使用多线程debug
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("end");
}
}
多线程debug方法
设置多线程debug。在这打上断点,对1断点按右键,suspend选Thread挂起方式,选中后能用断点控制线程的执行顺序,如果是all只会debug到本线程。make Default表示以后断点都是你选择的这个方式,但是之前打的断点不会修改。
对于Thread t0 和 t1,如果t1没有拿到对象,而t0拿到对象并且执行完,那么全都执行完时,这两个线程输出的对象的地址是不一样的;两个都未执行完的情况下,t0拿到了对象,t1没拿到对象,等t1拿到对象后,会把t0的对象覆盖,最后程序都执行完后,两个线程拿到的对象都是相同的。
2、使用synchronized改进
在getInstance
方法上加synchronized
关键字使它变成同步方法
//锁静态方法写法
public synchronized static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
//第二种写法
public LazySingleton getInstance() {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
//再进行debug我们可以看到Thread的状态会变为`Monitor`,被阻塞,只有另一个线程走完了才会变为`RUNNING`
缺点:synchronized锁太大,对性能有影响。
3、使用double check来改进
这种方式兼顾了性能和线程安全,也是懒加载的.
我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。
public static LazyDoubleCheckSingleton getInstance() {
//这边不加锁,如果不为null,直接返回,如果为null,也只有一个线程能够进入内部,保证只有一个线程能创建对象,对象创建好后,以后调用这个方法都不需要加锁,直接返回对象,大大降低synchronized加在方法上带来的开销
if (lazyDoubleCheckSingleton == null) {
//锁定这个单例的类
synchronized (LazyDoubleCheckSingleton.class) {
//再做一层判断
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//隐患2
//上面这一行进行了三个操作,在步骤2和3可能进行了重排序,2和3顺序可能被颠倒
//1、分配内存给对象
//2、初始化对象
//3、设置lazyDoubleCheckSingleton指向刚分配的内存地址
}
}
}
return lazyDoubleCheckSingleton;
}
但是这个方法也不是无懈可击,问题出在这个new,假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。
如果是单线程,没什么问题
如果是多线程的情况,线程0只是设置了指向内存的空间,海内初始化,但是此时线程1到了,看到内存中有instance地址,然后访问了一个尚未初始化的对象。
解决思路是:
1、volatile + double check
不允许步骤2和步骤3(初始化和指向内存空间)进行重排序,即使用volatile
来声明这个instance,这样重排序就会被禁止
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
2、通过静态内部类来解决–基于类初始化的延迟加载
允许线程0中的2和3重排序,但是不允许线程1看到这个重排序
public class StaticInnerClassSingleton {
//在静态内部类中直接new了一个instance的对象
//这个方法的核心在于静态内部类InnerClass的初始化锁被哪个线程拿到,哪个线程就去初始化它
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
//私有构造器是必须的
private StaticInnerClassSingleton() {
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
}
原理:jvm在类的初始化阶段期间会获取一个锁,这个锁可以同步多个线程对一个类的初始化(绿色部分),线程0和线程1试图去获取这个锁的时候,肯定只有一个线程能获得锁,假设线程0获得了,由线程0去执行静态内部类的初始化,对于静态内部类,即使步骤2和步骤3存在重排序,但是线程1是无法看到的(因为没有获得锁),只能等待。五种A类被立刻初始化的情况:
1、有一个A类型的实例被创建
2、A类中声明的一个静态方法被调用了
3、A类中声明的一个静态成员被赋值
4、A类中声明的一个静态成员被使用,且这个成员不是常量成员
5、A类是一个顶级类,且类中有嵌套的断言语句
三、饿汉式
在类加载的时候就完成实例化,我们可以把它设置为final,这样就不可更改了
public class HungrySingleton {
//直接实例化
private final static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
优点是类加载是写法简单,避免了线程同步问题,但缺点是,如果这个类自始至终都没用过,就造成了内存浪费,我们把单例实例化的过程放到静态代码块。
声明为final的变量必须在类加载完成时就已经赋值,第一种方式是直接new出来,第二种方式是放到静态块中去new,这个都能完成在类加载时赋值
public class HungrySingleton {
private static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
四、序列化和反序列化破坏单例模式
测试代码
public class Test {
//HungrySingleton实现序列化接口
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
//把instance单例对象序列化到一个文件中,再从文件中取出来,这两个对象还会是同一个对象吗
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); //HungrySingleton@1d44bcfa
System.out.println(newInstance); //HungrySingleton@b4c966a
System.out.println(instance == newInstance); //false
}
}
为什么会这样?序列化会通过反射调用无参数的构造方法创建一个新的对象。
具体分析见https://blog.csdn.net/chy6575/article/details/51063505
如何解决?添加readResolve()
方法并返回单例对象
import java.io.Serializable;
public class HungrySingleton implements Serializable {
private static HungrySingleton hungrySingleton;
static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
//解决序列化和反序列化破坏单例模式
private Object readResolve() {
return hungrySingleton;
}
}
//再run得到
//HungrySingleton@1d44bcfa
//HungrySingleton@1d44bcfa
//true
五、反射攻击解决方案
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射攻击解决方案
Class objectClass = HungrySingleton.class;
//通过反射把构造器权限打开来获取对象
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true); //打开权限
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance); //HungrySingleton@61bbe9ba
System.out.println(newInstance); //HungrySingleton@610455d6
}
}
通过反射打开了权限,private构造器就没用了。
解决方案是在类加载时构造器内进行判断,只有饿汉式和懒汉式的静态内部类方式能使用,这两个是在类加载时就初始化了对象
public class HungrySingleton implements Serializable {
private 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;
}
}
那对于不是在类加载时创建对象的情况怎么解决反射攻击呢?
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
LazySingleton instance = LazySingleton.getInstance();
Constructor<LazySingleton> constructor = LazySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = constructor.newInstance();
System.out.println(instance); //LazySingleton@61bbe9ba
System.out.println(newInstance); //LazySingleton@610455d6
}
}
通过给LazySingleton
的私有构造器添加判断代码,发现两种不同情况
//我们通过给LazySingleton的私有构造器添加判断代码,发现两种不同情况
先执行LazySingleton instance = LazySingleton.getInstance();
后执行LazySingleton newInstance = constructor.newInstance();
//结果返回true;
先执行LazySingleton newInstance = constructor.newInstance();
后执行LazySingleton instance = LazySingleton.getInstance();
//结果返回false;
//这个和cpu分配有关
结论是:对于Lazy这种情况,一旦多线程,反射攻击是无法避免的
六、多例模式
多例模式的关键点在于
- 通过实例容器保存容器
- 利用私有构造器阻止外部构造
- 提供
getInstance()
方法获取实例
多例模式必定是有限多例模式,无限多例只要不停new就行了。如果多例模式的上限是1,那就是单例模式了。
多例模式的特点
- 可以有多个实例
- 自己创建并管理自己的实例,向外界提供自己的实例
/**
* 多例模式
* 以围棋只有黑白两种棋子为例
*/
public class MultiplePattern {
//必须要有容器
private static List<Chess> chessList = new ArrayList<>();
private static final Chess white = new Chess("white");
private static final Chess black = new Chess("black");
private static final int maxCount = 2;
static {
chessList.add(white);
chessList.add(black);
}
//私有构造方法,避免外部创建实例
private MultiplePattern() {
}
//随机拿取棋子
public static Chess getInstance() {
Random random = new Random();
int crt = random.nextInt(maxCount);
return chessList.get(crt);
}
//指定拿取棋子
public static Chess getInstance(int index) {
return chessList.get(index);
}
}