《Java并发变成之美》阅读笔记二(第一章 并发编程线程基础)

58 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1.3线程通知与等待

1.wait()等待

当一个线程调用一个共享变量的wait方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才会返回:
1)其他线程调用了该共享对象的notify()或者notifyAll()方法
2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回
当然我们要注意这里的wait方法在使用的时候要先用这个对象的监视器锁,要不然会报错。

使用

public class Demo9 {
    private static Object object = new Object();
    public static int n = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            synchronized (object){
                while(true){
                    if(n % 2 == 0){
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    n++;
                    System.out.println(n);
                    object.notify();
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(()->{
            synchronized (object){
                while(true){
                    if(n % 2 == 1){
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    n++;
                    System.out.println(n);
                    object.notify();
                }
            }
        });
        t2.start();
    }
}

这个代码就是两个线程在轮流打印,一个打印偶数,一个打印奇数。 当上面的被wait之后,后面的线程会用notify来唤醒,记住!他们之间之所以能相互唤醒是因为他们的锁是同一个。如果不是同一个就做不到。

当然我们还要注意一个的是虚假唤醒,这个唤醒是不用notify()、notifyAll()、或者被中断、或者等待超时

虽然应用之中是很少遇到的,但是我们还是要防范一下,做法就是去不断去测试,该线程被唤醒的条件是否符合,如果不满足就继续等待。

我们可以根据一个简单的生产者和消费者的例子来加深理解的。

private int[] arr = new int[40];
private int size;
private int head;
private int tail;
private Object lock = new Object();
public void put(int v){
    synchronized (lock){
        while(size == arr.length){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        arr[tail] = v;
        tail++;
        size++;
        if(tail >= arr.length){
            tail = 0;
        }
        lock.notify();
    }
}
public Integer take(){
    synchronized (lock){
        while(size == 0){
            try {
                lock.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return null;
        }
        Integer ret = arr[head];
        head++;
        size--;
        if(head == arr.length){
            head = 0;
        }
        lock.notify();
        return ret;
    }
}

这上面的两个while判断可以防止锁被虚假唤醒。就算是被唤醒了,那我们也可以根据while里面的代码,把他再挂起。

现在我们还要加深一下,一把锁的wait只能由这把锁的notify解开。

private static Object objectA = new Object();
private static Object objectB = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
        try {
            synchronized (objectA) {
                System.out.println("t1 get objectA lock");
                synchronized (objectB) {
                    System.out.println("t1 get objectB lock");
                    System.out.println("t1 release objectA lock");
                    objectA.wait();
                }
            }
        }catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
    Thread t2 = new Thread(()->{
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            synchronized (objectA) {
                System.out.println("t2 get objectA lock");
                System.out.println("t2 try get objectB lock");
                synchronized (objectB) {
                    System.out.println("t2 get objectA lock");
                    System.out.println("t2 release objectB lock");
                    objectA.wait();
                }
            }
        }catch(InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
}

屏幕截图 2022-09-26 215604.jpg

这上面可以看出,t1拿到A之后又拿到了B,然后释放A之后,t2可以很顺利拿到A,但是t2拿不到B,这个是因为t1没有释放B。

wait(long timeout)

这个wait和上面的不一样的是,他有一个参数,如果没有在timeout (ms)之内被其他线程调用该共享变量的notify或者notifyAll方法唤醒,那么这个函数还是会因为超时而返回。

如果将timeout设置为0,那么就和wait()一样了,因为在wait()内部就是调用了wait(0),注意timeout不能小于0否则会抛异常。

notify 和 notifyAll 的区别

前者是只能释放一个。而且会有一个争抢效果,是所有的wait争抢一个notify

后者就是把所有的都释放了。

需要注意的是,notifyAll只能释放它之前的wait如果有个wait在这个notifyAll后面,那么这个wait是不会被释放的。