线程间同步通信问题

目录

线程间的同步有什么问题?

线程间的同步通信

join方法

wait()/notify()/notifyAll()方法

wait()

notify()

notifyAll()

使用方法

为什么wait()/notify()/notifyAll()需要配合synchronized?

线程饥饿

总结


线程间的同步有什么问题?

看看如下代码:

public class Test {
    public static void main(String[] args) {
        Thread th1 = new Thread(){
            @Override
            public void run() {
                System.out.println("开始加载");
                for (int i = 0; i <= 100; i+=10) {
                    System.out.println("图片加载中:"+i+"%");
                }
                System.out.println("加载完成");
            }
        };
        Thread th2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("等待显示");
                System.out.println("显示完成");

            }
        });
        th1.start();
        th2.start();
    }
}

显示结果:

开始加载
等待显示
显示完成
图片加载中:0%
图片加载中:10%
图片加载中:20%
图片加载中:30%
图片加载中:40%
图片加载中:50%
图片加载中:60%
图片加载中:70%
图片加载中:80%
图片加载中:90%
图片加载中:100%
加载完成

有什么问题?可以看到标红部分开始显示和显示完成在图片加载之前了,也就是th1线程在进行到一半,th2线程突然插入了,但题目想要的是th1完成后,th2再开始运行。换句话说,th1没有结束前,我想要th2一直处于阻塞状态。但是两个独立运行的线程怎么知道对方进行到哪了?这就需要用到线程间的同步通信

线程间的同步通信

  •  线程间的同步通信,就是让等待(B)线程主动进入等待(阻塞)状态,等唤醒(A)线程执行了对应操作后,再对等待(B)线程进行唤醒,等待(B)线程继续工作。因此,要通信的线程间必须要有联系,在面向对象语言中,这个联系就只能是一个相同的对象

join方法

  • 我们通常在B线程内调用A线程的join方法,目的是等待A线程结束再继续B线程下面的内容

如下:

public class Test {
    public static void main(String[] args) {
        Thread th1 = new Thread(){
            @Override
            public void run() {
                System.out.println("开始加载");
                for (int i = 0; i <= 100; i+=10) {
                    System.out.println("图片加载中:"+i+"%");
                }
                System.out.println("加载完成");
            }
        };
        Thread th2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("等待加载");
                //这里加入了th1的join方法,需要捕获异常
                try {
                    th1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("显示完成");
            }
        });
        th1.start();
        th2.start();
    }
}

结果如下:

等待加载
开始加载
图片加载中:0%
图片加载中:10%
图片加载中:20%
图片加载中:30%
图片加载中:40%
图片加载中:50%
图片加载中:60%
图片加载中:70%
图片加载中:80%
图片加载中:90%
图片加载中:100%
加载完成
显示完成

多运行几次可以发现等待加载和开始加载的顺序可能改变,但是显示完成一定是在最末尾,因为等待加载这句话在th1.join()前,两个线程都在就绪状态争抢时间片,所以可能会有不同,但是th2调用了th1的join()方法后,会自动进入阻塞状态,等待th1结束。当然,可以在join内填入一个毫秒时间(long类型),表示最多等待th1多久时间,防止死锁

但是用join方法有一个问题:现在用的是匿名内部类定义线程,两个线程是在同一个类中声明,可以互相调用,但如果不是匿名内部类呢?如果是使用了继承Thread类的线程或者实现Runnable方法的线程,是无法调用对方在主方法内定义的实例化对象的,可见join()方法不是万能的。

wait()/notify()/notifyAll()方法

wait()/notify()/notifyAll()这三个方法属于Object类,所以想要调用必须得实例一个Object对象,首先来看看三个方法的用法:

wait()

  • 调用后使线程进入阻塞状态,但需要先获得一个当前对象的状态锁,等进入阻塞状态后立马释放锁,当线程被唤醒时,也需要重新竞争锁,拿到了锁才可以回到就绪状态。

notify()

  • 调用后使阻塞状态中因为同Object类对象的wait()方法进入阻塞状态的一个线程重新回到就绪状态(按照线程优先级唤醒,如果相同优先级,则随机唤醒)。使用完notify()不会马上释放锁,要等相应的synchronized代码块内的代码执行完毕才会释放锁。

