前言
本篇介绍的是wait与notify方法,通过wait来顺序控制执行一些代码,了解单例模式,进行单例模式的简单实现,介绍饿汉模式下出现线程不安全的问题与解决;如有错误,请在评论区指正,让我们一起交流,共同进步!
文章目录
本文开始
1. 认识 wait 与 notify
为什么要有wait 与 notify ?
线程调度是无序的,但是一定场景下,希望线程是有序执行的,这就可以使用wait 和 notify;
之前了解了,join能够等待,来控制顺序,但是其功效有限;而wait也是等待的作用,但又不相同;
1.1 wait,notify的使用
wait: 发现条件不满足 / 时机不成熟,就会阻塞等待;
notify: 其他线程构造了一个成熟的条件,就可以唤醒等待线程;
例如:
A去ATM取钱,A进去取钱,发现ATM中没有钱,A就会wait释放锁并阻塞等待,当另外的线程,把ATM冲上钱,就可以唤醒A,让A进行取钱操作;
wait与notify前提使用条件:
wait 和 notify 是Object 方法,只要是类对象,就可以使用wait,notify;
1.2 使用 wait 必须加锁
不加锁代码:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
object.wait();//中断就会报异常
}
结果报错:
【注】illegal Monitor State Exception: 非法的,监视器(synchronized: 监视器锁),状态,异常;=》非法的锁状态异常;
object.wait() : wait方法做三件事
① 解锁: 这就说明,使用wait必须加锁(必须写的synchronized代码块中),才能解锁;
【注】加锁对象必须和wait的对象是同一个
② 阻塞等待
③ 当收到通知的时候,就唤醒,同时尝试重新获取锁;
join 与 wait 的区别:
前提有两个线程t1,t2;
① join等待一定是串行的,只能t2先执行完,再继续执行t1;
② wait和notify, 中的wait只是等待线程中的一部分;可以先让t2执行完一部分,再执行t1, t1执行一部分,t2再继续执行;(可以调整执行的顺序)
wait 使用的两种方式
使用wait会阻塞等待让线程进入WAITING状态;
① 带参数 的wait(时间):带参数的,指最大等待时间;(等到最大时间,没人通知,就会自己唤醒自己)
② 不带参数 的wait : 会一直等待;(只能等待被唤醒或者被中断)
1.3 notify 也需要加锁
前提:必须先执行wait, 然后才能有notify; (不然没锁,怎么解锁) 虽然没有唤醒wait,但是不会让代码出现异常;
notifyAll : 唤醒多个线程;
【注】如果有多个线程调用wait,notify只能随机唤醒一个;而notifyAll 可以把线程全部唤醒,此时多个线程重新竞争锁,再依次执行;
1.4 wait 与 sleep 的区别?
① wait用于线程通信,sleep用于阻塞线程;
② 设计的初心不同,wait解决线程之间顺序控制;sleep只能让当前线程休眠一会;
③ wait 是Object的方法,sleep是Thread的静态方法;
④ wait使用需要配合synchronized使用,sleep不需要;
2.单例模式
什么是单例模式?
单例模式,是一种经典的设计模式;类似于玩游戏的攻略 / 下棋的棋谱 ,有一些固定的招式;
2.1 单例模式的介绍
单例:指单个实例,一个程序中,某个类,只能创建出一个实例(对象),不能创建多个对象;
java中的单例模式,借助java语法,保证某个类,只能创建出一个实例,而不能new多次;
2.2 java语法中如何实现单例模式
这里介绍两种方式:
① 饿汉模式
从容的状态:比如吃饭洗碗,吃完饭马上洗碗;
② 懒汉模式
急迫的状态:比如吃饭洗碗,吃完饭,先不着急洗碗,等下一次用的再洗碗;
例如计算机读取硬盘中的文件内容,并显示;
饿汉:把文件所有内容全部读到内存中,并显示;
懒汉:只读取文件中的一部分,能填充当前屏幕,如果翻页,再读取其他文件内容,如果不翻页,就不用再读;
2.3 实现单例模式
此处把Singleton设置为单例的
2.3.1 饿汉模式实现单例
饿汉模式:开始直接创建实例,不管用不用;
【注】
① 唯一实例本体使用static修饰
② 获取实例方法提供get方法
③ 禁止外部再创建实例 使用private修饰无参构造(private修饰必须提供get方法来获取实例)
class Singleton {
//唯一实例本体
private static Singleton instance = new Singleton();
//被static修饰,该属性是类的属性 =》 类对象
//JVM 中,每个类的类对象只有唯一一份,类对象的成员也是唯一一份
//private修饰,需要get方法,外部才能获取到
//获取到实例的方法
public static Singleton getInstance() {
return instance;
}
//禁止外部new 实例
private Singleton() {
}
}
测试代码:
public static void main(String[] args) {
//此时singleton1,singleton2调用的是同一个对象
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
//为了防止new一个新的对象,给无参构造用private修饰
//Singleton singleton3 = new Singleton();//此时不能new了
}
2.3.2 懒汉模式实现单例模式
懒汉模式:开始不创建实例,等到使用的时候才会创建;
class SingletonLazy {
//不马上创建实例
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//等到调用的时候,才创建实例
if(instance == null) {
instance = new SingletonLazy();
}
//如果有了实例直接返回,也保证了只要一个实例
return instance;
}
private SingletonLazy() {}
}
测试:
public static void main(String[] args) {
SingletonLazy singletonLazy1 = SingletonLazy.getInstance();
SingletonLazy singletonLazy2 = SingletonLazy.getInstance();
//比较singletonLazy1 == singletonLazy2,相等就是同一个对象
System.out.println(singletonLazy1 == singletonLazy2);
}
2.3.3 懒汉模式下产生的线程不安全
通过上述两个方式实现单例模式,它们是线程安全的吗?
可以通过分析线程不安全产生的原因来判断;
饿汉模式:获取实例方法,只是读操作,不会修改变量,线程是安全的;
懒汉模式:多线程下,无法保证创建对象的唯一性, 线程是不安全的;
懒汉模式下两个线程调用getInstance:
问题1 产生原因:
t1线程先执行 if 判断,准备创建实例但还没有创建,此时t2 进行 if 判断,进入代码块也准备拆改那就实例,这就造成创建多个实例的现象;=》相当于多个线程修改同一个变量了;
( if 和 new不是原子的,会造成创建多个实例对象;创建过多会非常占内存空间,导致线程不安全;)
解决问题1方式:
加锁 保证 if 和 new 的原子性;
【注】加锁不一定线程安全,必须保证锁加对位置;
代码实现:
public static SingletonLazy getInstance() {
//给当前类对象加锁
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
问题2:
每次调用genInstance都会触发锁竞争,让加锁操作变得低效;
通过问题可以发现,懒汉模式的线程不安全,只出现在首次创建对象,一定对象创建完毕,后续调用getInstance,就只是读操作,就不会产生线程安全;=》只给第一次加锁即可;
解决问题2 方式:使用双重 if 解决
① 外层:判断是否要加锁
② 内层:判断是否创建对象
代码实现:
public static SingletonLazy getInstance() {
//判断是否要加锁,如果有对象就不必加锁,此时是线程安全的
//外层判断是否要加锁
if(instance == null) {
synchronized (SingletonLazy.class) {
//等到调用的时候,才创建实例
//里层判断是否创建对象
if(instance == null) {
instance = new SingletonLazy();
}
}
}
//如果有了实例直接返回,也保证了只要一个实例
return instance;
}
问题3:
多线程调用getInstance 进入操作会遇到new SingletonLazy操作, 创建实例会产生指令重排序问题;
创建实例一般有三步操作:
① 创建内存
② 调用构造方法 (初始化)
③ 把内存地址,赋给引用
原因:但是②③顺序不能够确定,假设两个线程t1,t2; t1可能先执行③顺序操作,此时t2执行 if判断,发现instance不为空,直接返回,而t1没有初始化变量等,t2只是拿到了不完整的实例对象,直接使用就会造成线程安全 ;
【注】上述指令重排序发生的概率很小,但不能忽视;
解决问题3 :使用volatile 禁止指令重排序
volatile private static SingletonLazy instance = null;
最后完整的代码:
public static SingletonLazy getInstance() {
//使用volatile: 防止指令重排序
volatile private static SingletonLazy instance = null;
//外层判断是否要加锁
if(instance == null) {
synchronized (SingletonLazy.class) {
//等到调用的时候,才创建实例
//里层判断是否创建对象
if(instance == null) {
instance = new SingletonLazy();
}
}
}
//如果有了实例直接返回,也保证了只要一个实例
return instance;
}
2.3.4 小结懒汉模式下产生的线程不安全
解决懒汉模式下的线程不安全方式:
① 加锁把 if 和 new操作变成原子的;
② 双重 if 减少不必要的加锁操作;
③ 使用 volatile 禁止指令重排序,保证后续线程能拿到完整对象;
总结
✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!