前言
这篇文章,我们会介绍一下单例模式,但这里的单例模式,不是我们所说的设计模式,当然听到设计模式,大家一定都说,我当然知道设计模式了,有23种呢?一下子一顿输出,当然我这里说的单例模式还是跟设计模式有一些区别的,当然我不做概述,因为我也没咋个去了解过设计模式,我把大家拉回来,什么是多线程对的单例模式呢?看完我以下的解释相信你会明白的.
一.什么是单例模式
多线程环境下的单例模式需要保证只有一个实例对象被创建,并且可以在多线程环境下安全地访问该实例。
概念性的东西就是这样,把这段话,反复读,相信你也读不出来什么结果.简单来说的话,就是我现在只能娶一个老婆,不能娶多个老婆一样的意思,如果我娶多个老婆的话,在现在看来就是犯法的,当然了,小伙伴们,可不敢这样想,我想要后宫佳丽3000,这种想想就行了,哈哈
再来说说,在java中的单例模式的情况,Java 中的单例模式,借助java语法,保证某个类,只能够创建出一个实例,而不能new 多次.下面我们继续来看看在java中如何实现单例模式.
二.在java中的单例模式
怎么说呢?在Java中实现单例模式主要有以下几种:
- 饿汉式
- 懒汉式
- 双重检查锁
- 静态内部类
- 枚举
当然,我们或许没列举完,如果有其他的,也请各位小友,补充以下,当然我也不会全部介绍完,只会详细的介绍饿汉式和懒汉式.
2.1 饿汉式的介绍
饿汉式,大家通常想到饿这个词的,通常就会跟狼吞虎咽这个词语联系到一起,当然,我举个简单的例子,就拿取快递来说吧,你买了8件快递,今天到了2件,你马上就会去取快递.这就是饿汉式,遇事就马上下决定,真男人,绝不退缩,突出一个猛.
了解完什么是饿汉式,我们来看一看,饿汉式的代码实现.
//把这个类设置成单例的
public class Singleton {
private static Singleton instant = new Singleton();
//获取实例的方法
public static Singleton getInstance() {
return instant;
}
//禁止外部new实例
private Singleton() {
}
;
}
class ThreadDemo16{
public static void main(String[] args) {
//此时的s1
// 此时 s1 和 s2 是同一个对象!!
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 ==s2);
}
}
简单来说,我们是怎么去实现这个饿汉式的单例模式的呢,主要进行了俩点,你注意看
我们这一句:
private static Singleton instant = new Singleton();
是类内部把实例创建好.
另外一个步骤就是,我们在创建对象,进行实例化的时候,我们吧构造方法私有化.
private Singleton() {
}
这俩个步骤,我们自然就实现了单例模式,保证只有一个实例对象被创建.
当然我们既然谈到线程,我们自然就要讨论一个问题,你觉的.我们饿汉式的方式是线程安全的吗?如果你看过,我写的那个线程安全的类别时候,我们就知道,是否会发生线程安全的问题了.
总的来说,这上面几种情况都是没有出现的,因此我们初步判断饿汉式是线程安全的,但是也不完全是,有些资料说会在类加载的时候就创建实例,如果该实例很大或者初始化过程很耗时,会造成资源浪费。这点值不值得考究,我就不知道了,但从表面上来说,我们并没有从饿汉式中看出什么端倪.
2.2 懒汉式的介绍
懒汉式,我们自然就注重一个懒字,那我们为啥要叫懒汉式呢?其实吧,我再拿快递的例子去解释一下,假设你买了八个东西,但突然有一个快递到了,但是你不会立刻去取,你要等到它们都到了,一起去取,这就是懒汉式,理解了这个概念之后,我们就看一下java代码的概念.
import com.sun.org.apache.regexp.internal.RE;
class SingleLazy{
private static SingleLazy instance=null;
private SingleLazy(){}
public static SingleLazy getInstance(){
if (instance == null){
instance=new SingleLazy();
}
return instance;
}
}
public class ThreadDemo18 {
public static void main(String[] args) {
SingleLazy s1 = SingleLazy.getInstance();
SingleLazy s2 = SingleLazy.getInstance();
System.out.println(s1 == s2);
}
}
看到了懒汉式的代码.你就可以看出来,跟饿汉式的区别是,懒汉式,就是非必要时候,不创建对象.具体是哪一句呢?我这里给你举出来,你可以思考一下,我说的是不是对的.
public static SingleLazy getInstance(){
if (instance == null){
instance=new SingleLazy();
}
return instance;
这里加了判断语句,非必要的时候,不创建对象.
当然我们居然说了这种单例模式后,你来思考一下,它是不是线程安全的呢?其实简单来说,它是不是线程安全的,判断还是很简单的,因为你这样看,我们这里有判断条件,有判断条件,就涉及到赋值判断操作,你想象多线程环境下,线程1会对起进行判断,线程2也会进行判断,万一判断不是同时进行的,那不就整了个乌龙时间了吗?另外我们再对照着线程不安全的情况,仔细的思考一下.
所以说,懒汉式会破坏单例模式的原则,导致线程不安全,但是有没有方式解决呢?答案是有的,接下来我就会解释,我们怎么去避免这个问题,所以大家还是不要心急,细细听我道来.
三 懒汉式的单例模式,线程不安全的解决方式
3.1 造成线程不安全的原因
大家不要嫌我啰嗦,我再说明一下原因,这样我们才好说出解决策略
当多个线程同时访问getInstance()方法时,可能会出现竞态条件,即两个线程同时执行了if (instance == null)语句,导致创建了两个实例。这种情况下,线程单例模式就会失效。这个东西,就是我们说的会导致的线程安全问题.
接下来我们来提供一个解决方案.
3.2 解决方案
当然哈,我们开始说了是线程安全的原因,就是在创建对象的那一步,出现了,多线程竞争的关系,那么我们是不是可以直接在创建对象的那一步,加锁.代码如下:
public static SingleLazy getInstance(){
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
当然,我们这一步的操作,其实是保证了,创建对象一开始的原子性.
当然,加锁的操作,我们还是谨慎的,由于加锁的机制,当多个线程同时调用该方法时,只能有一个线程进入临界区创建实例,其他线程则被阻塞,可能会导致性能瓶颈。因此我们要避免就是非必要情况不加锁,就拿当前的这个例子来说,我们首次进入这个代码段的时候,如果对象已经创建了,我们就不进行加锁,对象没有创建,我们就加锁,因此外面就必须再嵌套一层循环.
public static SingletonLazy getInstance() {
// 这个条件, 判定是否要加锁. 如果对象已经有了, 就不必加锁了, 此时本身就是线程安全的.
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
当然大家不要被这个这俩个if条件所迷惑,一个避免过度加锁,第二个if判断就是避免多线程对其进行同时操作.
到这里,大家觉得我们线程安全的问题结束了吗?其实并没有,因为我们创建对象的时候,还会发生指令重排序.因此我们还必须进行下一步的优化,代码如下所示:
lass SingletonLazy {
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
// 这个条件, 判定是否要加锁. 如果对象已经有了, 就不必加锁了, 此时本身就是线程安全的.
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
我们来解释一下为什么会出现指令重排序问题,如果你看过我的上一篇线程安全的粗略详解,你就大概明白了,这个问题,当然我们,再来说一下,我们这里为啥会出现现在的问题.
因为创建实例的代码通常会被拆分成多个指令,包括分配内存空间、初始化对象、将对象赋值给变量等。如果编译器或处理器为了提高性能而进行指令重排序,可能会导致上述指令的执行顺序与代码的顺序不一致。这样,如果一个线程在另一个线程完成了对象赋值操作之前执行了if语句的第一个条件判断,就会错误地认为实例已经被创建,从而导致程序出错。
为了解决这个问题,可以使用volatile关键字来修饰单例实例变量,这样可以保证线程对该变量的读写操作都是原子的,并且禁止指令重排序文章来源:https://www.toymoban.com/news/detail-738478.html
3.3 总结
最后让我们小小的总结一下,我们解决的办法
1.加锁,把if 和new变成原子操作.
⒉.双重 if,减少不必要的加锁操作
3.使用volatile禁止指令重排序,保证后续线程肯定拿到的是完整对象文章来源地址https://www.toymoban.com/news/detail-738478.html
到了这里,关于线程安全之单例模式的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!