多线程的框架
学习内容:
多线程的等待唤醒机制的用法与多线程的通信技术
多线程的等待唤醒机制
1.什么是多线程的等待唤醒机制?
相当于对线程进行通信(多线程任务执行的切换)
如:多个线程对一个任务的不同任务进行切换(通信)
拿一个生活中的例子举例:
客户:打开美团(加锁) – 》客户点外卖(Thread0线程任务开始执行)–》客户进行等待并唤醒外卖小哥(wait – notify)
外卖小哥:等待收取订单状态(wait)-- 》收取订单(notify) --》 加速送货跑跑跑…(Thread1线程开启) --》美食终于送到了到了,外卖小哥开始等待下一单并敲门提醒客户要吃饭啦(wait–notify)
具体的代码实现:
public class Test1 {
public static void main(String[] args) {
Object obj = new Object();
//开启点餐线程
new Thread()
{
public void run()
{
synchronized (obj) {
//点餐
System.out.println("打开美团--》下单汉堡两份不要辣");
//等待外卖小哥送餐
try {obj.wait();}catch(InterruptedException e) {}
System.out.println("你在搞啥子,这么慢?下次要快");
}
}
}.start();
//开启外卖小哥线程
new Thread()
{
public void run()
{
synchronized (obj) {
//等待客户点餐
try {Thread.sleep(20);}catch(InterruptedException e) {}
System.out.println("...花了20分钟时终于做好,快马加鞭送货中....咚咚咚,汉堡来了,帅哥");
//唤醒客户 吃东西啦
obj.notify();
}
}
}.start();
}
}
输出结果为:
打开美团–》下单汉堡两份不要辣
…花了20分钟时终于做好,快马加鞭送货中…咚咚咚,汉堡来了,帅哥
你在搞啥子,这么慢?下次要快
注:因为美团客户端与美团外卖小哥属于同平台,美团的小哥不可能送饿了么的单。所以相当于加锁动作
2.多线程的等待唤醒机制有什么用?
更加好的处理多线程之间的任务,使得线程之间具有配合性。
需要这个线程启动的时候才让他启动,或者一个线程执行完毕才让下一个线程启动。
简单说就是多线程之间具有了通信性
3.如何使用多线程的等待唤醒机制?
首先了解等待唤醒机制的三个方法(Object中的方法)
1.wait():
2.notify()
3.notifyAll()
用法:
1.定义在锁中(监视器)
2.先wait后notify
重点:
掌握wait notify方法的使用
难点:
理解wait notify 与锁(synchronized)的关系,在与多线程进行通信的时候它们之间是如何配合进行工作的。
思考与总结:
①.wait与sleep都是让线程处于冻结状态,区别
1.sleep 释放执行权 不释放锁
2.wait 释放执行权并同时释放锁。
②.wait一定要定义在锁中,也就是先要有锁才能wait
3.必须要先wait后notify,顺序不能错
③锁对象可以是任意的。说明监视器(锁 wait notify notifyAll)方法是Object中的方法(任意对象调用的方式一定定义在Object中)如
class Demo
{
String name;
}
class S implements Runnable
{
Demo d;
S(Demo d)
public void run()
{
synchronzied(d)
{
d.wart();//wait定义在锁中
.....代码块
d.notify
}
}
}
**
实践:多线程通信–等待唤醒机制代码的应用
**
/*vlog视频ID:多线程之间的通信--等待唤醒机制
*
* 代码功能:
* 输出男生或者女生的姓名与性别,输出男生的姓名性别的时候,女生的输出则需要等待男生输出完毕(如此交替进行输出)
*
*
* 代码思路:
* 1.涉及多线程技术中的等待唤醒机制
* 2.如果num==0 就输出name=liang;sex=nan 否则就输出name=菲菲;sex=女女女女
* 3.加入boolean 进行判断资源(Resources)中是否有名字与姓名false则无,无则可以设置名字与姓名,唤醒输出线程
* 4.然后输出线程被唤醒进行输出
*
* 代码难点
*
*
*
*
* */
public class ThreadTest4 {
public static void main(String[] args) {
Resources1 r = new Resources1();
Input1 in = new Input1(r);
Output1 out =new Output1(r);
new Thread(in).start();
new Thread(out).start();
}
}
class Resources1
{
public String name;
public String sex;
boolean flag = false;
}
class Input1 implements Runnable
{
Resources1 r;
Input1(Resources1 r)
{
this.r = r;
}
public void run() {
//如果falg为真就执行
int num = 0;
while (true) {
synchronized (r) {
if (r.flag)
try {r.wait();}catch(InterruptedException e) {}
if (num==0) {
r.name = "liang" ;
r.sex = "nan";
}
else
{
r.name = "菲菲";
r.sex = "女女女";
}
r.flag = true;//把标记改为真,让输出线程运行输出命令
r.notify();//唤醒输出线程
}
num = (num+1)%2;
}
}
}
class Output1 implements Runnable
{
Resources1 r;
Output1(Resources1 r) {
this.r = r;
}
@Override
public void run() {
while (true) {
synchronized (r) {
if (!r.flag)
try {r.wait();}catch(InterruptedException e) {}
System.out.println("name:"+ r.name + ":" + "<-->sex:" + r.sex);
r.flag = false;//输出完毕后改为假让输入线程进行拿到姓名与性别
r.notify();//唤醒输入线程
}
}
}
}
输出结果为:
name:菲菲:sex:女女女
name:liang:sex:nan
name:菲菲:sex:女女女
name:liang:sex:nan
name:菲菲:sex:女女女
....
由此可以看出多线程的等待唤醒机制的执行过程
①判断姓名年龄的值是否为空–》②空就赋值–》③最后输出–唤醒–①②③…如此循环往复
多线程的等待唤醒机制代码的优化
将资源的任务封装在资源中
**将资源的任务封装在资源中 姓名 与输出--》名字姓名的私有化,在外部提供访问的方法保证了资源类的安全性)**
public class ThreadTest$1 {
public static void main(String[] args) {
//创建资源
Resourcest r =new Resourcest();
//创建任务,开启线程。执行路径
Outputt out = new Outputt(r);
Inputt in = new Inputt(r);
new Thread(in).start();
new Thread(out).start();
}
}
class Resourcest
{
//资源里的属性要可控(需要安全性)私有化
private String name;
private String sex;
boolean flag = false;
public synchronized void set(String name,String sex)//设置名字线程,变成同步函数
{
this.name = name;
this.sex = sex;
if (flag) {
try {this.wait();}catch(InterruptedException e){}
}
this.flag = true;
this.notify();
}
public synchronized void out() //输出线程,同步函数
{
if (!flag) {
try {this.wait();}catch(InterruptedException e){}
}
System.out.println("name:"+name+":sex:"+sex);
this.flag = false;
this.notify();
}
}
class Inputt implements Runnable
{
Resourcest r;
Inputt(Resourcest r)
{
this.r = r;
}
public void run() {
int num = 0;
while (true) {
//加锁后进行判断真假值
if (num==0) {
r.set("liang", "nan");
}
else
{
r.set("菲菲", "女女女");
}
num = ++num%2;
}
}
}
class Outputt implements Runnable
{
Resourcest r;
public Outputt(Resourcest r) {
this.r = r;
}
public void run()
{
while (true) {
r.out();
}
}
}
经典多线程的实例–生产消费者的实例改良版
代码示例:
public class ThreadPcDemo {
public static void main(String[] args) {
/*原理基本上跟输出姓名性别的程序代码一致
* 不过换成了烤鸭 生产一只 就要消费一只 多了一个 count++ 变量
* 不同点就是生产消费需要记录数字 生产一只就消费一只
*
*
*
*
* */
Kaoya k = new Kaoya();
Production p = new Production(k);
Consumption c = new Consumption(k);
new Thread(p).start();
new Thread(c).start();
new Thread(p).start();
new Thread(c).start();
}
}
class Kaoya
{
private int count = 1;
private String name;
//为假就生产 为真就消费
boolean flag = false;
//创建生产方法
public synchronized void Input(String name)
{
//进行标记的判断
while(flag)
{
try {this.wait();}catch(InterruptedException e) {}
}
//对this.anme 进行封装 以便更好地显示出来
this.name = name+count;
System.out.println("生产了" + this.name + ":" + Thread.currentThread().getName());
this.flag = true;
this.notifyAll();
count++;
}
// 创建消费方法
public synchronized void Output()
{
//
while (!flag)
try {this.wait();}catch(InterruptedException e) {}
System.out.println("消费了:" + this.name + Thread.currentThread().getName());
this.flag = false;
this.notifyAll();
}
}
class Production implements Runnable
{
Kaoya k;
Production(Kaoya k)
{
this.k = k;
}
public void run() {
while (true) {
k.Input("烤鸭");
}
}
}
class Consumption implements Runnable
{
Kaoya k;
Consumption(Kaoya k)
{
this.k = k;
}
public void run() {
while (true) {
k.Output();
}
}
}
*
* */
/*思考:为什么生产消费要用notifyAll?判断语句由if改为了while? --》而前面的线程通信输出 姓名,性别程序代码则不需要?
- 1.这次代码由于线程的增多 不再是单独的两个线程输出与输入了
- 2个输入线程与2个输出线程
- 假如在运行的时候输入线程(Input-Thread0)单纯的notify唤醒随机的一个线程,很有可能再次唤醒输入线程(Input-Thread1)导致这次线程没有输出,又进行了一次输入操作,就算运气好唤醒了输出线程也会导致程序做了大量的无用功(效率降低)。
- 而输入线程 只用if(flag)判断的话 只会判断第一次,在运行时就相当于跳过了判断语句直接对线程任务进行操作,无需等待。这样就会导致了程序的不可控性。(安全性大大降低)
- 简单说:1.不加notifyAll会导致程序效率低 2.不加while判断会导致安全性降低。(只限于一个任务两个以上的线程进行操作的时候,而单任务单线程无这些风险)
、
运行图解
总结:
- 生产消费需要循环一遍 才会出错
(第一遍循环输出线程还执有线程的执行权 所以必定唤醒的是输入线程的方法)
不用while判断标记会导致多生产多消费的情况出现:if只判断一次(会导致下次被唤醒不判断标记直接进行生产) while判断多次就没有这个问题,为false就等待
不用notifyAll会导致死锁 原因:notify会随机唤醒一个线程,如输入继续唤醒的是本类线程唤醒两次输入线程–输入线程(Thread0 Thread1)开始等待,然后输出线程继续随机连续唤醒两次(Thread2 Thread3)
输出线程也处于了冻结状态,自此4个线程全部处于了冻结状态。导致了死锁的发生。
*
学习产出:
学习时间:
8:00-11:30
12:30-13:00
14:00-19:00
20:00-21:00
晚上安排时间进行前面学习内容的复习
明日暂定学习内容:
1.剩下的多线程生产与消费者进行总结,易错难点进行思考()
2.多线程的守护线程的学习与总结,易错难点进行思考(什么是守护线程,为什么要用守护线程,怎么使用守护线程)
3.多线程的其他方法join进行学习,总结易错难点思考(什么是join方法 为什么要用join方法 join怎么使用)
学习了三篇代码
1.等待唤醒机制
2.等待唤醒资源的优化 与安全性的保证
3.一个任务被多个线程操作的时候通信安全问题
今日学习内容:
1.(等待唤醒机制)
2.(等待唤醒机制的优化–》将资源的任务封装在资源中 姓名 与输出–》名字姓名的私有化,在外部提供访问的方法保证了资源类的安全性)
3.(经典多线程实例–生产者消费者的实例)
4.(经典多线程的实例–生产消费者的实例改良版)
总结
1、 技术笔记 1 遍
2、CSDN 技术博客 1 篇
随堂笔记:随手记录易错点
- num++ 需要单独使用 不能 num = num++;会产生问题
2.同步函数的锁是this 所以函数内的调用对象也是 this.wait();保持对象的一致性
while 控制的是重复循环判断 所以不需要括号要准确
while(flag)
{
try {this.wait();}catch(InterruptedException e) {}
}
思考问题?为什么生产消费要用notifyAll --》而前面的线程通信 姓名 性别则不需要?
3.while(flag)
{
try {this.wait();}catch(InterruptedException e) {}
}
//对this.anme 进行封装 以便更好地显示出来
this.name = name+count;
System.out.println("生产了" + this.name + ":" + Thread.currentThread().getName());
count++;
this.name = name +count 放在开头的话就会重复,因为放再开头不会判断标记直接就会在Input就赋值 count=2 当消费者拿到的时候就会出现多消费的情况