设计模式之——单例模式你真的会吗?

用户头像
诸葛小猿
关注
发布于: 2020 年 08 月 06 日
设计模式之——单例模式你真的会吗?

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。简单说,模式就是在某些场景下,针对某类问题的某种通用的解决方案。



通常说的23种设计模式,大致可以分成三类:创建型模式结构型模式行为型模式



创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。



结构型模式:把类或对象结合在一起形成一个更大的结构。



行为型模式:类和对象如何交互,及划分责任和算法。



今天我们就先来说说创建型模式中的单例模式

什么是单例模式



单例模式也是面试常见问题,虽然很多人都知道,但是很多人并不是都熟悉所有的写法,以及写法的问题和解决办法。



单例模式具备典型的3个特点:1、只有一个实例。 2、自我实例化。 3、提供全局访问点。



因此当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。



单例模式的主要优点就是节约系统资源、提高了系统效率,同时也能够严格控制客户对它的访问。也许就是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,所以扩展起来有一定的困难。



单例模式的各种写法



1.饿汉式(推荐)



这种方式使用private static final Singleton1 INSTANCE = new Singleton1()定义了一个静态变量,在JVM加载类的时候,就会实例化一个单例,由于一个class文件只会加载一次,所以jvm可以保证线程安全性。



同时private Singleton1()是私有化构造器,可以保证这个类不能在其他类中new出来。



外部类想 获得这个实例,必须通过 public static Singleton1 getInstance()方法获取。



package com.wuxiaolong.design.pattern.singleton;
/**
* Description: 饿汉式
* @author 诸葛小猿
* @date 2020-08-05
*/
public class Singleton1 {
/**
* 静态变量 启动时自动加载
*/
private static final Singleton1 INSTANCE = new Singleton1();
/**
* 私有化构造器,不能new
*/
private Singleton1(){}
/**
* 对外暴露一个方法,获取同一个实例
*/
public static Singleton1 getInstance(){
return INSTANCE;
}
/**
* 测试
*/
public static void main(String[] args) {
Singleton1 s1 = Singleton1.getInstance();
Singleton1 s2 = Singleton1.getInstance();
if(s1 == s2){
System.out.println("s1和s2是内存地址相同,是同一个实例");
}
}
}



这种方式的唯一问题就是,不使用也会被实例化出来。如果不吹毛求疵,其实这也不算是啥问题。



这种方式简单实用,个人非常推荐在实际项目中使用



2.懒汉式一



懒汉式,就是为了解决上面的饿汉式不管用不用都加载的问题。



所谓的懒汉式(lazy loading),就是什么时候使用,就什么时候初始化,不使用就不会加载。



使用private static volatile Singleton2 INSTANCE定义变量,但是并不会初始化实例。初始化变量是在调用getInstance()方法的时候。



package com.wuxiaolong.design.pattern.singleton;
import java.util.HashSet;
import java.util.Set;
/**
* Description: 懒汉式一
* @author 诸葛小猿
* @date 2020-08-05
*/
public class Singleton2 {
/**
* 先不初始化 使用volatile的原因见底部
*/
private static volatile Singleton2 INSTANCE;
/**
* 私有化构造器,不能new
*/
private Singleton2(){}
/**
* 对外暴露一个方法,获取同一个实例。只有实例不存在时才初始化
* 问题:多线程同时访问getInstance时,可能会new出多个实例
*/
public static Singleton2 getInstance(){
if(INSTANCE == null){
// try {
// Thread.sleep(10);
// }catch (Exception e){
// e.printStackTrace();
// }
INSTANCE = new Singleton2();
}
return INSTANCE;
}
/**
* 测试
* 测试时可以开启 getInstance()方法中的sleep
*/
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
Singleton2 temp = getInstance();
// 打印出内存地址,如果内存地址不一样,则生成了多个实例
System.out.println(temp.toString());
}
}).start();
}
}
}



使用上面的懒汉式同时也带来了很多其他问题。多线程访问getInstance()时,会有线程安全问题,会生成 多个实例。可以使用上面的main测试测试一下。



为了解决线程安全问题可以使用锁synchronized (静态方法加锁,是给这个类加锁):



public static synchronized Singleton2 getInstance(){
if(INSTANCE == null){
INSTANCE = new Singleton2();
}
return INSTANCE;
}



但是这又会带来性能问题。为了提高性能,给锁加在代码块上:



public static Singleton3 getInstance(){
if(INSTANCE == null){
synchronized (Singleton3.class){
INSTANCE = new Singleton3();
}
}
return INSTANCE;
}



但是这种方式依然有线程安全问题。多个线程可以同时进入到if中,虽然同时只有一个线程可以执行synchronized代码块,但是进入if中的线程都会进行实例化。



3.懒汉式二



为了解决上面的懒汉式一的线程安全问题,有人想出了一个“聪明”的技巧:双重检查锁定(Double-Checked Locking)



package com.wuxiaolong.design.pattern.singleton;
/**
* Description: 懒汉式二
*
* @author 诸葛小猿
* @date 2020-08-05
*/
public class Singleton3 {
/**
* 先不初始化
*/
private static volatile Singleton3 INSTANCE;
/**
* 私有化构造器,不能new
*/
private Singleton3(){}
/**
* 对外暴露一个方法,获取同一个实例
* 内部双重判断,使用两次INSTANCE == null的判断
*/
public static Singleton3 getInstance(){
if(INSTANCE == null){
synchronized (Singleton3.class){
// synchronized块内部,再判断一次是否为空
if(INSTANCE == null){
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}
/**
* 测试
*/
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
Singleton3 temp = getInstance();
// 打印出内存地址,如果内存地址不一样,则生成了多个实例
System.out.println(temp.toString());
}
}).start();
}
}
}



