Java多线程-线程通信

wait()、notify()、notifyAll()、都是Object中的final native方法,用于线程通信,是Java通过消息传递来实现线程通信的方式。他们都只能在同步方法或同步代码块中执行,而且必须是内置锁。

wait()

语义:使当前线程立即停止运行,释放对象锁,处于等待阻塞状态,并将当前线程置入锁对象的等待池中,直到被唤醒或被中断为止。

重载:

  • wait();死等,直到被唤醒或被中断。
  • wait(long timeout);超时等待:若在规定的时间内未被唤醒,则继续执行
  • wait(long timeout,int nanos);在上一个方法的基础上增加了纳秒控制。

notify()

语义:用来唤醒一个随机等待该对象锁的其他线程,notify方法被调用后,当前线程不会立即释放对象锁,要等到当前线程执行完毕后再释放锁。

notifyAll()

语义:用来唤醒等待该对象的所有线程。

现在我们来看看下面的程序

public class Demo {
    /**
     * 锁对象
     */
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        MyThread1 myThread1 = new MyThread1();
        myThread1.start();
        System.out.println("子线程启动");
        synchronized (lock) {
            System.out.println("主线程获得同步锁“);
            System.out.println("主线程释放了同步锁,阻塞");
            lock.wait();
        }
        System.out.println("主线程执行结束");
    }

    static class MyThread1 extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("子线程获得同步锁");
            }
            System.out.println("子线程释放了同步锁,子线程执行结束");
        }
    }
}

以上代码在主线程和子线程中都有同步代码块,而且两个同步代码块都使用相同的对象同步锁。

由于CPU调度两个同步代码块的执行顺序可能先后不一定:

  1. 主线程先抢到lock对象锁:随后执行lock.wait(),主线程处于等待状态,释放了同步锁。子线程立即获得了同步锁,执行完线程体。而主线程依旧处于等待阻塞状态。
子线程启动
主线程获得同步锁
主线程释放了同步锁,阻塞
子线程获得同步锁
子线程释放了同步锁,子线程执行结束
  1. 子线程先抢到lock对象锁:随后执行完线程体,释放同步锁。主线程立即获得了同步锁,执行lock.wait(),主线程处于等待状态,释放了同步锁。
子线程启动
子线程获得同步锁
子线程释放了同步锁,子线程执行结束
主线程获得同步锁
主线程释放了同步锁,阻塞

以上两种执行过程,主线程最终都处于等待阻塞状态,因为释放了同步锁后没有再次被唤醒。

如果给wait()方法传入等待时长,则两种执行过程都会在时长结束后正常结束。

如果在子线程体中使用notify唤醒:

static class MyThread1 extends Thread {
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("子线程获得同步锁");
            System.out.println("唤醒其他线程");
            lock.notify();
        }
        System.out.println("子线程释放了同步锁,子线程执行结束");
    }
}

我们再来分析执行过程,依旧有两种:

  1. 主线程先抢到lock对象锁:随后执行lock.wait(),主线程处于等待状态,释放了同步锁。子线程立即获得了同步锁,执行lock.notify(),唤醒其他线程,但此时还未释放同步锁,等到执行完线程体,同步锁才被释放。主线程立即获取到同步锁,进而继续执行lock.wait()之后的语句,主线程正常执行完毕。
子线程启动
主线程获得同步锁
主线程释放了同步锁,阻塞
子线程获得同步锁
唤醒其他线程
子线程释放了同步锁,子线程执行结束
主线程执行结束
  1. 子线程先抢到lock对象锁:随后执行lock.notify()(此时并没有线程处于等待状态),执行完线程体,释放同步锁。主线程立即获得了同步锁,执行lock.wait(),主线程处于等待状态,释放了同步锁,但主线程再也不会被唤醒了。
子线程启动
子线程获得同步锁
唤醒其他线程
子线程释放了同步锁,子线程执行结束
主线程获得同步锁
主线程释放了同步锁,阻塞

所以,程序按方式1执行则能够正常结束。

锁和监视器

上述程序同步代码块的同步锁对象是lock,因此都是通过lock.wait()和ock.notify()方法来实现线程通信,如果直接书写成wait()和notify(),会报非法的监视器状态异常

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
	at java.base/java.lang.Object.notify(Native Method)
	at com.jc.thread.Demo$MyThread1.run(Demo.java:26)

所以lock拥有一个同步锁和一个监视器。

在Java中,每个对象和Class内部都有一个锁,都会和一个监视器关联。锁是存在于对象内部的数据结构,监视器是操作系统层次的概念,是一个独立的结构但是和对象关联。

锁池和等待池

  • 锁池:多个线程共同争夺同一临界资源,而线程访问临界资源必须先持有临界资源的同步锁,当A线程持有了同步锁,那么其他线程就只能进入锁对象的锁池中,处于同步阻塞状态。等待同步锁被释放。然后被CPU调度,才能继续访问该临界资源。
  • 等待池:当A线程访问了临界资源,但调用了锁对象的wait()方法,线程A就释放了该同步锁,进入了锁对象的等待池中,处于等待阻塞状态。等待被持有该同步锁的其他线程唤醒执行notify()/notifyAll(),才能进入锁池,等待重新被调度。

