第六十九条 并发工具优先于wait和notify

java中经常会用到子线程等,当各个线程作操作后,需要处理数据,我们需要自己重写wait和notify方法,但1.5以后,java平台对这些要求提高了,我们可以用高级的工具来代替。比如用线程池管理线程,或者java提供的并发集合类以及同步器(Synchronizer)。线程池上一条介绍过了。并发集合Concurrent Collection,它的标准接口如 List Queue Map等,为了提供良好的高并发性,它们内部管理自己同步,因此,并发集合不能排除并发活动,锁定它们没什么用,只会让速度变慢,我们无法对它们做过度的操作,但我们可以根据它们的特性,根据实际业务作出对应的选择。比如,HashMap不支持并发,单线程效率比较高,相应的HashTable提供支持并发功能,但由于对方法加锁,所以效率低;因此,后来 ConcurrentMap 出师了,它实现了Map接口,它是实行分段加锁技术,所以多线程的情况下,效率比较高,并发性能比较卓越,速度也很快,所以map集合的并发情况,如无意外或特殊的要求,我们优先使用 ConcurrentMap,而不是用 HashTable 等等。有些接口通过阻塞操作进行扩展,例如BlockingQueue扩展了Queue接口,并添加了take()在内的几个方法,它从队列中删除并返回了头元素,如果队列为空,没有元素,它就等待。这样就允许将阻塞队列用于工作队列,也称作生产者-消费者队列,可以一边添加,一边移除返回,并处理工作项目。上一条总,大多数ExecutorService实现(包括ThreadPoolExecutor)都使用BlockingQueue。


同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作。最常用的同步器是CountDownLatch和Semaphore,较不常用的是CyclicBarrier和Exchanger。我们分析一下其中的用法:

