JavaEE:多线程代码案例(单例模式)

10 篇文章 0 订阅


单例模式

一些概念

单例模式是一种设计模式.
设计模式类似于"棋谱",即固定套路,专门针对一些特定场景,给出的一些比较好的解决方案.

设计模式,就是针对编写代码过程中的"软性约束"(可以遵守,也可以不遵守).

框架,就是针对编写代码过程中的"硬性约束". 针对一些特定的场景问题,大佬们把基本的代码,已经写出来了,大部分的逻辑也都写好了,留出来一些空位,让你在空位上填充一些自定制的逻辑.

在开发中希望有的类在一个进程中,不应该存在多个实例(对象).
此时,就可以使用单例模式,限制某个类,只能有唯一实例.

比如一般来说,一个程序中,只有一个数据库,对应的mysql服务器只有一份,此时DataSource这个类,就没有必要创建出多个实例.

DataSource dataSource = new MysqlDataSource();

此时就可以使用单例模式,来描述DataSource,这样就可以避免不小心创建出多个实例了~

Java 中单例模式的实现有很多种写法,(不乏有一些,奇葩的做法)只介绍两种最主流的写法

饿汉模式

代码:

// 创建一个单例的类
// 饿汉方式实现
class Singleton {
	// 在类被加载的时候,就会创建出这个单例的实例
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
}

public class Demo15 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

运行结果:
在这里插入图片描述

此时只要不在其他代码中,new Singleton这个类,每次使用时都通过getInstance来获取,这样的话这个类就是单例的了~

但是万一别人一不小心重新new了对象,那不就完了?
是的,所以我们也要解决这个问题~

把构造方法用private 修饰可以啦~

class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    // 单例模式最关键的部分
    private Singleton() { }
}

private Singleton() { }这意味着在类的外面,就无法调用构造方法,也就无法创建实例了~

有人可能就要说了: 你不是private,那我通过反射拿到构造方法,通过反射api来调用,不就能new出实例了吗?

当然是可以的(大力出奇迹)…

单例模式,只能避免别人"失误",不能应对别人的"故意攻击".
(虽然是有办法规避故意攻击的,但是代价太大了)

懒汉模式

懒汉模式: 推迟了创建实例的时机,第一次使用的时候,才会创建实例.

能不搞就不搞,很多时候,就可以剩下一部分开销.
比如,有一个编辑器,打开一个非常大(1G)文本文档

  1. 一启动,就把所有的文本内容都读取到内存中,然后再显示到界面上[饿汉]
  2. 启动之后,只加载一小部分数据(一个屏幕能展示的最大数据),随着用户进行翻页操作,再按需加载剩下的内容.[懒汉]

懒汉模式代码:

// 懒汉模式实现的单例模式
class SingletonLazy {
    // 此处先把这个实例的引用设为null,先不急着创建实例
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    private SingletonLazy() { }
}

 class Demo16 {
     public static void main(String[] args) {
         SingletonLazy s1 = SingletonLazy.getInstance();
         SingletonLazy s2 = SingletonLazy.getInstance();
         System.out.println(s1 == s2);
     }
}

在这里插入图片描述
可以看到确实是单例模式.

public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

当首次调用getInstance时,因为现在引用为null,就会进入if分支,从而创建实例,后续再重复调用getInstance,都不会创建实例~

饿汉和懒汉是否是线程安全?

上述单例模式的讨论,只是"引子",接下来才是正题.

上述写的 饿汉 和 懒汉 单例模式代码,是否是线程安全的?
(如果多线程环境下,调用getInstance,是否会有问题呢?)

其实,饿汉没有问题,但是懒汉有问题~

多个线程针对一个变量进行修改,如果只是读取,则没有问题~
但下面的懒汉模式明显不是.

public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

先判定,再修改. 这种代码模式,是属于典型的线程不安全代码,判断和修改之间可能涉及到线程的切换.

假设现在有两个线程,t1和t2.
画个图更容易理解:
在这里插入图片描述

通过上图,我们可以看出,之所以出现线程安全问题,是因为在if判定和new操作之间出现了线程切换.
如果要解决上述问题.很简单,只要使用锁把if和new包裹到一起,变成一个"原子"就行啦~

初步改进

代码如下:

