AI摘要:本文介绍了JavaEE中的多线程案例,重点讲解了单例模式的实现方式,包括饿汉模式和懒汉模式,并分析了它们在多线程环境下的线程安全问题。通过加锁和使用volatile关键字,解决了懒汉模式的线程安全问题。同时,文章还简要介绍了阻塞队列的概念和特点,强调了其在生产者消费者模型中的应用。
Powered by AISummary.
多线程案例
一.单例模式
1.1单例模式介绍
单例模式是一个非常经典的设计模式
既然我们提到了设计模式,那么什么是设计模式呢?
设计模式,就是我们程序猿的棋谱,我们在实际开发过程中,如果遇到一些特定的经典场景,我们就可以按照这个解决方案来进行编码
校招中,考察的设计模式最常见的有两个
- 单例模式
- 工厂模式
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。
在有些场景中,我们希望有的类只能有一个对象,不能有多个,这时候我们就可以使用单例模式了。
看到这里大家可能会有一个疑问:我只new一次对象不就好了吗,为什么要这么复杂?
单例模式的重要性
我们需要让编译器帮我们监督,确保这个类不会new出多个对象。如果只靠注释标明这个类不能创建多个对象那是万万不能的。
这样的思想方法,我们之前学习过的很多地方也都有涉及,比如下面这四个。
- final
- interface
- @Override
- throws.
但是在语法层面上,并没有对单例模式做出支持,所以我们就只能通过一些编程技巧,来达成类似的效果。
接下来我们详细介绍一下单例模式具体的实现方式, 分成 "饿汉" 和 "懒汉" 两种.
1.2饿汉模式(基础)
1.2.1代码描述
- 私有构造函数:类
Singleton
有一个私有构造函数,这意味着在类外部无法使用new
关键字创建其实例。这强制要求获取Singleton
类的唯一实例只能通过getInstance
方法来实现。 - 静态实例变量:该类有一个私有的静态实例变量
instance
,它保存了该类的唯一实例。它在类加载时初始化,因为它在一行中声明和实例化。 - 静态
getInstance
方法:getInstance
方法是公共的和静态的,允许外部代码获取Singleton
类的唯一实例。该方法返回instance
变量。
1.2.2代码实现
♾️ java 代码:public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {}
}
1.2.3 引用测试
♾️ java 代码:public class Demo1 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
System.out.println(singleton1.equals(singleton2));
}
}
调用代码后我们发现,singleton1和singleton2实际上引用了同一个对象。这就是Singleton模式的主要作用:确保在整个应用程序中只有一个实例,并提供一种全局的方式来访问它。
1.3饱汉模式(基础)
1.3.1 代码描述
- 私有构造函数:类
SingletonLazy
有一个私有构造函数,这阻止了在类外部通过new
关键字创建类的实例。 - 静态实例变量:类中有一个私有的静态成员变量
INSTANCE
,用于存储类的唯一实例。初始时,它被设置为null
。 - 静态
getInstance
方法:getInstance
方法是公共的和静态的,用于获取SingletonLazy
类的唯一实例。在这个方法中,检查INSTANCE
是否为null
。如果INSTANCE
是null
,则创建一个新的SingletonLazy
实例并将其分配给INSTANCE
,然后返回该实例。如果INSTANCE
不是null
,则直接返回已经存在的实例。
1.3.2代码实现
♾️ java 代码:public class SingletonLazy {
private static SingletonLazy INSTANCE = null;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if(INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
return INSTANCE;
}
}
1.3.3 引用测试
♾️ java 代码:public class Demo1 {
public static void main(String[] args) {
SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
System.out.println(singletonLazy1 == singletonLazy2);
System.out.println(singletonLazy1.equals(singletonLazy2));
}
}
1.4 线程安全问题
回看我们这一章的标题”多线程“,我们来思考一下这两种写法,是否存在线程安全问题?(多个线程同时调用getinstance是否能出现问题?)
那么让我们回顾一下怎样才能构成线程安全问题;
如果多个线程,同时修改同一个变量就可能构成线程安全问题
如果多个线程,同时读取一个变量就不会构成线程安全难问题
看到这里,大家心里可能已经有了答案:饿汉模式没有线程安全问题、懒汉模式具有线程安全问题。
因为饿汉模式只进行了读取,没有进行修改:而懒汉模式既会读取又会修改。
那么我们应该怎么做呢?加锁!
♾️ java 代码:public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
}
return INSTANCE;
}
我们按照这个方式给代码加锁之后,确是能解决部分多线程修改的问题,但是一旦代码这么写,后续每次调用getinstance,都需要先加锁。但是实际上,懒汉模式中线程安全问题只出现在最开始还没有new对象的时候。一旦对象new出来了,后续多线程调用getinstance就只有读取,就不会有线程安全问题了。
所以我们应该如何修改提升代码的性能呢?
我们上面提到,懒汉模式只有在第一次new对象的时候才会加锁,那么我们可以在加锁操作前进行一次if判断,如果对象已经创建了,线程就安全了,此时就不需要加锁了
♾️ java 代码: public static SingletonLazy getInstance() {
if(INSTANCE == null) {
synchronized (SingletonLazy.class) {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
}
}
return INSTANCE;
}
第一个iff用来判断是否需要加锁
第二个if用来判断是否需要new对象
我们平常很少写出这样的语句。在这里只不过是凑巧,这俩判断是一样的写法。
代码写到这里,我们还需要注意一个非常重要的问题指令重排序
我们之前提到过,编译器为了提高执行效率,可能调整原代码的执行顺序。
就比如我们去超市,想买白菜→韭菜→西红柿→蒜薹
但是实际中按这样的顺序去超市购买会来来回回的跑,非常复杂。
编译器就可能帮我们优化成西红柿→蒜薹→白菜→韭菜
通常情况下,指令重排序能够在保证逻辑不变的前提下,优化程序的执行速度
但是在我们上面的这个代码中,就可能会出现一些问题
new操作,是可能触发指令重排序的
new操作可以拆分为散步:
- 申请内存空间
- 在内存空间上构造对象(构造方法)
- 把内存的地址,赋值给instance引用
可以按照1 2 3的顺序来执行,也可以按照1 3 2的顺序来执行。(1一定是最先执行的)
其实在单线程中哪一种执行方式都是无所谓的,但是在多线程中就会有问题了
eg执行顺序①③②
当线程一执行完1和3时,Instance变成非NULL了,但是Instance指向的是一个还没有初始化的对象
此时线程二开始执行,由于我们第一个 if(INSTANCE == null)
没有加锁,所以可以线程二判定instance == NULL不成立,于是线程二返回INSTANCE。进一步线程二线程代码就可能访问INSTANCE里的方法和属性
所以我们针对这个代码加一个volatile即可解决。
♾️ java 代码:public class SingletonLazy {
private static volatile SingletonLazy INSTANCE = null;
private static SingletonLazy() {}
public static SingletonLazy getInstance() {
if(INSTANCE == null) {
synchronized (SingletonLazy.class) {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
}
}
return INSTANCE;
}
}
1.5阻塞队列
阻塞队列是一种特殊的队列,其最大的意义就是实现“生产者消费者模型”
- 线程安全
带有阻塞特性
- 如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往队列里添加元素为止
- 如果队列为满,继续如队列,也会发生阻塞,阻塞到其他线程从队列中取走元素为止