JAVA并发编程 - concurrent包的使用

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/fd_mas/article/details/52583307

自JAVA1.5版本开始,引入了一个新的包:concurrent
他里面包含了大量牛逼且好用的工具,以后,所有你希望用死循环等手段做的事情、多线程要处理的事情,等等,首先要想到使用它。

1 发布/订阅(也就是队列)

如果我们希望有一个数据缓冲区,有人可以随意向里面写数据,有人可以从里面按照“先进来先出去”的原则得到数据,该怎么做?

最简单的做法就是设计一个尽人皆知的单例类,里面放一个数组,然后提供读和写的函数,并且,代码中要仔细考虑如何对这个数组进行同步互斥,如何控制这个数组的大小,如何保证读/写的性能。

现在,看看concurrent里面提供了什么牛逼的东西。

- 【LinkedBlockingQueue】类(无边界队列)
使用它,程序瞬间变的及其简单了:

//主程序中创建一个队列
final BlockingQueue<Integer> queue=new LinkedBlockingQueue<Integer>();

//在线程中直接使用这个对象就可以了
queue.put(i);//把数据放到队列中

queue.take();//从队列头取出数据

为了上面的需求,代码变得如此简单了,再不需要考虑什么所谓的互斥、同步、读写竞争、单例模式,等等。
而且,如果你在new对象的时候,传入一个数字,这就是变成有边界的队列了,比如:new LinkedBlockingQueue(10); 这样,在执行queue.put时,如果队列已经存了10个数据,那么程序就自然被阻塞起来了,直到有另外的线程从里面取了数据(执行了take),你根本不要写大量的代码来处理队列阻塞的问题了。

  • 【DelayQueue】类(延迟队列)

如果,我们希望存入队列中的数据,在延迟期满后才能被取出来:比如,我们向缓存添加内容时,给每一个key设定过期时间,系统自动将超过过期时间的key清除。用这个类,就瞬间满足要求了。

DelayQueue 对元素进行持有,直到一个特定的延迟到期。
注入其中的元素必须实现 java.util.concurrent.Delayed 接口。

DelayQueue 将会在每个元素的 getDelay() 方法返回的值的时间段之后才释放掉该元素。如果返回的是 0 或者负值,延迟将被认为过期,该元素将会在 DelayQueue 的下一次 take 被调用的时候被释放掉。

  • 还有其他有特色的工具类

比如:SynchronousQueue,这个是很牛B的工具,可以好好用用。

2 等待线程结束再做其他事情

在你的程序中,如果需要启动100个线程去做其他事情,但是,你又希望你的主程序在这100个线程都结束后再继续执行,该怎么做?
一种最土鳖的方式就是在线程代码的最后一行,把一个状态记录到数据库中,然后你的主程序死循环去读数据库。

更优雅的做法是使用join来解决这个问题。
为了理解join的用法,我们假定有下面一个场景:
有三个工人,分别叫Worker_1,Worker_2,Worker_3。有一个任务需要他们三个人协作完成,且,Worker_3开始工作的前提是Worker_1和Worker_2完工后再开始,而Worker_1和Worker_2是并行同时工作。
为此,我们的程序可以这样写:
1. 创建1个继承自Thread的类:Worker
2. 在我们的主函数中,以如下的代码来实现上面的需要:

Worker Worker_1 = new Worker(“工人1”);
Worker Worker_2 = new Worker(“工人2”);
Worker Worker_3 = new Worker(“工人3”);

Worker_1.start();
Worker_2.start();
Worker_1.join();
Worker_2.join();

Worker_3.start();

这样,Worker_3这个线程,就会等待1和2这两个线程结束后才会被启动。

理解了join后,看看concurrent里面提供了什么牛逼的东西来处理同样的问题。

  • 【CountDownLatch】类
    join能有的能力,他都实现了。
    它有一个await()方法,调用后,就会一直阻塞。它的实现原理是通过其内部的一个计数器逐渐递减,在减为0之前,await()方法都会阻塞着:
CountDownLatch countDownLatch = new CountDownLatch(2);
Worker Worker_1 = new Worker(“工人1”, countDownLatch);
Worker Worker_2 = new Worker(“工人2”, countDownLatch);
Worker Worker_3 = new Worker(“工人3”, countDownLatch);

Worker_1.start();
Worker_2.start();
countDownLatch.await();//这就在等待了

Worker_3.start();

使用起来,与join()的不同有3个:
1. 要创建一个CountDownLatch对象,并传递给线程
2. 在线程类中需要增加一行代码:countDownLatch.countDown(); 这行代码的作用就是让CountDownLatch对象的内部计数器减一。
3. 代替join,使用await()方法实现在主程序中阻塞

看起来,使用CountDownLatch要比join麻烦,那么,为什么要用它?

为了理解CountDownLatch的用法,我们假定有下面一个场景:

有三个工人,分别叫Worker_1,Worker_2,Worker_3。有一个任务需要他们三个人协作完成,且,Worker_3开始工作的前提是Worker_1和Worker_2的工作完成了50%后再开始,而Worker_1和Worker_2是并行同时工作。

就这一个小小的不同,join就无法实现了,因为join只有一个能力,就是对调用了它的线程阻塞到执行结束。所以,如果我们要在被阻塞的线程内部的执行过程进行控制时,join就歇菜了,这时,就是CountDownLatch的用武之地了。

3 其他

3.1 让程序睡眠一会

使用TimeUnit,代替原来大家经常使用的Thread.sleep。

3.2 制作数组

使用CopyOnWriteArrayList。

在CopyOnWriteArrayList里处理写操作(包括add、remove、set等)是先将原始的数据通过Arrays.copyof()来生成一份新的数组。

然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象。这样保证了每次写都是在新的对象上(因为要保证写的一致性)。

然后读的时候就是在引用的当前对象上进行读。

CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁。

在对一个ArrayList对象进行大量读,而仅有少量写的场景下,很合适用它,比如缓存。

假定有如下一个场景:
在你的应用中有一个列表(List),它被频繁的遍历,但是很少被修改。就像“网站主页上的前十个分类”,它被频繁的访问,但是每个小时通过Quartz的Job来调度更新。如果使用ArrayList来作为该列表的数据结构并且不使用同步(synchronization),可能会遇到ConcurrentModificationException,因为在使用Quartz的Job修改该列表时,其他的代码可能正在遍历该列表。

如果为了这个需求,使用synchronization来解决,就得不偿失了。因为这个List是被高并发的读取,仅仅因为偶尔需要对其进行更新就是使用synchronization这种机制,其资源竞争会导致性能的极度恶化。

那么现在,CopyOnWriteArrayList就是一个牛逼方案了。

CopyOnWriteArrayList的另一个使用案例是观察者设计模式。如果事件监听器由多个不同的线程添加和移除,那么使用CopyOnWriteArrayList将会使得正确性和简单性得以保证。

展开阅读全文

没有更多推荐了,返回首页