java常用设计模式-单例模式原创
# 介绍以及场景
本人之前去一家公司面试,面试官就当场让你手写单例模式,还好早有准备,本人一下写出了三种,下面会详细介绍单例模式的种类。先说说单例模式是什么?单例模式就是在任何情况下获取有且仅有一个实例,并能够全局访问。举个生活中的例子,公司组织架构中的CEO角色便是。
通过查阅相关的资料信息,发现看似简单的单例模式,其实分为很多种类。每种都有自己的优缺点,下面我们详细学习整理一下相关的例子。
# 单例模式不同种类应用
# 一、饿汉式单例模式
什么叫做饿汉式?饿汉给人的形象就是有食物就迫不及待地去吃的形象。那么饿汉式单例模式很形象的也就是当类创建的时候就迫不及待地去创建单例对象,这种单例模式是绝对线程安全的,因为这种模式在尚未产生线程之前就已经创建了单例。可以很明显地想到其优缺点。
优点:线程安全,类加载时完成初始化,获取对象的速度较快.
缺点:浪费内存,有时候并不需要对象的时候缺已经创建了一个对象在内存中。
对应我们的spring中,其IOC容器本身就是一个饿汉式单例模式,spring启动的时候就将对象加载到了内存中。
以下分别看两段简单代码即:
/**
* @Author: maoba
* @Description: 饿汉单例模式
* @Date: 2020-05-11 22:45
*/
public class EHanSingletonDemo1 {
private static final EHanSingletonDemo1 demo1 = new EHanSingletonDemo1();
private EHanSingletonDemo1(){
}
//静态方法获取实体类
public static EHanSingletonDemo1 getInstance(){
return demo1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EHanSingletonDemo2 {
public static final EHanSingletonDemo2 eHanSingleDemo2;
static {
eHanSingleDemo2 = new EHanSingletonDemo2();
}
private EHanSingletonDemo2(){
}
public static EHanSingletonDemo2 getInstance(){
return eHanSingleDemo2;
}
}
2
3
4
5
6
7
8
9
10
11
上面说了饿汉模式的缺点就是比较浪费内存,对象会预先随着类的创建而创建,那么为了解决这个问题,我们将其改成需要调用的时候才会去创建对应的单例对象,这就是我们所说的懒汉单例模式。下面我们详细看一下懒汉单例模式的优点以及缺点。
# 二、懒汉式单例模式
通过上述的描述,我们很容易就能得出结论,懒汉式的单例模式,特点说白了就是等到调用的时候才会去生成实例。以下,我们直接来看一下其实现代码。
/**
* @Author: maoba
* @Description: 懒汉单例模式
* @Date: 2020-05-12 21:17
*/
public class LHanSingletonDemo {
private static LHanSingletonDemo lHanSingletonDemo = null;
public static LHanSingletonDemo getInstance(){
if(lHanSingletonDemo == null){
lHanSingletonDemo = new LHanSingletonDemo();
}
return lHanSingletonDemo;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
看了上述代码,没错,内存问题是解决了,但是相对应多线程的情况下会不会出现问题呢?我们接下来就做个实验。
我们创建一个线程类,并且开启两条线程去运行相对应的逻辑。
public class ExcutorThread implements Runnable{
public void run() {
LHanSingletonDemo lHanSingletonDemo = LHanSingletonDemo.getInstance();
System.out.println(Thread.currentThread().getName()+":"+lHanSingletonDemo);
}
}
2
3
4
5
6
public class LHanSingletonDemoTest {
public static void main(String[] args) {
Thread thread1 = new Thread(new ExcutorThread());
Thread thread2 = new Thread(new ExcutorThread());
thread1.start();
thread2.start();
System.out.println("END");
}
}
2
3
4
5
6
7
8
9
运行该test,我们可以一定概率可以得到两种不同的结果,如下:
END
Thread-0:LHanSingleton.LHanSingletonDemo@2be50a8
Thread-1:LHanSingleton.LHanSingletonDemo@5933f8be
2
3
END
Thread-1:LHanSingleton.LHanSingletonDemo@1c2b3523
Thread-0:LHanSingleton.LHanSingletonDemo@1c2b3523
2
3
这两种不同的结果,说明这种方式肯定存在线程安全性的问题,当然大家也可以将程序切换到Thread的Debug模式下,手动可以切换线程,我们会神奇地发现,我们完全可以通过切换线程的方式,让线对应的实例初始化两次,如下截图。
通过上述方式我们很清楚地可以发现实例的初始化情况。
那么如何解决该线程安全问题呢?
我们尝试给线程类方法块中加上锁试试。具体代码如下:
public class LHanSingletonDemo {
private static LHanSingletonDemo lHanSingletonDemo = null;
public synchronized static LHanSingletonDemo getInstance(){
if(lHanSingletonDemo == null){
lHanSingletonDemo = new LHanSingletonDemo();
}
return lHanSingletonDemo;
}
}
2
3
4
5
6
7
8
9
然后我们采用相同的方式进行切换进程,我们可以发现,当一个线程进行初始化实例的时候,另一个线程尝试去初始化实例的时候原先的Running状态自动变成了Monitor状态,如下图,这就表明了,此时两个线程不会同时创建一个对象,也就是说对象不会被初始化两次,这说明该线程安全性的问题得以解决了。
但是随着线程的增多,大家可以很自然而然地想到一个问题,大量的线程会同时 进行争夺这把锁,到时候会给CPU带来非常大的压力,大量的线程会处于挂起的状态,非常浪费性能。那么在这样一个基础上还能不能进行优化了呢?答案是肯定的,我们既要性能,又要安全,下面我们看一下利用单例模式的双重校验锁去做,并且分析一下其优缺点。
public class LHanSingletonDemo {
private static LHanSingletonDemo lHanSingletonDemo = null;
public static LHanSingletonDemo getInstance(){
if(lHanSingletonDemo == null){
synchronized (LHanSingletonDemo.class){
if(lHanSingletonDemo == null){
lHanSingletonDemo = new LHanSingletonDemo();
}
}
}
return lHanSingletonDemo;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
通过我们之前的线程断点模式调试的方法,我们很容易就发现,现在的两个线程在执行初始化方法的时候都会处于running状态,如下图:
这说明了这种方式可以有效降低锁的竞争,锁不会将整个方法全部锁定,而是锁定了某个代码块。其实完全做完调试之后我们还是会发现锁争夺的问题并没有完全解决,用到了锁肯定会对整个代码的执行效率带来一定的影响。所以最最终极的做法就是不用锁同时又能够保证线程的安全,并且能够不浪费内存。这种终极解决方案到底是否存在呢?答案是肯定的。我们来看下面这种写法:
public class LHanInnnerClassDemo {
private LHanInnnerClassDemo(){
}
public static final LHanInnnerClassDemo getInstance(){
return LazyHolder.LH;
}
//静态内部类,只有当使用的时候才会去加载(这个知识点,大家可自行做一些实验)
private static class LazyHolder{
private static final LHanInnnerClassDemo LH = new LHanInnnerClassDemo();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
这种方式既解决了饿汉式加载浪费内存的情况,又避免了使用锁带来的性能问题,内部类一定要在调用方法之前进行初始化,所以这种方式又巧妙地 兼具了线程安全性。所以是单例模式的终极选择。