CountDownLatch:

    private void testCountDownLatch() {

        final CountDownLatch latch = new CountDownLatch(2);

        Thread t1 = new Thread(){
            public void run() {
                try {
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread t2 = new Thread(){
            public void run() {
                try {
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"正在执行");
                    Thread.sleep(3000);
                    Log.e(TAG, "子线程"+Thread.currentThread().getName()+"执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        try {
            t1.start();
            t2.start();
            Log.e(TAG, "等待2个子线程执行完毕...");
            latch.await();
            Log.e(TAG, "2个子线程已经执行完毕");
            Log.e(TAG, "继续执行主线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

调用这个方法,java 平台打印结果:

等待2个子线程执行完毕...
子线程Thread-0正在执行
子线程Thread-1正在执行
子线程Thread-1执行完毕
子线程Thread-0执行完毕
2个子线程已经执行完毕
继续执行主线程

android 平台打印为:

12-03 16:06:43.077 12545-12545/com.example.cn.desigin E/ConcurrentActivity: 等待2个子线程执行完毕...
12-03 16:06:43.077 12545-12712/com.example.cn.desigin E/ConcurrentActivity: 子线程Thread-5正在执行
12-03 16:06:43.077 12545-12713/com.example.cn.desigin E/ConcurrentActivity: 子线程Thread-6正在执行

12-03 16:06:46.078 12545-12712/com.example.cn.desigin E/ConcurrentActivity: 子线程Thread-5执行完毕
12-03 16:06:46.078 12545-12713/com.example.cn.desigin E/ConcurrentActivity: 子线程Thread-6执行完毕
12-03 16:06:46.078 12545-12545/com.example.cn.desigin E/ConcurrentActivity: 2个子线程已经执行完毕
12-03 16:06:46.078 12545-12545/com.example.cn.desigin E/ConcurrentActivity: 继续执行主线程

中间会报异常日志,是内部的,不会导致程序崩溃或停止功能的执行,可以不考虑异常日志

用途: 比如说有一个任务a,它要等待其他2个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch 来实现这种功能了;或者常见的一个网络功能,接口有时候功能单一,我们需要调用两个不同服务器的接口,都获取到后才能刷新UI界面,此时,也可以使用 CountDownLatch 来实现。


Semaphore:

    private void testSemaphore() {
        int N = 8;            //人数
        Semaphore semaphore = new Semaphore(5); //机器数目
        for (int i = 0; i < N; i++) {
            new SemaphoreThread(i, semaphore).start();
        }
    }

    static class SemaphoreThread extends Thread {
        private int num;
        private Semaphore semaphore;

        public SemaphoreThread(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire();
                Log.e(TAG, "人" + this.num + "坐在板凳上吃饭...");
                Thread.sleep(2000);
                Log.e(TAG, "人" + this.num + "吃完离开板凳");
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

打印结果为:  android 平台

12-03 16:19:23.545 24901-25171/? E/ConcurrentActivity: 人1坐在板凳上吃饭...
12-03 16:19:23.545 24901-25170/? E/ConcurrentActivity: 人0坐在板凳上吃饭...
12-03 16:19:23.545 24901-25172/? E/ConcurrentActivity: 人2坐在板凳上吃饭...
12-03 16:19:23.545 24901-25173/? E/ConcurrentActivity: 人3坐在板凳上吃饭...
12-03 16:19:23.545 24901-25174/? E/ConcurrentActivity: 人4坐在板凳上吃饭...
12-03 16:19:25.545 24901-25171/? E/ConcurrentActivity: 人1吃完离开板凳
12-03 16:19:25.545 24901-25175/? E/ConcurrentActivity: 人5坐在板凳上吃饭...
12-03 16:19:25.545 24901-25172/? E/ConcurrentActivity: 人2吃完离开板凳
12-03 16:19:25.545 24901-25173/? E/ConcurrentActivity: 人3吃完离开板凳
12-03 16:19:25.545 24901-25176/? E/ConcurrentActivity: 人6坐在板凳上吃饭...
12-03 16:19:25.545 24901-25177/? E/ConcurrentActivity: 人7坐在板凳上吃饭...
12-03 16:19:25.545 24901-25174/? E/ConcurrentActivity: 人4吃完离开板凳
12-03 16:19:25.545 24901-25170/? E/ConcurrentActivity: 人0吃完离开板凳
12-03 16:19:27.546 24901-25175/? E/ConcurrentActivity: 人5吃完离开板凳
12-03 16:19:27.546 24901-25176/? E/ConcurrentActivity: 人6吃完离开板凳
12-03 16:19:27.547 24901-25177/? E/ConcurrentActivity: 人7吃完离开板凳


Semaphore 可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。我们发现,它和线程池控制线程的个数有点相似,都是达到上限后,有线程执行完后,线程池是复用已有的线程,Semaphore是允许剩下的线程依次执行,如果再次达到上限,继续这个循环,直到执行完毕为止。


CyclicBarrier:

    private void testCyclicBarrier() {
        int N = 4;
        Runnable runable = new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, "当前线程" + Thread.currentThread().getName());
            }
        };
        CyclicBarrier barrier = new CyclicBarrier(N, runable);
        for (int i = 0; i < N; i++) {
            new CyclicBarrierThread(barrier).start();
        }
    }

    static class CyclicBarrierThread extends Thread{
        private CyclicBarrier cyclicBarrier;

        public CyclicBarrierThread(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            Log.e(TAG, "线程" + Thread.currentThread().getName() + "正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                Log.e(TAG, "线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            Log.e(TAG, "所有线程写入完毕,继续处理其他任务...");
        }
    }

打印结果为:  android 平台

12-03 16:34:28.962 7791-8164/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-5正在写入数据...
12-03 16:34:28.962 7791-8165/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-6正在写入数据...
12-03 16:34:28.962 7791-8166/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-7正在写入数据...
12-03 16:34:28.962 7791-8167/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-8正在写入数据...
12-03 16:34:34.140 7791-8165/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-6写入数据完毕,等待其他线程写入完毕
12-03 16:34:34.141 7791-8167/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-8写入数据完毕,等待其他线程写入完毕
12-03 16:34:34.141 7791-8164/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-5写入数据完毕,等待其他线程写入完毕
12-03 16:34:34.141 7791-8166/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-7写入数据完毕,等待其他线程写入完毕
12-03 16:34:34.146 7791-8166/com.example.cn.desigin E/ConcurrentActivity: 当前线程Thread-7
12-03 16:34:34.146 7791-8165/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:34:34.146 7791-8166/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:34:34.147 7791-8164/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:34:34.147 7791-8167/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...


四个线程执行完了后,会去执行传递进去的 runnable,四个线程哪个先执行完,就用哪条线程执行 runnable 回调,看到这,我们明白了,和 CountDownLatch 的功能类似,如果仅仅是这样,就太小看 CyclicBarrier 了, CountDownLatch 是一次性的,而 CyclicBarrier 是可以循环使用的。 如下,添加新的线程。由于android 的 ANR 机制,我们开启一个子线程,在子线程中调用 testCyclicBarrier() 方法。

    private void testCyclicBarrier() {
        int N = 4;
        Runnable runable = new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, "当前线程" + Thread.currentThread().getName());
            }
        };
        CyclicBarrier barrier = new CyclicBarrier(N, runable);
        for (int i = 0; i < N; i++) {
            new CyclicBarrierThread(barrier).start();
        }

        try {
            Thread.sleep(25000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Log.e(TAG, "CyclicBarrier重用");

        for (int i = 0; i < N; i++) {
            new CyclicBarrierThread(barrier).start();
        }
    }

打印结果为:

12-03 16:45:12.801 18898-19075/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-9正在写入数据...
12-03 16:45:12.801 18898-19074/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-8正在写入数据...
12-03 16:45:12.801 18898-19073/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-7正在写入数据...
12-03 16:45:12.801 18898-19072/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-6正在写入数据...
12-03 16:45:17.804 18898-19073/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-7写入数据完毕,等待其他线程写入完毕
12-03 16:45:17.804 18898-19072/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-6写入数据完毕,等待其他线程写入完毕
12-03 16:45:17.805 18898-19075/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-9写入数据完毕,等待其他线程写入完毕
12-03 16:45:17.805 18898-19074/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-8写入数据完毕,等待其他线程写入完毕
12-03 16:45:17.805 18898-19074/com.example.cn.desigin E/ConcurrentActivity: 当前线程Thread-8
12-03 16:45:17.806 18898-19074/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:17.806 18898-19073/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:17.806 18898-19072/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:17.806 18898-19075/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:37.801 18898-19071/com.example.cn.desigin E/ConcurrentActivity: CyclicBarrier重用
12-03 16:45:37.802 18898-19469/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-10正在写入数据...
12-03 16:45:37.803 18898-19470/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-11正在写入数据...
12-03 16:45:37.803 18898-19471/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-12正在写入数据...
12-03 16:45:37.806 18898-19472/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-13正在写入数据...
12-03 16:45:42.803 18898-19469/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-10写入数据完毕,等待其他线程写入完毕
12-03 16:45:42.803 18898-19470/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-11写入数据完毕,等待其他线程写入完毕
12-03 16:45:42.804 18898-19471/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-12写入数据完毕,等待其他线程写入完毕
12-03 16:45:42.806 18898-19472/com.example.cn.desigin E/ConcurrentActivity: 线程Thread-13写入数据完毕,等待其他线程写入完毕
12-03 16:45:42.807 18898-19472/com.example.cn.desigin E/ConcurrentActivity: 当前线程Thread-13
12-03 16:45:42.807 18898-19472/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:42.807 18898-19469/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:42.808 18898-19470/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...
12-03 16:45:42.810 18898-19471/com.example.cn.desigin E/ConcurrentActivity: 所有线程写入完毕,继续处理其他任务...


并发这一块,能涉及的地方可以很深,我们只是在这稍微涉及点皮毛,如果想深入了解,建议阅读源码和相关书籍。最后,我们在说一下notify,一个相关的话题是,为了唤醒正在等待的线程,你应该使用notfiy还是notifyAll。一种常见的说法是,你总是应该使用notifyAll。这是合理而保守的建议。它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程你可能也会唤醒其他一些线程,但是这不会影响程序的正确性,这些线程醒来之后,会检查他们正在等待的条件如果发现条件并不满足,就会继续等待。从优化的角度来看,如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条件中被唤醒,那么你应该选择调用notify,而不是notifyAll。即使这些条件都是真的,也许还是有理由使用notifyAll而不是notify。就好像把wait调用放在一个循环中,以避免在公有可访问对象上的意外或恶意的通知一样,与此类似,使用notifyAll代替notify可以避免来自不想关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使真正的接收线程无限的等待下去。

没有理由在新代码中使用wait和notify,即使有、也是极少的。你应该优先使用notifyAll,而不是使用notify。如果使用notify,请一定要小心,以确保程序的活性。

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/84773136