notifyAll()

  • 使用原理同notify(),但是notifyAll()会唤醒所有因为同Object类对象的wait()方法进入阻塞状态的线程。

使用方法

  1. 两个要通信的线程一定要拥有一个同样的Object类对象,这个对象可以没有任何含义,纯粹是工具对象,可以通过构造器传入,也可以调用static修饰的变量。
  2. 要在等待(B)线程需要进入阻塞状态的地方调用传入的工具对象的wait()方法,记得捕获异常
  3. 在唤醒(A)线程需要唤醒的地方加上同样工具对象的notify()/notifyAll()方法,不需要捕获异常
  4. 在wait()/notify()/notifyAll()的外面包上synchronized锁,锁对象就是那个工具类

举例如下:

public class Test {
    public static void main(String[] args) {
        String str = "这是一个工具对象,可以有用也可以没用";//工具对象
        Load th1 = new Load(str);
        Thread th2 = new Thread(new Show(str));
        th1.start();
        th2.start();
    }
}
class Load extends Thread{
    private Object obj;//工具对象

    //第一步构造器传入obj对象
    public Load(Object obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
        System.out.println("开始加载");
        for (int i = 0; i <= 100; i+=10) {
            System.out.println("图片加载中:"+i+"%");
        }
        //第四步加上同步锁
        synchronized (obj){
            obj.notify();//第三步调用notify()/notifyAll()方法
            System.out.println("加载完成");//由于锁还没被释放,这句话肯定会在显示完成之前打印
        }
    }
}
class Show implements Runnable{
    private Object obj;//工具对象

    //第一步构造器传入obj对象
    public Show(Object obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
        System.out.println("等待加载");
        //第四步加上同步锁
        synchronized (obj){
            try {
                obj.wait();//第二步调用wait()方法
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("显示完成");
    }
}

显示结果:

等待加载
开始加载
图片加载中:0%
图片加载中:10%
图片加载中:20%
图片加载中:30%
图片加载中:40%
图片加载中:50%
图片加载中:60%
图片加载中:70%
图片加载中:80%
图片加载中:90%
图片加载中:100%
加载完成
显示完成

为什么wait()/notify()/notifyAll()需要配合synchronized?

  • 如果不配合synchronized,编译可以通过,但是会抛出IllegalMonitorStateException异常。但是为什么系统要抛出这个异常?还是因为安全问题,如果用synchronized套住notify(),就可以保护该线程可以继续执行一段代码,如果不加锁,那么notify唤醒后,阻塞的线程会马上进入就绪状态,和唤醒线程争抢时间片,可能会导致接下来的代码段执行顺序出错,加上了锁,就只有在synchronized代码块执行完后才会释放锁,而wait()必须要先获得锁,才可以进入就绪状态,以此保护了synchronized代码块内的代码一定会按顺序执行。
  • 同理也是保护wait()前的代码块能正确执行,若是有多个线程包含wait(),用同步锁可以保护一段代码按顺序运行,如果有多个线程正在等待,被一起唤醒,如果没有锁,接下来这些线程就会抢占时间片,但是如果有了锁,在一个线程取得锁之后,在他还回锁之前,别的线程由于抢不到锁,无法与其抢夺时间片。
  • 归根结底,synchronzied是为了将一串原子操作捆绑在一起

线程饥饿

  • 如果在一个线程wait()前,对应的notify()就已经被执行了,那么wait()就会因为没人唤醒而一直等待下去,这就是线程饥饿

举个例子,某个银行只有一个ATM机,储户可以去银行取钱,取钱的过程为:储户获得锁,储户开始取钱操作,储户释放锁,等待下一个储户来取钱。当ATM机没钱的时候,需要有银行人员来填充钱,银行人员装钱的过程为:银行人员获得锁,银行人员开始存钱,银行人员存钱完毕升级ATM机系统,银行人员释放锁。代码如下:

public class Test {
    static boolean havemoney = false;
    public static void main(String[] args) {
        //储户
        Runnable user = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                //储户先要获得锁,拿不到锁说明别人正在取钱
                synchronized (lock){
                    System.out.println(name+"拿到了锁,准备取钱");
                        System.out.println(name+"发现ATM机没钱了,还回锁,等有钱了再来");
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    //当ATM机装入钱后,通知正在等待的储户被唤醒,准备取钱
                    System.out.println(name+"取到了钱");
                    System.out.println(name+"释放了锁");
                }
            }
        };
        Thread adm = new Thread(){
            @Override
            public void run() {
                synchronized (lock){
                    System.out.println("管理员来装钱了");
                    System.out.println("管理员装好钱了,等我升级完系统就可以取钱了");
                    lock.notifyAll();
                    System.out.println("管理员升级一下系统");
                    System.out.println("管理员升级好系统了,释放锁");
                }
            }
        };

        Thread xiaoming = new Thread(user,"小明");
        Thread xiaohong = new Thread(user,"小红");
        xiaoming.start();
        xiaohong.start();
        adm.start();
    }

}

