本人是一个刚刚上路的IT新兵,菜鸟!分享一点自己的见解,如果有错误的地方欢迎各位大佬莅临指导,如果这篇文章可以帮助到你,劳请大家点赞转发支持一下!
本篇文章讲解了多线程中常用的wait与notify方法,与软件开发的一种模式,单例模式,细细品读,你会为其中的细节着迷。
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。
假设有一个做饭程序
其中有三个线程,买菜线程,洗菜线程,炒菜线程。
这其中我们就希望这三个线程是按照
1️⃣先执行买菜线程。
2️⃣买菜线程执行完毕后,开始执行洗菜线程。
3️⃣洗菜线程执行完毕后,开始执行炒菜线程。
在多线程3该篇文章中,讲解了一个join()方法可以控制顺序,但是他的功效还是有限的,因此便提供了wait()与notify()方法。
wait()与notify()是Object类的方法,只要你不是内置类型与基本数据类型,都可以使用wait与notify。
wait的功能:
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常
锁对象必须与waite对象是同一个
wait 结束等待的条件:
notify的功能:
notify 要搭配 synchronized 来使用. 脱离 synchronized 使用 notify 会直接抛出异常
notify方法只是唤醒某一个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程。
【注意】
wait()与notify()
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
// 创建锁对象
Thread t1 = new Thread(() -> {
System.out.println("t1线程开始执行");
try {
synchronized (locker) {
System.out.println("t1线程开始等待t2线程");
// 锁对象必须与waite对象是同一个
locker.wait();
// 让t1线程开始等待,直到其他线程中调用locker.notify()方法
System.out.println("t2线程执行完毕,t1线程开始执行");
System.out.println("t1线程执行完毕");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程开始执行");
System.out.println("t2线程执行完毕");
synchronized (locker) {
locker.notify();
// 调用notify,唤醒使用locker.wait()方法
}
});
t1.start();
Thread.sleep(1000);
t2.start();
}
wait()与notifyAll()
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
System.out.println("t1线程开始执行");
synchronized (locker) {
try {
System.out.println("t1等待t4");
locker.wait();
System.out.println("t1执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2线程开始执行");
synchronized (locker) {
try {
System.out.println("t2等待t4");
locker.wait();
System.out.println("t2执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3 = new Thread(() -> {
System.out.println("t3线程开始执行");
synchronized (locker) {
try {
System.out.println("t3等待t4");
locker.wait();
System.out.println("t3执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t4 = new Thread(() -> {
System.out.println("t4线程开始执行");
synchronized (locker) {
System.out.println("t4执行完毕");
System.out.println("唤醒所有等待线程");
locker.notifyAll();
}
});
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
t4.start();
}
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种。
就以读小说
来举例解释饿汉模式
与懒汉模式
的区别
饿汉模式
饿汉模式,顾名思义,饿汉就会急切的想要吃饭,所以饿汉模式的特点就是很急,把能做的事都做完。
读小说,一次只能显示一页的内容,饿汉模式就会将整部小说都加载到内存当中,并显示一页内容。
懒汉模式
懒汉模式,顾名思义,懒汉很懒,只会做必须要做的事,非必要不做事。所以懒汉模式的特点就是只有当你真正用到的时候,我才会创造出来提供给你。
读小说,一次只能显示一页的内容,懒汉模式就会只加载一页的内容显示给用户,如果用户要翻下一页,那么再去加载。
饿汉模式
class Singleton {
// 唯一实例的本体
private static Singleton instance = new Singleton();
// 饿汉模式直接创建出了Singleton这个对象
// 禁止外部 new 该对象
private Singleton() {}
// 获取到对象的方法
public static Singleton getInstance() {
return instance;
}
}
懒汉模式
class SingletonLazy {
// 唯一实例的本体
private static SingletonLazy instance = null;
// 懒汉汉模式不会直接创建Singleton这个对象
// 禁止外部 new 该对象
private SingletonLazy() {}
// 获取到对象的方法
public static SingletonLazy getInstance() {
// 如果该对象为null,那么就创建对象
if(instance == null) {
instance = new SingletonLazy();
}
// 对象不为null,直接返回当前instance
return instance;
}
}
饿汉模式在多线程的情况下,并不会有bug出现。
下面就重点讲解多线程版懒汉模式。
上一篇文章讲解过多个线程修改同一共享数据时出现的bug。
其实此处的bug也类似。
大前提:此时instance为null
(提醒,这里创建对象的语句并未执行,因此instance仍为null)
这就是多线程版懒汉模式由线程抢占式执行
导致的BUG。
还有一个指令重排序
导致的BUG。
创建一个对象分为三步:
1️⃣创建内存、。
2️⃣调用构造方法、。
3️⃣把内存地址,赋给引用对象、。
有极小的概率,执行顺序会从1️⃣2️⃣3️⃣经过编译器的指令重排序编程1️⃣3️⃣2️⃣。
如果在多线程情况下,执行完1️⃣3️⃣,此时引用对象已经创建好了,还没来得及创建里面的构造方法。
系统就把CPU调度给其他线程了,其他线程再识别instance就不是null,就会返回instance,如果此时调用instance中的方法,就会出bug。
针对线程抢占式执行
导致的BUG,只需要加一个锁,便可以解决。
针对指令重排序
导致的BUG,只需要使用volatile
关键字修饰instance变量即可。
class SingletonLazy {
private static volatile SingletonLazy instance = null;
// 懒汉汉模式不会直接创建Singleton这个对象
// 禁止外部 new 该对象
private SingletonLazy() {}
// 获取到对象的方法
public static SingletonLazy getInstance() {
// 如果该对象为null,那么就创建对象
synchronized (SingletonLazy.class) {
// SingletonLazy.class得到调用该方法的对象
if(instance == null) {
instance = new SingletonLazy();
}
}
// 对象不为null,直接返回当前instance
return instance;
}
}
上述代码还可以优化。
上述加锁之后,无论instan是否为null,只要执行getInstance(),就一定会进入该锁,其他线程只能等待。
所以我们可以再加一个if语句,来判断他是否为null,
如果为null那么进入该锁,创建对象,为了线程安全,其他线程等也就等了。
如果不为null,那么线程就直接返回当前的instance,此时既保证了线程安全,也提高了效率。
class SingletonLazy {
private static volatile SingletonLazy instance = null;
// 懒汉汉模式不会直接创建Singleton这个对象
// 禁止外部 new 该对象
private SingletonLazy() {}
// 获取到对象的方法
public static SingletonLazy getInstance() {
// 如果该对象为null,那么就创建对象
if(instance == null) {
synchronized (SingletonLazy.class) {
// SingletonLazy.class得到调用该方法的对象
if (instance == null) {
instance = new SingletonLazy();
}
}
}
// 对象不为null,直接返回当前instance
return instance;
}
}
上述的两个if语句,缺一不可,大家可以去掉一个语句,在脑海中模拟一下,看看是否还会存在BUG。
如果有老铁不明白,可以评论区或私信我。
以上就是今天要分享的内容了,多线程的内容,既危险又迷人,这也是以后工作中一定会用到的内容,同志们加油吧!!
路漫漫,不止修身也养性。