Java并发编程实战————wait、notify、CountDownLatch

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014745069/article/details/82316130

引言

前面的学习中已经解除了关于加锁,共享变量可见性,原子性,synchronized,volatile等知识。在多线程中,还有很多需要掌握的小知识点。

避免字符串常量作为锁对象

在下面的例子中,m1和m2其实锁定的是同一个对象这种情况下还会发生比较诡异的现象,比如你用到一个类库,在该类库中代码锁定了字符串"Hello",但是你读不到源码,所以你在自己的代码中也锁定了"Hello",这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁

public class T {
    String s1 = "Hello";
    String s2 = "Hello";

    void m1() {
        synchronized (s1) {
        }
    }

    void m2() {
        synchronized (s2) {
        }
    }
}

避免将锁定对象的引用变成另一个对象

锁定某个对象,如果o的属性发生改变,不影响锁的使用,但是如果o变成另外一个对象,则锁定的对象发生改变,应该避免将锁定对象的引用变成另一个对象。

public class T {
    Object o = new Object();

    void m() {
        synchronized (o) {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        new Thread(t::m, "t1").start();// 启动第一个线程

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 创建第二个线程
        Thread t2 = new Thread(t::m, "t2");
        // 锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,t2 将永远得不到执行机会。
        // 这是因为锁定的是堆内存中实际的对象,而不是栈内存中的引用,如果引用指向的对象发生了改变,则新对象不会有锁。
        t.o = new Object();
        t2.start();
    }
}

面试题

曾经的面试题(淘宝?)
实现一个容器,提供两个方法:add()、size()
写两个线程,线程1 为容器添加10个元素,线程2实时监控容器中元素的个数,当个数为5时,线程2给出提示并结束。
分析下面这个程序,能完成这个功能吗?

程序一

public class MyContainer1 {
    // 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
    volatile List<Object> lists = new ArrayList<>();
    
    public void add(Object o) {
        lists.add(o);
    }
    
    public int size() {
        return lists.size();
    }
    
    public static void main(String[] args) {
        MyContainer1 c = new MyContainer1();
        
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                c.add(new Object());
                System.out.println("add" + i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        
        new Thread(() -> {
            /* t2循环检测c中的大小是否已经等于5*/
            while(true) {
                if (c.size() == 5) {
                    break;
                }
            }
            System.out.println("t2 结束");
        }, "t2").start();
    }
}

执行结果:

程序二

/**
 * 曾经的面试题(淘宝?) 实现一个容器,提供两个方法:add , size 写两个线程,线程1 为容器添加10个元素,线程2实时监控容器中元素的个数,
 * 当个数为5时,线程2给出提示并结束 <br>
 * 
 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做?
 * =======================================================================
 * 这里使用wait和notify 做到,wait会释放锁,而notify不会释放锁。
 * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是先让t2监控才可以。
 * 
 * 阅读下面程序,并分析输出结果
 * 可以读到输出结果并不是size==5时t2退出,而是t1结束时t2才接收到通知而退出
 * 想想这是为什么?
 * 
 * 类名:MyContainer2<br>
 * 作者: mht<br>
 * 日期: 2018年9月1日-上午11:04:54<br>
 */
public class MyContainer2 {
    // 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
    volatile List<Object> lists = new ArrayList<>();
    
    public void add(Object o) {
        lists.add(o);
    }
    
