4 java多线程:线程间通信

线程间的通信:主要是多个线程间有依赖,需要进行消息的沟通,例如:搬运工搬运10号货物,  
需要9号货物先搬走, 10号货物才能搬走,因此这两个线程需要进行通信,告知情况。

线程间通信

通过Object对象中的wait(当前线程等待)和notify(通知其他线程)方法,我们就可以建立一个多线程之间的通信。

public class MyThread {
    //公共变量,用来标明通信
    public static Integer num = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (num != 9) {
                    try {
                        System.out.println("9号物品还没有搬走,我等等");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                 }  else {
                    System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
                }
            }
        },"10号工人").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
                num = 9;
                this.notifyAll();
            }
        },"9号工人").start();
    }
}
-- 执行结果:
Exception in thread "10号工人" java.lang.IllegalMonitorStateException

发现上述执行结果报错,一脸的懵逼,查看IllegalMonitorStateException:Thrown to indicate that a thread has attempted to wait on an object’s monitor or to notify other threads waiting on an object’s monitor without owning the specified monitor。当前线程试图在一个对象监视器上等待或者试图唤醒在该对象监视器上的其他等待线程时,发现自己竟然没有拥有这个监视器,所以就会抛出这个异常,那如何拥有对象的监视器呢?
通过搜索发现,获取对象的监视器有3中方式:

  1. 通过执行此对象的同步 (Sychronized) 实例方法。
  2. 过执行在此对象上进行同步的 synchronized 语句的正文。
  3. 对于 Class 类型的对象,可以通过执行该类的同步静态方法。

通过上述方法,发现获取对象的监视器需要通过synchronized来实现。

获取对象监视器

进阶代码二:

public class MyThread {
    //公共变量,用来标明通信
    public static Integer num = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (num != 9) {
                    synchronized(this){
                        try {
                            System.out.println("9号物品还没有搬走,我等等");
                            this.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                 }  else {
                    System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
                }
            }
        },"10号工人").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this){
                    System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
                    num = 9;
                    this.notifyAll();
                }
            }
        },"9号工人").start();
    }
}

--- 输出内容:
9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。

不报错,9号工人已经搬走货物了,等了好久还没有等到10号工人搬东西,后来发现程序一致运行,但是10号工人阻塞了。

排查发现,索然大家都获取到了对象的监视器,但是发现对象不一样啊,这个this和那个this不是同一个对象。

同一对象的监视器

进阶代码三,既然没有获取同一个对象的监视器,那我们就用同一个对象,正好num是公共的,可以大家一同使用。

public static Integer num = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (num != 9) {
                    synchronized(num){
                        try {
                            System.out.println("9号物品还没有搬走,我等等");
                            num.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }  else {
                    System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
                }
            }
        },"10号工人").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (num){
                    System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
                    num = 9;
                    num.notifyAll();
                }
            }
        },"9号工人").start();
    }
---输出
Exception in thread "9号工人" java.lang.IllegalMonitorStateException
结果还是报上一个错。

通过上述报错,我们发现9号线程报错了,说明synchronized获取的监视器对象和num.notifyAll()不是同一个对象,那这是什么地方出现问题?
通过代码我们发现9号线程进行赋值,是不是赋值导致对象发生变化?通过9号线程的debug发现,num通过赋值,对象时不一样的。因此当我们通过wait和notify进行线程通信时,必须要保证对象唯一,最好不要用有意义的对象,直接用一个无关的唯一的,这就是我们在很多方法中,看到private final Object lock这种代码,直接作为synchronized的对象,就是为了防止出现赋值或其他操作导致对象发生变化。

if和while

通过上述的分析,进阶代码4

public class MyThread {
    //公共变量,用来标明通信
    public static Integer num = 0;
    public static final Object obj = new Object();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (num != 9) {
                    synchronized(obj){
                        try {
                            System.out.println(Thread.currentThread().getName()+":9号物品还没有搬走,我等等");
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                } else {
                    System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
                }
                System.out.println(Thread.currentThread().getName()+":我结束了");
            }
        },"10号工人").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj){
                    System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
                    num = 9;
                    obj.notifyAll();
                }
            }
        },"9号工人").start();
    }
}

---
10号工人:9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
10号工人:我结束了
通过输出内容,发现10号工人就没有搬东西,被唤醒后,直接就结束了,  
这就是if原因,判断后就不会再次判断了。

因此需要通过while进行处理,即:线程被唤醒后也需要再次判断是否符合条件,因为可能存在其他线程唤醒线程10。

进阶代码

public class MyThread {
    //公共变量,用来标明通信
    public static Integer num = 0;
    public static final Object obj = new Object();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(num != 9){
                    synchronized (obj){
                        try {
                            System.out.println(Thread.currentThread().getName()+":9号物品还没有搬走,我等等");
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
                System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
                System.out.println(Thread.currentThread().getName()+":我结束了");
            }
        },"10号工人").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj){
                    System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
                    num = 9;
                    obj.notifyAll();
                }
            }
        },"9号工人").start();
    }
}
输出:
10号工人:9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
10号工人:我搬走了10号物品
10号工人:我结束了

notify和notifyAll的区别

根据方法说明,notify随机唤醒该对象监听器中等待线程的一个,而notifyAll是唤醒所有等待线程;正常我们要使用notifyAll,存在notify唤醒的线程因为某种原因,本身又wait了,那么该监视器的所有的线程都处于等待中,就都挂了。当然notifyAll还是有成本的,唤醒了不少无法执行的线程。

扫描二维码关注公众号,回复: 4238286 查看本文章

wait和sleep

wait:Object类的方法,当前线程处于等待状态,让出cpu和释放锁(后期再说)。
sleep:Thread类的方法,当前线程处于休息,并没有让出cpu,同时持有锁对象。

其他通信方式

其实除了通过wait和notify这种通信方式,还可以通过代码逻辑进行实现。
代码

public class ThreadCommunication {
    public static Integer num = 0;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(num != 9){
                    System.out.println(Thread.currentThread().getName()+":9号物品还没有搬走,我等等");
                }
                System.out.println(Thread.currentThread().getName()+":我搬走了10号物品");
                System.out.println(Thread.currentThread().getName()+":我结束了");
            }
        },"10号工人").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + ":我搬走9号物品了。");
                num = 9;
            }
        },"9号工人").start();
    }
}
输出:
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
10号工人:9号物品还没有搬走,我等等
9号工人:我搬走9号物品了。
10号工人:9号物品还没有搬走,我等等
10号工人:我搬走了10号物品
10号工人:我结束了

就是通过while循环,条件不符合就一直进行判断,直到条件符合,这样对cpu存在浪费,10号工人一直在空转(while判断),除了上述原因,这种方法,不适用多线程间互相通知,尤其是存在对num进行操作的线程存在多个时,即num不适用超过2个线程之间的通信。

总结

线程间的通信,如果通过wait和notify进行通信,需要注意以下情况:

  1. wait和notify必须在synchronized中。
  2. 获取同一对象的监视器,才能互相唤醒。
  3. 为了防止监视器对象发生变化,最好单独定义一个监视器对象,不参与任何业务逻辑,例如:private final Object obj = new Object();
  4. 线程等待的条件不要用if,使用while判断,保证唤醒后,判断条件是否符合,因为存在无效的唤醒(唤醒你并不是条件符合才唤醒你)。
  5. 当唤醒时,使用notifyAll方法,尽量不要使用notify方法。

猜你喜欢

转载自blog.csdn.net/u010652576/article/details/84501974