notify 和 notifyAll

两者都是只能被持有同步锁的线程调用,

  • notify只能从等待池中随机唤醒一个等待线程进入锁池。
  • notifyAll是唤醒所有的等待线程到锁池中。

在锁池中的线程共同参与同步锁的竞争,能否获取到则取决于线程的优先级及CPU的调度。只有线程再次调用wait()方法才会又进入等待池。
请看下面的代码

public class Demo {
    /**
     * 锁对象
     */
    public static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new MyTHread().start();
        new MyTHread().start();
        // 确保子线程都处于等待状态
        Thread.sleep(1000);
        synchronized (lock) {
            System.out.println("主线程获取同步锁");
            lock.notify();
        }
        System.out.println("主线程执行完毕");

    }

    static class MyTHread extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 获取同步锁,等待……");
                    lock.wait();
                    System.out.println(Thread.currentThread().getName() + " 被唤醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " 释放同步锁");
        }
    }
}

先让三个子线程处于等待阻塞状态,然后主线程获取同步锁,执行notify操作。运行结果如下:

Thread-0 获取同步锁,等待
Thread-1 获取同步锁,等待
主线程获取同步锁
Thread-0 被唤醒
Thread-0 释放同步锁

如上,0号线程被主线程唤醒,执行完同步代码块。而1号线程则会一直处于等待阻塞状态,这样就造成了死锁。

我们来看下死锁的定义:

  • 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

这里所说的外力指的就是notify/notifyAll操作,那么notify造成的死锁就是由于线程通信造成的阻塞状态

如果使用notifyAll操作,运行结果如下:

Thread-0 获取同步锁,等待……
Thread-1 获取同步锁,等待……
主线程获取同步锁
Thread-0 被唤醒
Thread-1 被唤醒
Thread-0 释放同步锁
Thread-1 释放同步锁
主线程执行完毕

notifyAll从等待池中唤醒了所有的线程到锁池中,那么都能被CPU调度从而正常执行完程序。

正确地使用 wait、notify、notifyAll

  • 通过同步锁对象调用
  • 只能在synchronized中调用
  • 永远在循环里调用,而不要使用if
  • notify可能会造成死锁,更多的使用notifyAll

对于上面第三点,你的线程可能被错误唤醒,将会继续执行后序的代码,从而造成数据被破坏。如在生产者消费者的例子中,缓冲区为满的时候生产者线程被唤醒,如果wait在if中被调用,则不会再次检查缓冲区,将继续生成数据。

生产者-消费者模式

/**
 * 资源类
 */
public class Source {
    /**
     * 资源数据
     */
    private int data;
    /**
     * 最大资源数
     */
    private final int MAX = 10;

    /**
     * 添加资源
     */
    public void push() throws InterruptedException {
        synchronized (this) {
            // 判断资源数是否达上线
            while (data == MAX) {
                System.out.println("添加失败,资源已满");
                wait();
            }
            // 生产资源
            data++;
            // 唤醒其他线程操作资源
            notifyAll();
            System.out.println("添加资源成功,data=" + data);
        }
    }

    /**
     * 取出资源
     */
    public void pop() throws InterruptedException {
        synchronized (this) {
            // 判断资源是否被消耗完
            while (data == 0) {
                System.out.println("取出失败,没有资源");
                wait();
            }
            // 消费资源
            data--;
            // 唤醒其他线程操作资源
            notifyAll();
            System.out.println("取出资源成功,data=" + data);
        }
    }
}
/**
 * 消费者
 */
public class Customer extends Thread{

    private Source source;

    public Customer(Source source) {
        this.source = source;
    }
    @Override
    public void run() {
        while(true){
            try {
                sleep(50);
                source.pop();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 生产者
 */
public class Productor extends Thread {
    private Source source;

    public Productor(Source source) {
        this.source = source;
    }

    @Override
    public void run() {
        while (true){
            try {
                sleep(70);
                source.push();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
/**
 * 生产者-消费者
 */
public class Demo {
    public static void main(String[] args) {
        Source source = new Source();

        new Productor(source).start();
        new Customer(source).start();
        new Productor(source).start();
        new Productor(source).start();
        new Customer(source).start();
    }
}

执行结果:

取出失败,没有资源
取出失败,没有资源
添加资源成功,data=1
取出资源成功,data=0
添加资源成功,data=1
添加资源成功,data=2
取出资源成功,data=1
取出资源成功,data=0
取出失败,没有资源
添加资源成功,data=1
取出资源成功,data=0
添加资源成功,data=1
添加资源成功,data=2
取出资源成功,data=1
取出资源成功,data=0
添加资源成功,data=1
添加资源成功,data=2
......
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值