1

【JavaEE初阶】多线程 _ 基础篇 _ 单例模式(案例一)

 2 years ago
source link: https://blog.51cto.com/u_15747763/5609410
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前面已经向各位铁铁们介绍了 关于多线程的一些基本的知识点,为了可以让大家更好的理解 多线程的一些相关的特性,这篇博客 就会结合着一些具体的代码案例 来向大家介绍~

这篇博客就向大家介绍 关于多线程的第一个案例 —— 单例模式~

一、单例模式的概念

所谓单例模式,是一种常见的设计模式~

单例模式希望:有些对象,在一个程序中应该只有唯一一个实例,就可以使用单例模式~

换句话说,在单例模式下,对象的实例化被限制了,只能创建一个,多了也创建不了~

如果关是靠人来保证,是不靠谱的,所以就借助语法,强行限制不可以创建多个实例,避免程序员不小心出错~

设计模式:类似于棋谱,是 前辈们已经总结好了的一些固定套路,照着棋谱来下棋,棋就不会下的太差,这就提高了下限(比如说,象棋 ......)~

二、单例模式的简单实现

在 Java 里面的单例模式,有很多种实现方式,在这里主要介绍两个大类:饿汉模式、懒汉模式~

饿汉模式 和 懒汉模式 两种模式,描述了创建实例的时间~

这两种模式,并没有什么高低贵贱之分,不是 现实生活中的 "贬义词",相反 在计算机领域,"懒"还是一个褒义词,这个字意味着计算机的性能比较高~

在计算机中,这种思想是很常见的~

比如说,想要了解某个资料(大文件 10G,存放在硬盘中),那么 此时使用某个编辑器,打开文件,就会出现两种情况:

  1. [饿汉] 把 10G 都读到内存中,读取完毕之后 再允许用户进行查看和修改~
  2. [懒汉] 只读取一点点(当前屏幕能显示出的范围),随着用户翻页,继续再读后续内容~

所以说,如果是 饿汉模式,那么显示所需要的时间就会比较多;如果是 懒汉模式,那么显示的时间就会比较低,效率就会比较高(也有可能 用户打开文件以后,只看了两眼就关了,后面的大部分都没有读,那么内存读了那么多也没有意义)~

所以,通常我们都认为,懒汉模式 要比 饿汉模式 更高效~

当然,还有 刷抖音、看小说、看微信、上网浏览内容 ...... 的时候,都是借鉴了 懒汉模式~

2.1 饿汉模式

饿汉模式 的意思是,程序一旦启动,就会立刻创建实例~

这就好比,一个饿了的人,看到一张饼,就会迫不及待的往嘴里塞,我们把它叫做 "饿汉"~

static关键字 的来龙去脉:

static 名字叫做 "静态",但是实际上和字面意思没有任何的关系,这是一个历史遗留的问题~

实际表示的含义是 "类属性 / 类方法",同样的,我们把 不是静态的普通的成员叫做 "实例属性 / 实例方法"~

Java 里面叫做 "静态" 是因为 C++ 里面表示 "类属性",就是用 static,Java 是从 C++ 那里抄来的;而 C++ 则是因为 引入面向对象之后。需要搞一个方式来定义类属性,就需要引入一个关键字,但是引入新的关键字 成本极高,所以关键字设计者的大佬们 目光就盯住了 旧的关键字~

于是,static 就中招了,static 原来表示的是 变量放到静态内存区,但是随着时间的推移,系统的进化,已经没有 "静态内存区" 这个说法了,但是 static关键字 还在,于是 "旧瓶装新酒",就用来表示 "类属性 / 类方法" 了~

此时,"类属性 / 类方法" 和 静不静态 字面意思上没有啥关系,只是 随便找一个之前旧的关键字,现在没啥用了,赋予一个新的功能,仅此而已~

引入新的关键字成本极高的原因:

写代码的时候,变量名不能和关键字一样,当引入新的关键字的时候,不可以确定 其他人是不是已经引入了 新的关键字 作为变量名(全世界的 C++ 代码那么多 ......)~

更大的可能是 新的关键字一引入,就会导致已有的一些代码 编译就会失败~

然后这把火就会烧到了 关键字设计者 的身上~

而类属性就长在类对象上,类对象在整个程序中只有唯一一个实例(JVM保证的) ,所以说 类的静态成员就只有唯一一个了~

package thread;

//单例模式,饿汉的方式
class Singleton {
private static Singleton instance = new Singleton();

//后续如果需要这个实例,就需要统一基于 getInstance 方法来获取 实力独苗,不要去 new 了~
public static Singleton getInstance(){
return instance;
}

//构造方法设为 私有,此时 其他的类想来 new 就不可以了 (通过 编译器的规则来确保只有一个实例对象)~
private Singleton(){ }
}
public class Demo19 {
public static void main(String[] args){
//饿汉模式 的调用
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
}
}

运行结果:

【JavaEE初阶】多线程 _ 基础篇 _ 单例模式(案例一)_饿汉模式

