前言:大家好,我是小熊,25届毕业生,目前在一家满意的公司实习。本篇文章将23种设计模式中的单例模式,此篇文章为一天学习一个设计模式系列文章,后面会分享其他模式知识。这些东西,刚开始应该是不全面的,但文章会一直更新的。
🧑个人简介:大家好,我是小熊,一个想吃鱼的男人😉😉
目前状况🎉:25届毕业生,在一家满意的公司实习👏👏💕欢迎大家:这里是CSDN,我用来快速回顾知识准备面试的地方,欢迎来到我的博客😘
单例模式
目录
(4)双重校验锁(double-checked locking)
单例模式可以说是Java设计模式中最简单的一个了,它属于创建型模式。核心就是,对象只要由一个线程创建且只创建一次。主要是需要大量重复的使用同一个相同的对象,为减少内存消耗,才这么做的
关键思路
要做到一个类的实例只能由类自身进行创建,关键就是私有化其构造函数。且在该类内部,需要实现单例创建与否的判断,根据判断的结果,如果有则直接返回,如果没有则创建之后返回。
单例模式主要是应用在需要控制实例数量,节省系统资源的场景下。而其优点相应的就是减少了内存的开销,尤其是在频繁的创建和销毁实例的时候。但是它也有缺点,比如它就与单一职责原则冲突了,一个类只应该关心其内部逻辑的实现,而不应该插手外部对其的实例化。
代码实现
单例模式的代码实现其实很简单,关键点就是私有化其构造函数。但是结合不同的场景需求(单线程多线程)以及Java的特性(锁和类加载),就会有很多种实现方法,我这里总结了常见的五种。
(1) 饿汉式(调用效率好,线程安全,不能延迟加载)
饿汉式实现起来简单,它基于类加载机制,不用加锁也能避免多线程同步的问题,所以执行效率会比较高。但是这种方法有个缺点,就是在类加载的时候就会进行实例化,没有达到Lazy Loading的效果。该种方法的代码实现如下:
public class Singleton1 {
private Singleton1() {
}
private static final Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
}
(2)懒汉式(为实现延迟加载,效率不是很好,线程也不安全)
针对饿汉式中没有Lazy Loading的效果,可以通过如下方法来实现。不过该方法也有个最大的问题,就是只能支持在单线程环境下工作,它是线程不安全的,多线程的话会出现多个实例化对象的出现。具体代码如下:
public class Singleton2 {
private Singleton2() {
}
private static Singleton2 instance;
public static Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
(3)懒汉式(线程安全,影响性能)
针对线程不安全的问题,最简单直接的解决方法就是加锁,对getInstance( )方法加锁,这能很快的解决多线程安全问题,但是带来的问题也很明显,就是太影响性能了。具体代码如下所示:
public class Singleton3 {
private Singleton3() {
}
private static Singleton3 instance;
synchronized public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
(4)双重校验锁(double-checked locking)
直接对整个getInstance( )方法上锁,能够解决线程安全问题,但会大大影响程序的性能,因为很容易出现锁竞争的情况。这里使用双重校验的机制,一方面能够解决线程安全问题,另一方面还能保持高性能。关键就是在于两个if判断,其实大部分情况下外部的if判断是不会通过的,因为只要该单例类被实例化了一次,外部if的判断条件就成立不了了,所以能够保持较好的性能;而内部的if判断,则针对的是单例类还没被实例化的时候,可能有多个线程同时通过了外部的if判断,要对单例类进行实例化,此时就需要上锁,并在临界区内再次判断实例是否已经被创建,防止多次创建,因此内部的if判断保证了只有一个实例会被创建。
volatile
但是,注意,此时属性需要加上volatile,
/** * 1.线程可见性: * 其他线程会立即看到这个变化, * 2.禁止指令重排: * new SingletonDemo3()这个操作实际上包含以下三个步骤: * 分配内存空间 * 初始化对象 * 引用赋值给singletonDemo3 * 重排序后先引用赋值,才初始化,其他线程可能会看到一个尚未完全初始化的对象。 导致程序出错 */
具体代码如下:
public class Singleton4 {
private Singleton4() {
}
private static volatile Singleton4 instance;
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
(5)静态内部类(具备所有优良特点)
该方法能够达到同双重校验方式相同的效果,且实现方法更为简单,不需要借助锁就能实现。主要是借助了类加载的特性,内部静态类只有在使用的时候才会进行加载,这就能够实现Lazy Loading;且类加载时只会加载一次,天然的线程安全保证。具体代码如下:
public class Singleton5 {
private Singleton5() {
}
private static class InnerClass {
private static final Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance() {
return InnerClass.instance;
}
}
/** * 问题:为什么这种内部静态类的方式,是线程安全的? * 答:首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的<clinit>方法。那么什么是<clinit>方法? * 这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的【静态成员的的赋值语句】和【静态代码块】组成的。JVM内部会保证一个类的 * <clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程 * 都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>方法。也就是说同一个加载器下, * 一个类型只会初始化一次。 * * 那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>代码,当我们执行getInstance方法的时候,会导致SingletonClassInstance * 类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以他只会有一个实例。 */b
补充延申:
单例模式是为了让我们使用同一个对象,但是这种模式通过反射是可以被破坏的,单例模式的关键点在于constructor方法的私有化private,我们可以通过反射detDeclareConstructor()获取构造函数,从而构建新的对象。
At least,最后小熊建议各位