    public int size() {
        return lists.size();
    }
    /**
     * 使用wait和notify必须锁定同一个Object,否则将不能使用wait和notify,虽然size=5时线程1发出了notify的命令
     * 唤醒了t2,但是notify不会释放lock,t2依然需要阻塞,等待t1执行完后释放锁后才可以执行。另外sleep()也是不释放锁的。
     */
    public static void main(String[] args) {
        MyContainer2 c = new MyContainer2();
        // 锁对象,可以是任意的一个对象,使用wait和notify必须在同一个对象锁上
        final Object lock = new Object();
        
        new Thread(() -> {
            System.out.println("t2启动");
            synchronized (lock) {
                // 这里判断size是否等于5,如果不满足条件,则t2进入等待状态
                if (c.size() != 5) {
                    try {
                        System.out.println("size = " + c.size() + ",t2 wait...");
                        // wait会让当前线程进入阻塞状态,并释放锁
                        lock.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2结束");
            }
        }).start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(() -> {
            System.out.println("t1启动");
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    c.add(new Object());
                    System.out.println("add" + i);
                    // t1线程内部发出一个满足条件时的唤醒消息,notify代表唤醒一个线程,这个唤醒是随机的
                    if (c.size() == 5) {
                        System.out.println("size = 5, 叫醒其他线程!");
                        lock.notify();// notifyAll()可以唤醒全部等待中的线程,随机抢得
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

执行结果:

 

 程序三

/**
 * 使用wait和notify 完成最终的目标MyContainer2.java最终解决方法
 * 使用wait和notify在线程之间来回等待和叫醒,这样做的确可以完成要求,但是
 * 这样做非常的麻烦
 * 类名:MyContainer2<br>
 * 作者: mht<br>
 * 日期: 2018年9月1日-上午11:04:54<br>
 */
public class MyContainer3 {
    // 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
    volatile List<Object> lists = new ArrayList<>();
    
    public void add(Object o) {
        lists.add(o);
    }
    
    public int size() {
        return lists.size();
    }
    /**
     * 使用wait和notify必须锁定同一个Object,否则将不能使用wait和notify,虽然size=5时线程1发出了notify的命令
     * 唤醒了t2,但是notify不会释放lock,t2依然需要阻塞,等待t1执行完后释放锁后才可以执行。另外sleep()也是不释放锁的。
     */
    public static void main(String[] args) {
        MyContainer3 c = new MyContainer3();
        // 锁对象,可以是任意的一个对象,使用wait和notify必须在同一个对象锁上
        final Object lock = new Object();
        
        new Thread(() -> {
            System.out.println("t2启动");
            synchronized (lock) {
                // 这里判断size是否等于5,如果不满足条件,则t2进入等待状态
                if (c.size() != 5) {
                    try {
                        System.out.println("size = " + c.size() + ",t2 wait...");
                        // wait会让当前线程进入阻塞状态,并释放锁
                        lock.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2结束");
                // 注意!!wait状态的线程不会因为其他线程的结束而自动执行,必须叫醒线程1
                lock.notify();
            }
        }).start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(() -> {
            System.out.println("t1启动");
            synchronized (lock) {
                for (int i = 0; i < 10; i++) {
                    c.add(new Object());
                    System.out.println("add" + i);
                    // t1线程内部发出一个满足条件时的唤醒消息,notify代表唤醒一个线程,这个唤醒是随机的
                    if (c.size() == 5) {
                        System.out.println("size = 5, 叫醒其他线程!");
                        lock.notify();// notifyAll()可以唤醒全部等待中的线程,随机抢得
                        try {
                            lock.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

程序四

/**
 * 曾经的面试题(淘宝?) 实现一个容器,提供两个方法:add , size 写两个线程,线程1 为容器添加10个元素,线程2实时监控容器中元素的个数,
 * 当个数为5时,线程2给出提示并结束 <br>
 * 
 * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做?
 * =======================================================================
 * 使用Latch(门闩) 代替wait notify来进行通知
 * 好处是通信方式简单,同时也可以指定等待时间
 * 使用 await和countdown方法来替代wait 和 notify
 * CountDownLatch不涉及锁定,当count的值为零时当前线程继续执行
 * 当不涉及同步,只是涉及通信的时候,使用synchronized + wait/notify就显得太重了
 * 这时应该考虑CountDownLatch/CyclicBarrier/Semaphore
 * 
 * 类名:MyContainer2<br>
 * 作者: mht<br>
 * 日期: 2018年9月1日-上午11:04:54<br>
 */
public class MyContainer4 {
    // 如果不加volatile,lists的变化无法及时被其他线程感知,因此可能导致不可见的问题
    volatile List<Object> lists = new ArrayList<>();
    
    public void add(Object o) {
        lists.add(o);
    }
    
    public int size() {
        return lists.size();
    }
    /**
     * 使用wait和notify必须锁定同一个Object,否则将不能使用wait和notify,虽然size=5时线程1发出了notify的命令
     * 唤醒了t2,但是notify不会释放lock,t2依然需要阻塞,等待t1执行完后释放锁后才可以执行。另外sleep()也是不释放锁的。
     */
    public static void main(String[] args) {
        MyContainer4 c = new MyContainer4();
        CountDownLatch latch = new CountDownLatch(1);

        new Thread(() -> {
            System.out.println("t2 启动");
            if (c.size() != 5) {
                try {
                    latch.await();// 和o.wait()效果类似
                    // 也可以指定时间
                    // latch.await(5, TimeUnit.SECONDS);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2 结束");
        }).start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        new Thread(() -> {
            System.out.println("t1 启动");
            for (int i = 0; i < 10; i++) {
                c.add(new Object());
                System.out.println("add" + i);
                
                if (c.size() == 5) {
                    // 打开门闩使得t2得以执行
                    latch.countDown();
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

推荐第四种写法,效率高,可读性好。

NTES部门同步CountDownLatch模拟

/**
 * 通过CountDownLatch模拟网易邮箱请求多线程
 * <br>类名:NtesDemo<br>
 * 作者: mht<br>
 * 日期: 2018年9月2日-下午12:01:36<br>
 */
public class NtesDemo {
    private volatile static Integer apiResult;
    private volatile static Integer mysqlResult;

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(2);
        
        new Thread(() -> {
            if (apiResult == null || mysqlResult == null) {
                System.out.println("apiResult = " + apiResult + ", mysqlResult = " + mysqlResult);
                try {
                    latch.await(5, TimeUnit.SECONDS);
                    // 两个结果依然有null值,则提示超时了
                    if (apiResult == null || mysqlResult == null) {
                        System.out.println("超时了");
                    } else {
                        System.out.println("mysqlResult(" + mysqlResult + ") + apiResult(" + apiResult + ") = " + (mysqlResult + apiResult));
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        // 数据库查询
        new Thread(() -> {
            int count = 0;
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                count++;
            }
            mysqlResult = count;
            latch.countDown();
        }).start();
        // 网易邮箱查询
        new Thread(() -> {
            int count = 0;
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                count++;
            }
            apiResult = count;
            latch.countDown();
        }).start();
    }
}

执行结果:

总结

使用wait和notify来等待和唤醒线程是一种粒度非常细的操作,使用者需要非常了解两者的特性和使用方法才能够熟练运用。

wait和notify使用时必须将对象锁定,否则无法使用,线程在使用对象的wait方法后会进入等待状态(类似于阻塞,但wait必须由其他线程使用notify才可以唤醒,而阻塞不需要任何唤醒的机制,一直处于竞锁状态),notify()和notifyAll()可以唤醒其他线程,前者为随机唤醒一个线程。

wait和notify的操作是相对复杂的,虽然强大,但是在处理复杂的业务逻辑中书写较麻烦,相当于多线程中的汇编语言。

使用CountDownLatch可以有效的替代wait和notify的使用场景,而且不受锁的限制,书写简便且易于理解。

猜你喜欢

转载自blog.csdn.net/u014745069/article/details/82316130