老铁们,今天开始,我们聊聊设计模式。
前言
有人说了,为啥要用设计模式,我不用它,不是照样能实现业务么。是的,你说的没错,确实是这样。但是,要知道,好的设计模式下,程序才会有更好的抽象性、复用性和扩展性,程序代码才能在业务需求发生变化时,对原逻辑改动最小,他好你也好嘛。
比如,策略模式,就是先抽象出一个父类,而将不同的逻辑用不同的子类去继承实现,如果需要增加新的逻辑分支,只需要再扩展出一个子类即可,而不需要修改父类及其他子类。
正如梅耶大爷提到的软件设计中最重要的原则之一:开闭原则。
对扩展开放,对修改关闭。
再比如,适配器模式。假设有个十年前的老系统,现在也需要加入到目前流行的微服务注册中心去管理,时间紧,任务重,怎么办。这时,就可用适配器的设计思想,用微服务的方式包装一层,适配一下。既不影响老系统的业务逻辑,又可以实现新的管理方式。
在编码中,设计模式的运用,体现了一个程序员的良好修养。目前,公认的设计模式有23种,后面我们都会一一讲到。
单例模式
今天,咱们先从最简单的单例模式开始。 所谓单例模式,是指系统中只允许有一个实例存在。怎么来保证呢?
public class Singleton01 {
private static Singleton01 sg = new Singleton01();
private Singleton01(){
}
public static Singleton01 getInstance(){
return sg;
}
public static void main(String[] args) {
int i=0;
while(i<100){
new Thread(()->{
System.out.println(getInstance());
}).start();
i++;
}
}
}
我们首先定义一个private 的构造函数,防止其他程序再去new它。为什么呢?A程序new一个,B程序new一个,当然就不是单例了。
同时,提供一个静态公共方法 public static Singleton01 getInstance(),供其它程序调用,来获取该类的实例。有人说了,这里不用static行吗,那太不行了,这个方法就是要去让其他类调用去创建对象的嘛。
由于sg对象被定义为static的,Singleton01类在加载、初始化时,就会执行 new Singleton01() 操作, 把sg对象初始化好。
在getInstance()方法中,我们直接返回类加载时创建的唯一sg对象。
这就叫做单例模式的饿汉式方法,顾名思义就是迫不及待,先创建好实例。
我们先看下这样写有没有问题,是不是确实是单例。运行下(为了避免重复,main方法在下面代码中不再展示,其实目的就是在多线程环境下打印出对象的内存地址,来验证是否是同一个对象),可以看到输出的是同一个地址,没问题。
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
Singleton.Singleton01@1e60980
...
饿汉式存在的问题
有人说了,你这个对象一上来就加载好了,万一我压根儿不用呢,这不是浪费内存吗?
那我们换个写法,调用 getInstance 时,先判断,为空时再创建对象。对,这就是传说的 懒汉式,它不着急嘛。
懒汉式
1.
public class Singleton02 {
private static Singleton02 sg;
private Singleton02(){
}
public static Singleton02 getInstance(){
if(sg==null){
dosth();
sg = new Singleton02();
}
return sg;
}
//模拟耗时的逻辑
private static void dosth(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Singleton.Singleton02@172603c
Singleton.Singleton02@172603c
Singleton.Singleton02@1e60980
Singleton.Singleton02@1406116
...
明显可以看到,内存地址不一样了,也就是说,产生了多个Singleton02对象,说明这样写是不对的。
那问题出在哪儿了呢?是 dosth 吗?看起来好像是,把他去掉就好了。其实不是,我们这里加 dosth(为避免重复,以下代码省略) 只是模拟了创建对象的过程。
我们分析一下原因。
线程1和线程2上来,都判断对象为空,都去创建对象了。
所以,问题在于,多线程环境下,并发访问,没有加锁。
2.
public class Singleton03 {
private static Singleton03 sg;
private Singleton03(){
}
public synchronized static Singleton03 getInstance(){
if(sg==null){
dosth();
sg = new Singleton03();
}
return sg;
}
}
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
Singleton.Singleton03@173340f
...
现在来看,代码没问题,是单例了。但是我们这样直接在方法声明上加synchronized关键字会有效率问题,因为每次调用该方法取对象时,即使对象已经存在了,还是要去申请锁。
3.
为了改进上述写法上存在的效率问题,我们改进一下,缩小synchronized的范围。增加判断,如果为空,需要创建对象时才加锁。
public class Singleton04 {
private static Singleton04 sg;
private Singleton04(){
}
public static Singleton04 getInstance(){
if(sg==null){
synchronized(Singleton04.class){
dosth();
sg = new Singleton04();
}
}
return sg;
}
}
Singleton.Singleton04@afc10b
Singleton.Singleton04@afc10b
Singleton.Singleton04@14c637e
Singleton.Singleton04@14c637e
Singleton.Singleton04@df4aed
Singleton.Singleton04@12c6b2b
从运行结果上看,是有问题的,问题出在哪儿呢,我们分析一下。
假设有2个线程同时访问getInstance方法,线程1和线程2在时刻1都判断sg为空,线程1在时刻2拿到锁并且创建了对象,返回,然后释放锁。随后,线程2在时刻3拿到锁并且又创建了对象。此时,系统中便产生了两个实例。
那怎么办呢?我们改进一下,在加锁之后,再次判断,实例是否为空。
4.
public class Singleton05 {
private static volatile Singleton05 sg;
private Singleton05(){
}
public static Singleton05 getInstance(){
if(sg==null){
synchronized(Singleton05.class){
if(sg==null){
dosth();
sg = new Singleton05();
}
}
}
return sg;
}
}
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
Singleton.Singleton05@142188f
...
这种写法就叫做DCL(Double Check Lock),判断两次是否为空。其实第一次的判断能拦截大多数的请求了,如果对象已经创建,就不会进行后续的加锁处理了。相比上面一种写法,效率上会有很大提升。
另外,还有一个点,不知道大家注意到没有。对,就是关键字 volatile 。我们都知道,volatile的两大作用:线程可见和禁止指令重排。
这里sg对象声明时增加volatile修饰,是为了防止JVM进行指令重排的。
简单来讲,就是防止对象在没有完全初始化的情况下返回。在这先不展开了,我们以后讲JVM的时候再细说。
静态内部类
我们再来看最后一种,静态内部类的写法。
public class Singleton06 {
private Singleton06(){
}
private static class Singleton06_Inner{
private static Singleton06 sg = new Singleton06();
}
public static Singleton06 getInstance(){
return Singleton06_Inner.sg;
}
}
Singleton.Singleton06@3c0755
Singleton.Singleton06@3c0755
Singleton.Singleton06@3c0755
Singleton.Singleton06@3c0755
我们还是将构造方法定义成private的,其他类不能去创建对象。 在这里声明了一个内部类Singleton06_Inner,它是可以去调用外部类的构造方法去创建对象的。
那有人说了,都是一上来就创建对象,这跟上面的饿汉式有啥区别。
我们说,内部类是在属性或方法被调用的时候才会被加载,而虚拟机加载一个类的时候,只加载一次,这是虚拟机内部去保证的。
所以说,这个方式可以实现懒加载且是单例。
使用场景
单例模式一般应用于资源共享的情况下,例如数据库连接池、线程池、缓存、日志对象、读取配置文件的类等。
Spring中也有用过单例模式,比如默认情况下的bean等。
总结
我们今天讲了单例模式,即保证系统中只能创建一个类实例。 主要讲了创建单例的三种写法,饿汉式(类初始化时就创建对象)、懒汉式(DCL)以及静态内部类的方式。
饿汉式相对简单,但是可能会浪费系统资源; 懒汉式稍复杂,需要注意到多线程访问时的处理; 静态内部类是简单又安全,推荐使用。
好了,本次分享就到这里,下个设计模式见。
最后,如果你觉得有用,写的还可以的话,欢迎转发、点赞,感谢。
我是冷风,专注于技术开发领域,关注我,让我们一起成长。