含义:
单例模式是实际开发中最常见的设计模式之一,即在整个类中只有一个实例对象。说白了就是你只能娶一个媳妇(实例对象),但是可以有好几个孩子(使用单例类的类)。
设计模式:设计模式就好比象棋、围棋中的棋谱。在实际开发中,我们也会遇到很多“问题场景”,针对这些问题场景大佬们总结出了一些固定的套路,按照这些套路去编写代码,我们就能更好的解决问题了。
使用场景:
1.频繁的创建和销毁对象,例如学校官网首页页面缓存。
2.频繁访问IO资源的对象,例如数据库连接或是访问配置文件。
3.某些被创建时会消耗大量资源,但又经常使用的对象。
结构:
单例类:只有一个实例对象的类。
访问类:使用单例类的类。
分类:
饿汉模式:类加载的同时,创建实例(还没调用方法就已经先把实例创建出来了)。
懒汉模式:类加载的同时不创建实例,第一次使用的时候才创建实例(你不调用,他就不会创建)。
优缺点:
优点:
1.对于需要频繁创建和销毁的对象,使用单例模式可以提高性能和效率,避免了重复创建对象的开销,在一定程度上节省了CPU资源。
2.简化了对象的管理和操作,所有对该类对象的访问都通过单一实例进行,方便统一管理。
缺点:
1.单例模式一般使用全局变量或者静态成员变量来储存实例,这可能引起线程安全问题。在多线程环境下,可能会出现竞争条件,需要加入额外的逻辑来确保线程安全。
2.单例模式对代码的扩展性和可测试性有一定的影响,因为它进入了全局状态,有些情况下可能会增加代码的耦合度。
3.单例模式的使用增加了代码的复杂性,有时候会导致难以调试和理解。
注意:
1.单例模式只能有一个实例。
2.单例模式必须自己创建自己的唯一实例。
3.单例类必须给所有其他对象提供这一实例。
具体实现思路:
1.确保只能有一个实例对象
只对类进行一次实例化,所有人都只能使用这个第一次实例化的对象
2.构造方法私有化
可以避免其他代码能够创建出实例,保证外界不能通过new来创建一个新对象,而是只能使用上面第一次实例化的对象
3.提供一个静态方法供外界来使用这个唯一的实例对象
因为外界不能new新对象了,所以我们必须提供一个类方法才能让外界访问到这个唯一的实例对象
代码实现:
饿汉模式:
//饿汉模式
//这里我们希望这个类里有唯一实例
class Singleton{
//static成员,在Singleton类被加载的时候,就会执行到这里的创建实例的操作(比较早的时机)
//这里就是饿汉模式(还没调用方法就先把实例创建出来了)
private static Singleton instance = new Singleton();
//通过这个静态方法来获取到刚才的实例
//后续如果想使用这个类的实例,都通过getInstance方法来获取
public static Singleton getInstance(){
return instance;
}
//把构造方法设置成私有,避免其他代码能够创建出实例
private Singleton() {
}
//通过上述方式,我们就能避免其他程序员在使用这个类的时候,就不会创建出多个对象了
}
public class Dome21 {
public static void main(String[] args) {
//此处又有一个实例了,这就不是单例了
//Singleton s1 = new Singleton();
Singleton s1 = Singleton.getInstance();
}
}
这种写法是否属于线程安全呢?
饿汉模式是线程安全的,因为该模式对instance这个变量只是读取并没有修改
懒汉模式:
普通式:
class SingletonLazy1 {
private static SingletonLazy1 instance = null;//这里并没有进行实例化,只是创建
private SingletonLazy1(){
}
public static SingletonLazy1 getInstance(){
if(instance == null){
instance = new SingletonLazy1();//这里是进行了实例化
}
return instance;
}
public static void main(String[] args) {
}
}
通过上述代码我们可以看到,懒汉模式并不是一开始就把对象进行实例化,而是什么时候需要什么时候实例化,这样虽然实现了懒汉模式,但是可能会出现线程安全问题。如果是单线程那倒好说,但是到了多线程环境下你就会发现:一个线程进入了 if (instance == null) 语句,但未完成实例化;同时另一个线程也进入到了 if (instance == null) 语句,因为之前未完成实例化,所以第二个线程也会进入到 if 语句,这样就创建了多个实例;不仅如此,在多线程环境下还有可能会发生阻塞。创建多个实例就不符合单例模式的条件了,因此这样是不安全的。
考虑到线程安全,我们做出如下改进,加锁:
class SingletonLazy1 {
private static SingletonLazy1 instance = null;
private SingletonLazy1(){
}
public static SingletonLazy1 getInstance(){
synchronized (SingletonLazy1.class) {
if (instance == null) {
instance = new SingletonLazy1();
}
}
return instance;
}
public static void main(String[] args) {
}
}
这样写虽然改善了线程安全问题,但是又有了一个新的问题:
一旦这么写,后续每次调用 getInstance() 都需要加锁。但是在实际上,懒汉模式的线程安全问题只出现在最开始的时候(最开始你的对象还没 new ),此时你再加锁,就有点画蛇添足的意思了。因为加锁其实是一个开销很大的操作,加锁可能就会涉及到“锁冲突”,一冲突就会引起线程阻塞。一旦对象 new 出来了,后续多线程调用 getInstance() 的时候,就只有读操作,这是线程就安全了。
因此为了避免上面那种画蛇添足的情况,我们在加锁语句的外层再加一个 if 语句来判断是否要加锁(双重校验锁):
class SingletonLazy1 {
private static volatile SingletonLazy1 instance = null;
private SingletonLazy1(){
}
public static SingletonLazy1 getInstance(){
if(instance == null) {
synchronized (SingletonLazy1.class) {
if (instance == null) {
instance = new SingletonLazy1();
}
}
}
return instance;
}
public static void main(String[] args) {
}
}
这两个 if 的执行时机可能会差异很大,两个 if 的执行结果也可能截然相反。
第一个 if 用来判断是否需要加锁(判断的标准是该对象有没有进行实例化,如果已经实例化就直接返回实例,不在进行加锁),第二个 if 用来判断是否需要 new 对象,这二者都是一样的写法。
这里在 instance 加了一个 volatile 关键字进行修饰,原因是:
第二个 if 语句里的 instance = new SingletonLazy1(); 可能触发指令重排序:
new 操作,总共分为三步(初始时):
1.申请内存空间
2.在内存上构造对象(调用构造方法)
3.把内存的地址赋值给 instance 引用(此时 instance 的值才从默认值 null 变为 SingletonLazy1 对象的内存地址)
如果触发了指令重排序,那么执行顺序就会改变:要么是1 3 2 ,要么是 1 2 3( 1 一定是先执行的),所以当一条线程访问instance时,由于instance未必初始化完成,也就造成了线程安全问题,这些顺序在单线程环境下是无所谓的,但是在多线程环境下就出问题了。
除了上述的几种写法,还有一种静态内部类的写法:
class SingletonLazy1 {
private static class SingletonLazy2{
private static final SingletonLazy1 instance = new SingletonLazy1();
}
private SingletonLazy1(){
}
public static SingletonLazy1 getInstance(){
return SingletonLazy2.instance;
}
}
这种写法是性能最优的,静态内部类里的逻辑需等外部方法调用的时候才执行,巧妙的运用了内部类的特征,运用JVM底层逻辑,完美避开了线程安全问题(通过 static final 修饰,可以保证 instance 只被初始化一次),而且整个过程没有加锁,代码执行效率也比双重校验锁的实现方法更高。
这样写虽然已经足够安全了,但是还可能遭到反射破坏,即使构造方法已经私有。所以为了避免反射破坏,我们还可以做出如下改进:
class SingletonLazy1 {
private static class SingletonLazy2{
private static final SingletonLazy1 instance = new SingletonLazy1();
}
private SingletonLazy1(){
if(SingletonLazy2.instance != null){
throw new RuntimeException("禁止创建多个实例");
}
}
public static SingletonLazy1 getInstance(){
return SingletonLazy2.instance;
}
}
在该类的构造方法里加上一个 if 语句,如果这个对象再次被实例化是就直接抛出异常。
补充:反射:
在Java中,反射(Reflection)是指在运行时动态地获取类的信息,以及动态调用对象的方法和访问对象的属性。通过反射,我们可以在程序运行期间获取类的方法、字段、构造函数等信息,并可以在运行时创建对象、调用方法以及设置和获取字段的值。
反射提供了一种强大的机制,使得我们可以在编译时无法确定类的情况下,仍然能够操作类的成员。比如,在程序中通过反射来动态加载和使用某个类,可以提高程序的灵活性和可扩展性。
通过反射,我们可以通过类的全限定名来获取对应的Class对象,从而可以获取类的信息,比如类的方法、字段、构造函数等。然后,我们可以使用Class对象来创建对象、调用方法以及设置和获取字段的值。
需要注意的是,反射虽然提供了一种强大的机制,但由于在运行时动态地获取并执行类的成员,可能会产生一些性能上的开销,因此在性能要求较高的场景下,应该谨慎使用反射。
还有就是,反射非常的灵活,这就可能引起反射破坏,比如说:可以通过构造特定的类名、方法名和参数等信息,来绕过一些安全检查、身份验证或访问控制等措施,从而执行未经授权的操作,例如调用私有方法、获取私有字段的值、创建恶意对象等。