显示结果:

小明拿到了锁,准备取钱
小明发现ATM机没钱了,还回锁,等有钱了再来
管理员来装钱了
管理员装好钱了,等我升级完系统就可以取钱了
管理员升级一下系统
管理员升级好系统了,释放锁
小红拿到了锁,准备取钱
小红发现ATM机没钱了,还回锁,等有钱了再来
小明取到了钱
小明释放了锁

//程序到这里一直停不下来!!!!!

程序为什么会停不下来?说明有线程没结束。哪个线程没结束?小红,小红还一直处于wait()的阻塞状态。为什么会没人唤醒?因为在小红来之前,银行管理员已经执行了notifyAll()命令,小红来了就没人唤醒造成了线程饥饿。因此,wait()操作并不一定是必须要进行的,如果notify操作已经进行,wait()操作就不用再进行了,所以需要给wait加一个条件判断,在这里可以是一个布尔类型havemoney,如果havemoney是false代表ATM机没钱,储户需要进入wait()状态等待,否则直接跳过wait()开始取钱。管理员装入钱后,先将havemoney置为true,然后再唤醒其他线程。

public class Test {
    static boolean havemoney = false;//ATM机当前状态
    static String lock = "这是一把锁";
    public static void main(String[] args) {
        //储户
        Runnable user = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                //储户先要获得锁,拿不到锁说明别人正在取钱
                synchronized (lock){
                    System.out.println(name+"拿到了锁,准备取钱");
                    //在此要判断ATM机有没有钱,没钱的话退出来还回锁,进入等待队列
                    if (!havemoney){
                        System.out.println(name+"发现ATM机没钱了,还回锁,等有钱了再来");
                        try {
                            lock.wait();
                            System.out.println(name+"又拿到了锁,准备取钱");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //当ATM机装入钱后,通知正在等待的储户被唤醒,准备取钱
                    System.out.println(name+"取到了钱");
                    System.out.println(name+"释放了锁");
                }
            }
        };
        Thread adm = new Thread(){
            @Override
            public void run() {
                synchronized (lock){
                    System.out.println("管理员来装钱了");
                    havemoney = true;
                    System.out.println("管理员装好钱了,等我升级完系统就可以取钱了");
                    lock.notifyAll();
                    System.out.println("管理员升级一下系统");
                    System.out.println("管理员升级好系统了,释放锁");
                }
            }
        };

        Thread xiaoming = new Thread(user,"小明");
        Thread xiaohong = new Thread(user,"小红");
        xiaoming.start();
        xiaohong.start();
        adm.start();
    }

}

显示结果

小明拿到了锁,准备取钱
小明发现ATM机没钱了,还回锁,等有钱了再来
管理员来装钱了
管理员装好钱了,等我升级完系统就可以取钱了
管理员升级一下系统
管理员升级好系统了,释放锁
小红拿到了锁,准备取钱
小红取到了钱
小红释放了锁
小明又拿到了锁,准备取钱
小明取到了钱
小明释放了锁

