目录
在前面两篇博客中,我们学习了进程和线程的概念,内容以及很重要的线程安全,接下来,我们再继续学习线程的其他内容。
1.wait和notify
由于线程之间是抢占式执⾏的,因此线程之间执⾏的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执⾏先后顺序。
完成这个协调⼯作,主要涉及到三个⽅法:
• wait()/wait(long timeout):让当前线程进⼊等待状态。
• notify()/notifyAll():唤醒在当前对象上等待的线程。
1.1 wait
wait做的事情:
• 使当前执⾏代码的线程进⾏等待.(把线程放到等待队列中)
• 释放当前的锁 -> 因此前提需要先加上锁
• 满⾜⼀定条件时被唤醒,重新尝试获取这个锁.
wait要搭配synchronized来使⽤.脱离synchronized使⽤wait会直接抛出异常.
wait结束等待的条件:
• 其他线程调⽤该对象的notify⽅法.
• wait等待时间超时(wait⽅法提供⼀个带有timeout参数的版本,来指定等待时间).
• 其他线程调⽤该等待线程的interrupted⽅法,导致wait抛出InterruptedException异常.
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
Object objet = new Object();
synchronized (objet){
System.out.println("wait 之前");
objet.wait(); //wait会持续等待下去,直到
System.out.println("wait 之后");
}
}
}
没有唤醒,就会一直等待。
注意:
1.调用wait不一定只有一个线程调用,N个线程都可以调用wait,此时,当有多个线程调用的时候,这些线程都会阻塞等待下去
2.wait的唤醒有两种方式,wait除了默认的无参版本外,还有一个带有参数的版本,带参数的版本就是指定超时时间,避免wait无休止的等待下去。
还是刚才一直循环的代码,加上时间参数后,就避免了死等,2s后执行了"wait之后"。
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
Object objet = new Object();
synchronized (objet){
System.out.println("wait 之前");
objet.wait(2000); //wait会持续等待下去,直到
System.out.println("wait 之后");
}
}
}
1.2 notify
notify()⽅法
notify⽅法是唤醒等待的线程.
• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其
它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈wait状态的线程。(并没有"先来后到")
• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏
完,也就是退出同步代码块之后才会释放对象锁
public class Demo20 {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("wait 之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 之后");
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {
System.out.println("进行通知");
object.notify();
}
});
t1.start();
t2.start();
}
}
t1先拿到锁,执行了"wait之前",然后wait释放锁,进入阻塞等待,释放锁后t2就可以拿到锁,执行sleep1s后,打印出"进行通知",完成后唤醒wait,t1重新拿到锁,打印出“wait之后”。
notify | notifyAll |
一次唤醒一个线程 | 一次唤醒全部线程 |
notifyAll唤醒全部线程后,虽然是同时唤醒多个线程,但是这多个线程需要竞争锁。所以并不是同时执⾏,⽽仍然是有先有后的执⾏。
public class Demo20 {
public static void main(String[] args) {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object) {
System.out.println("wait 之前");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 之后");
}
});
Thread t3 = new Thread(() -> {
synchronized (object) {
System.out.println("我在wait之前...");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("我在wait之后...");
}
});
Thread t4 = new Thread(() -> {
synchronized (object) {
System.out.println("hello fanzhendong");
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("fanfanfan");
}
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (object) {
System.out.println("进行通知");
object.notify();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
但是将notify改为notifyAll后,就发生了改变,唤醒了全部线程。
由于唤醒后,它们全部重新竞争锁,因此打印结果是随机的。
2.面试题
wait和sleep的对⽐
其实理论上wait和sleep完全是没有可⽐性的,因为⼀个是⽤于线程之间的通信的,⼀个是让线程阻塞⼀段时间,唯⼀的相同点就是都可以让线程放弃执⾏⼀段时间.
当然为了⾯试的⽬的,我们还是总结下:
1. wait需要搭配synchronized使⽤,sleep不需要。
2. wait是Object的⽅法,sleep是Thread的静态⽅法。
3.补充
以上三篇博客我们就基本讲完了进程和线程的基本内容。
接下来我们补充一个小的知识点,观察当前进程里的线程情况
多线程程序运行的时候,可以使用idea或者jcnsole来观察到该进程里的多线程情况。
jdk中带有的一个安装包
选中进程,点击连接就ok了。
4.总结
4.1 线程的优点
1. 创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
2. 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多
3. 线程占⽤的资源要⽐进程少很多
4. 能充分利⽤多处理器的可并⾏数量
5. 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
6. 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
7. I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
4.2 进程与线程的区别
1. 进程是系统进⾏资源分配和调度的⼀个独⽴单位,线程是程序执⾏的最⼩单位。
2. 进程有⾃⼰的内存地址空间,线程只独享指令流执⾏的必要资源,如寄存器和栈。
3. 由于同⼀进程的各线程间共享内存和⽂件资源,可以不通过内核进⾏直接通信。
4. 线程的创建、切换及终⽌效率更⾼。