双重检查就是在synchronized内外分别判断一次INSTANCE == null,这样就可以保证线程安全了:



public static Singleton3 getInstance(){
// 第一次检查
if(INSTANCE == null){
synchronized (Singleton3.class){
// synchronized块内部,第二次检查
if(INSTANCE == null){
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}



上面第一个INSTANCE == null是否有必要? 当然有,如果没有第一个判断,每个线程进来都上锁,这样会非常消耗性能;加了第一个判断,很多线程就不会来获得锁了。



这是最完美的写法之一,也推荐大家再实际项目中使用;使用双重判断,没有线程安全问题。



但是个人感觉,真的没这个必要,使用饿汉模式就可以了。这可能是处女座推荐使用的。



4.懒汉式三(推荐)



这里使用静态内部类private static class InnerSingleton,既保证了懒加载,也保证了线程安全。



这里的懒加载的原因是,jvm加载Singleton4.class的时候,不会加载静态内部类InnerSingleton.class;只有调用getInstance()才会加载静态内部类InnerSingleton.class



这里的线程安全是有JVM保证的:jvm加载Singleton4.class的时候,只会加载一次;所以InnerSingleton.class也只加载一次,所以INSTANCE也只实例化一次。



package com.wuxiaolong.design.pattern.singleton;
/**
* Description:
* 既保证了懒加载,也保证了线程安全。
* @author 诸葛小猿
* @date 2020-08-05
*/
public class Singleton4 {
/**
* 私有化构造器,不能new
*/
private Singleton4(){}
/**
* 使用静态内部类,jvm加载Singleton4.class的时候,不会加载静态内部类InnerSingleton.class
* 只有调用getInstance()才会加载静态内部类InnerSingleton.class
*/
private static class InnerSingleton{
private static final Singleton4 INSTANCE = new Singleton4();
}
/**
* 对外暴露一个方法,获取同一个实例
* 返回的是静态内部类的成员变量,这个变量在调用的时候才会初始化
*/
public static Singleton4 getInstance(){
return InnerSingleton.INSTANCE;
}
/**
* 测试
*/
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
Singleton4 temp = getInstance();
// 打印出内存地址,如果内存地址不一样,则生成了多个实例
System.out.println(temp.toString());
}
}).start();
}
}
}



这也是完美的方案,墙裂推荐。



5.终极模式-枚举



java开发都知道一本书叫《Effective Java》。《Effective Java》是Joshua Bloch写的。他是Java 集合框架创办人,领导了很多 Java 平台特性的设计和实现,包括 JDK 5.0 语言增强以及屡获殊荣的 Java 集合框架。2004年6月他离开了SUN公司并成为 Google 的首席 Java 架构师。此外他还因为《Effective Java》一书获得著名的 Jolt 大奖。



这本书中,给出了终极解决方案,使用枚举。



package com.wuxiaolong.design.pattern.singleton;
/**
* Description:
* 在《Effective Java》这本书中,给出了终极解决方案,使用枚举。
* 使用枚举这种方式,不仅可以解决线程同步问题,还可以防止反序列化。
* @author 诸葛小猿
* @date 2020-08-05
*/
public enum Singleton5 {
INSTANCE;
/**
* 测试
*/
public static void main(String[] args) {
for(int i=0;i<100;i++){
new Thread(new Runnable() {
@Override
public void run() {
// 打印出内存地址,如果内存地址不一样,则生成了多个实例
System.out.println(Singleton5.INSTANCE.toString());
}
}).start();
}
}
}



使用枚举这种方式,不仅可以解决线程同步问题,还可以防止反序列化。



单例模式为什么要防止序列化和反序列化呢?在之前的几种形式中,依然可用通过反射(classForName)的形式再实例化出类。因为枚举没有构造方法,所以不能通过反射的形式再实例化类了。



虽然在语法上,这种写法是最完美的,但是这种方式个人不是很喜欢,感觉定义的和常量一样。



使用volatile关键字修饰的原因



在饿汉模式中,成员变量INSTANCE变量用volatile关键字修饰了,这是什么原因呢?



没有用volatile关键字修饰的,会导致这样一个问题:



public static Singleton3 getInstance(){
if(INSTANCE == null){
synchronized (Singleton3.class){
if(INSTANCE == null){
INSTANCE = new Singleton3();
}
}
}
return INSTANCE;
}



在线程执行到第2行的时候,可能出现虽然INSTANCE不为null,但INSTANCE引用指向的对象还没有完成初始化的情况。主要的原因是重排序。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

第5行的代码创建了一个对象,这一行代码可以分解成3个操作:



memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;  // 3:设置instance指向刚分配的内存地址



根源在于代码中的2和3之间,可能会被重排序。例如:



memory = allocate();  // 1:分配对象的内存空间
instance = memory;  // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象



这在单线程环境下是没有问题的,但在多线程环境下会出现问题:第二步和第三步的指令重排序后,A线程进入到上面synchronized代码块内部,执行到INSTANCE = new Singleton3()这一行的的第三个指令时instance = memory,这时B线程进来,INSTANCE != null,就会获得一个还没有被初始化的对象。



第二步和第三步的指令重排序不影响线程A的最终结果,但会导致线程B在判断出instance不为空,访问到一个还未初始化的对象。

所以只需要做一点小的修改(把instance声明为volatile型),就可以实现线程安全的延迟初始化。因为被volatile关键字修饰的变量是被禁止重排序的。



关注公众号,输入“java-summary”即可获得源码。



完成,收工!





传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。





发布于: 2020 年 08 月 06 日 阅读数: 62
用户头像

诸葛小猿

关注

我是诸葛小猿,一个彷徨中奋斗的互联网民工 2020.07.08 加入

公众号:foolish_man_xl

评论

发布
暂无评论
设计模式之——单例模式你真的会吗?