加了个if(!havemoney)判断语句,不会出现线程饥饿,但随之而来的问题是,如果管理员释放锁后,小明被唤醒,此时有另一个储户小刚以迅雷不及掩耳之势抢先拿到了锁,取完了里面所有的钱,那么按道理来讲,小明应该重新等待,但是改程序会让小明直接继续取钱。所以,为了杜绝这种情况发生,应该在小明被唤醒后再进行一次判断if(!havemoney),所以不应该用if循环,而应该是while循环

public class Test {
    static boolean havemoney = false;//ATM机当前状态
    static String lock = "这是一把锁";
    public static void main(String[] args) {
        //储户
        Runnable user = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                //储户先要获得锁,拿不到锁说明别人正在取钱
                synchronized (lock){
                    System.out.println(name+"拿到了锁,准备取钱");
                    //在此要判断ATM机有没有钱,没钱的话退出来还回锁,进入等待队列
                    while (!havemoney){
                        System.out.println(name+"发现ATM机没钱了,还回锁,等有钱了再来");
                        try {
                            lock.wait();
                            System.out.println(name+"又拿到了锁,准备取钱");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    //当ATM机装入钱后,通知正在等待的储户被唤醒,准备取钱
                    if (name=="小刚"){
                        System.out.println("小刚取走了所有的钱");
                        havemoney = false;
                    }else{
                        System.out.println(name+"取到了钱");
                    }
                    System.out.println(name+"释放了锁");
                }
            }
        };
        Thread adm = new Thread(){
            @Override
            public void run() {
                while(true){
                    if (!havemoney){
                        synchronized (lock){
                            System.out.println("管理员来装钱了");
                            havemoney = true;
                            System.out.println("管理员装好钱了,等我升级完系统就可以取钱了");
                            lock.notifyAll();
                            System.out.println("管理员升级一下系统");
                            System.out.println("管理员升级好系统了,释放锁");
                        }
                    }
                }
            }
        };

        Thread xiaoming = new Thread(user,"小明");
        Thread xiaohong = new Thread(user,"小红");
        Thread xiaogang = new Thread(user,"小刚");

        xiaoming.start();
        xiaohong.start();
        xiaogang.start();

        adm.setDaemon(true);//设置守护线程,否则这个线程无法结束
        adm.start();
    }
}

显示结果:

小明拿到了锁,准备取钱
小明发现ATM机没钱了,还回锁,等有钱了再来
小刚拿到了锁,准备取钱
小刚发现ATM机没钱了,还回锁,等有钱了再来
管理员来装钱了
管理员装好钱了,等我升级完系统就可以取钱了
管理员升级一下系统
管理员升级好系统了,释放锁
小红拿到了锁,准备取钱
小红取到了钱
小红释放了锁
小刚又拿到了锁,准备取钱
小刚取走了所有的钱
小刚释放了锁
小明又拿到了锁,准备取钱
小明发现ATM机没钱了,还回锁,等有钱了再来
管理员来装钱了
管理员装好钱了,等我升级完系统就可以取钱了
管理员升级一下系统
管理员升级好系统了,释放锁
小明又拿到了锁,准备取钱
小明取到了钱
小明释放了锁

 这样就对了。

而回到上一大论点,为什么wait()/notify()/notifyAll()一定要搭配synchronized使用?如果没有同步锁,管理员升级完系统后,所有程序被唤醒,开始和管理员抢夺时间片,但是管理员接下来还需要升级系统,逻辑上来讲,应该等管理员升级完系统再让储户进来,但因为这几个操作是原子操作,所以必须加上同步锁,保证这些代码先执行。对于储户的wait()也是,如果没有锁,那么在储户wait()之前,可能会出现的情况是:小明进入了ATM机->小刚也进入了ATM机->小刚wait()->小明wait。或者也可能是小明进入了ATM机->小明发现没钱->管理员进入了ATM机->管理员存钱并且唤醒所有人->小明wait()产生线程饥饿

总结

  1. B线程要等别的线程结束了再继续执行,可以调用别的线程的join()方法,但前提时B线程必须在别的类实例化的方法中,否则无法取得这个实例化对象。
  2. 可以用wait()/notify/notifyAll(),这个更加灵活,但最好要在wait()前while循环判断
  3. wait()/notify/notifyAll()的使用一定要搭配synchronized同步锁使用!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值