class SingletonLazy {
    // 此处先把这个实例的引用设为null,先不急着创建实例
    private static SingletonLazy instance = null;
    private static Object locker = new Object();
    
    public static SingletonLazy getInstance() {
        //使用锁,把if和new操作包裹到一起
        synchronized (locker) {
            if(instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }
    private SingletonLazy() { }
}

仔细想想.加锁之后,确实解决了线程安全问题,但是加锁同样也可能带来阻塞,影响到性能.

如果上述代码,已经new完对象了,if分支再也进不去了,后续的代码都是单纯的读操作.

此时,getInstance不加锁,也是线程安全的.那就没有必要加锁了~
因此,针对这个问题,还需要进一步的改进.

进一步改进

通过条件判断,在应该加锁的时候才加锁,不需要加锁的时候,直接跳过加锁.

class SingletonLazy {
    // 此处先把这个实例的引用设为null,先不急着创建实例
    private static SingletonLazy instance = null;

    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
    	// 不需要加锁,就跳过
        if(instance == null) {
            //使用锁,把if和new操作包裹到一起
            synchronized (locker) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() { }
}

这样就可以解决线程安全问题了…吗?

再次改进

instance = new SingletonLazy();

上述代码可能会因为指令重排序(一种编译器的优化方式),从而引发线程安全问题.

这行代码,看似只有一句,其实并不然,它干了三件事:

  1. 分配内存空间
  2. 执行构造方法
  3. 将内存空间的地址赋值给引用变量

编译器可能按照 1 2 3 的顺序来执行
也可能按照 1 3 2 的顺序来执行

对于单线程来说,先执行2,还是先执行3,本质上是一样的.
但是在多线程环境下,如果按照 1 3 2 的顺序来执行,那就可能会出现问题.

一图胜千言:
在这里插入图片描述

有没有办法来解决呢?
当然有啦,我们发现出现上述问题的原因是编译器优化,那我们让编译器不要优化这里就行了.
也就是使用"volatile"关键字.

把代码改成:

private static volatile SingletonLazy instance = null;

此时编译器就知道了,instance 是易失的,那么编译器围绕这个变量的优化就会非常克制.(不仅仅会在读取变量的优化上克制,也会在修改变量的优化上克制)

在这里插入图片描述
因此Java的volatile有两个功能

  1. 保证内存可见性
  2. 禁止指令重排序(针对赋值)

最终代码:

// 懒汉模式实现的单例模式
class SingletonLazy {
    // 此处先把这个实例的引用设为null,先不急着创建实例
    private static volatile SingletonLazy instance = null;

    private static Object locker = new Object();
    public static SingletonLazy getInstance() {
        if(instance == null) {
            //使用锁,把if和new操作包裹到一起
            synchronized (locker) {
                if(instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    private SingletonLazy() { }
}

总结

设计模式,就是针对编写代码过程中的"软性约束"

框架,就是针对编写代码过程中的"硬性约束".

在开发中希望有的类在一个进程中,不应该存在多个实例(对象).此时,就可以使用单例模式,限制某个类,只能有唯一实例.

饿汉模式(线程安全): 在类被加载的时候,就会创建出这个单例的实例.

饿汉模式代码:

// 饿汉方式实现
class Singleton {
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    // 单例模式最关键的部分
    private Singleton() { }
}

懒汉模式: 在程序第一次使用这个实例的时候,才会创建实例.

懒汉模式代码(线程不安全):

public static SingletonLazy getInstance() {
        if(instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }

懒汉模式代码(线程安全):

// 懒汉模式实现的单例模式
public class Singleton {
    // 声明,并用volatile修饰,保证在多线程环境下的有序性
    private volatile static Singleton instance = null;
    // 私有构造方法
    private Singleton () {}
    // 对外提供一个获取实例的方法,
    public static Singleton getInstance() {
        // 使用双重if检查, 降低锁竞争的频率
        if (instance == null) {
            // instance没有被实例化才去加锁
            synchronized (Singleton.class) {
                // 获取到锁后再检查一遍,确认instance没有被实例化后去真正的实例化
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Java的volatile有两个功能:

  1. 保证内存可见性
  2. 禁止指令重排序(针对赋值)

本文到这里就结束啦~

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月临水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值