如果不小心 想创建另一个实例,那么就会编译报错了:

【JavaEE初阶】多线程 _ 基础篇 _ 单例模式(案例一)_饿汉模式_02

使用 静态成员表示实例(唯一性) + 让构造方法设为私有(堵住了 new 创建新实例的口子)~

按照上面的代码,当 Singleton类 被加载的时候,就会执行到 实例化操作,此时 实例化的时机非常早(非常迫切的感觉),我们把它称为 饿汉模式~

对于饿汉模式来说,在多线程的情况下,多次调用的是 getInstance() 方法, 而 这个方法只是一个 读操作,对于多线程读操作来说,是线程安全的~

2.2 懒汉模式

懒汉模式 的意思是,程序启动,先不着急创建实例,等到真正用的时候,再创建实例~

这个也很形象,比较 "懒",不想干活,等到需要的时候再去干活~

//懒汉模式的实现
class SingletLazy {
//此处没有立即创建实例
private static SingletLazy instance = null;

//当首次调用 getInstance() 的时候,才会创建实例
public static SingletLazy getInstance(){
if (instance == null) {
instance = new SingletLazy();
}
return instance;
}

//同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
private SingletLazy(){ }
}

在多线程的情况下,懒汉模式,多次调用 getInstance() 方法,而且涉及到了 两次读操作(读出 instance 是否为空,读出 返回的 instance 值)和 一次写操作(修改 instance 变量的值),这是线程不安全的~

当然,一旦实例创建好了以后,后续 if 条件语句就进不去了,此时也就是 全是读操作了,也就线程安全了~

既然已经明确了,懒汉模式 是线程不安全的,那么 如何解决懒汉模式线程不安全的问题呢?

办法就是 需要加锁!!!

通过 加锁 来保证 "判断" 和 "修改" 这组操作是原子的~

//懒汉模式的实现
class SingletLazy {
//此处没有立即创建实例
private static SingletLazy instance = null;

//当首次调用 getInstance() 的时候,才会创建实例
public static SingletLazy getInstance(){
synchronized (SingletLazy.class) {
if (instance == null) {
instance = new SingletLazy();
}
}
return instance;
}

//同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
private SingletLazy(){ }
}

懒汉模式,只是在初始情况下,才会有线程不安全的问题,一旦实例创建好了以后,此时就安全了~

所以说,在后续调用 getInstance 的时候就不应该尝试加锁了~

如果使用上述的代码,无论 instance 是否为空(是否初始化),都会进行加锁,使得锁竞争加剧,消耗一些没有必要消耗的资源,就会很影响效率了~

在加锁之前,还需要进行判断 instance 是否为空(是否初始化),如果为空才会进行加锁:

//懒汉模式的实现
class SingletLazy {
//此处没有立即创建实例
private static SingletLazy instance = null;

//当首次调用 getInstance() 的时候,才会创建实例
public static SingletLazy getInstance(){
if (instance == null) {
synchronized (SingletLazy.class) {
if (instance == null) {
instance = new SingletLazy();
}
}
}
return instance;
}

//同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
private SingletLazy(){ }
}

外层 if 判定当前是否已经初始化好,如果未初始化好,就尝试加锁;如果已经初始化好,那么就接着往下走~

里层 if 是在多个线程尝试初始化,产生了锁竞争,这些参与竞争的线程 拿到锁之后,再进一步确认,是否真的要初始化~

当然,上面的代码操作还是有一些问题的 —— 有的线程在读,有的线程在写~

这就联想起了 —— 内存可见性问题~

其实,这里的情况 和 之前的情况还不一样,每一个线程都有自己的上下文,都有自己的寄存器内容,按理来说 是不应该会出现优化的~

但是,实际上也不好说,也并不能保证 编译器优化 是啥样的过程~

因此,给 instance 加上 volatile 是更加稳健的做法~

如果不加 volatile 不一定会有问题,但是 稳妥起见,还是加上更好~

//懒汉模式的实现
class SingletLazy {
//此处没有立即创建实例
volatile private static SingletLazy instance = null;

//当首次调用 getInstance() 的时候,才会创建实例
public static SingletLazy getInstance(){
if (instance == null) {
synchronized (SingletLazy.class) {
if (instance == null) {
instance = new SingletLazy();
}
}
}
return instance;
}

//同理,创建构造方法 SingletLazy(),防止该类实例化其他的对象
private SingletLazy(){ }
}

懒汉模式 线程的三个要点:

  1. volatile(不加可能是错的,但加了一定是正确的)

关于 单例模式 的内容就先介绍到这里了,当然,在 Java 中也有许多其他的方式 也可以实现单例模式,如 基于枚举、基于内部类 等等~

如果感觉这一篇博客对你有帮助的话,可以一键三连走一波,非常非常感谢啦 ~

【JavaEE初阶】多线程 _ 基础篇 _ 单例模式(案例一)_饿汉模式_03
【JavaEE初阶】多线程 _ 基础篇 _ 单例模式(案例一)_懒汉模式_04

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK