并发集合是Java™的一个巨大补充,但是许多Java开发人员避免使用此程序包,因为他们认为,像它要解决的问题一样,它必须很复杂。
实际上, java.util.concurrent
包含许多类,这些类可以有效解决常见的并发问题,而无需您费劲。 继续阅读以了解诸如CopyOnWriteArrayList
和BlockingQueue
类的java.util.concurrent
类如何帮助您解决多线程编程的有害挑战。
1. TimeUnit
尽管它本身不是Collections类,但java.util.concurrent.TimeUnit
枚举使代码更容易阅读。 使用TimeUnit
可以使开发人员免于使用您的方法或API的烦恼。
TimeUnit
包含所有时间单位,从MILLISECONDS
和MICROSECONDS
到DAYS
和HOURS
,这意味着它可以处理开发人员可能需要的几乎所有时间跨度类型。 而且,由于在枚举中声明了转换方法,所以当时间加快时,将HOURS
转换回MILLISECONDS
甚至是微不足道的。
2. CopyOnWriteArrayList
就时间和内存开销而言,制作一个阵列的新副本对于日常使用来说是一项昂贵的操作; 开发人员经常求助于使用同步的ArrayList
。 但是,这也是一个代价高昂的选择,因为每次迭代集合的内容时,都必须同步所有操作(包括读写)以确保一致性。
对于许多读者正在读取ArrayList
但很少修改它的场景,这使成本结构倒退。
CopyOnWriteArrayList
是解决这个问题的神奇小宝石。 它的Javadoc将CopyOnWriteArrayList
定义为“ ArrayList
线程安全变体,其中所有可变操作(添加,设置等)都通过制作数组的新副本来实现”。
该集合在进行任何修改后都会在内部将其内容复制到一个新的数组中,因此,读取该数组的内容的读者不会产生任何同步开销(因为它们永远不会对可变数据进行操作)。
从本质上讲, CopyOnWriteArrayList
非常适合ArrayList
我们失败的确切情况:经常读取,很少写入的集合,例如JavaBean事件的Listener
。
3. BlockingQueue
BlockingQueue
接口声明它为Queue
,这意味着它的项目以先进先出(FIFO)的顺序存储。 以特定顺序插入的项目将以相同的顺序进行检索-但增加了保证,从空队列中检索项目的任何尝试都会阻塞调用线程,直到准备好检索该项目为止。 同样,任何将项目插入已满的队列的尝试都会阻塞调用线程,直到队列的存储空间可用为止。
BlockingQueue
巧妙地解决了如何将一个线程收集的项目“移交给”另一个线程进行处理的问题,而无需担心同步问题。 Java教程中的Guarded Blocks路径就是一个很好的例子。 它使用手动同步和wait()
/ notifyAll()
构建一个单插槽有界缓冲区,以便在有新项目可供使用时以及在插槽准备好由新项目填充时在线程之间发出信号。 (有关详细信息,请参见“ 防护块”实现 。)
尽管Guarded Blocks教程中的代码可以正常工作,但是它很长,很混乱,而且不完全直观。 早在Java平台的早期,是的,Java开发人员不得不纠缠于此类代码。 但肯定情况有所改善吗?
清单1显示了Guarded Blocks代码的重写版本,其中我使用了ArrayBlockingQueue
而不是手写的Drop
。
清单1. BlockingQueue
import java.util.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class ABQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new ArrayBlockingQueue(1, true);
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
ArrayBlockingQueue
还尊重“公平性”,这意味着它可以为读取器和写入器线程提供先进先出的访问权限。 另一种选择是更有效率的策略,可能会耗尽一些线程。 (也就是说,在其他读取器保持锁定状态时允许读取器运行会更有效率,但是您可能会冒着持续不断的读取器线程流的危险,使写入器无法继续工作。)
BlockingQueue
还支持带有时间参数的方法,该方法指示线程在返回信号失败以插入或检索有问题的项之前应该阻塞多长时间。 这样做可以避免无限制的等待,因为无限制的等待很容易变成需要重新启动的系统挂起,因此无止境的等待可能会使生产系统丧命。
4. ConcurrentMap
Map
包含一个细微的并发错误,该错误已导致许多无意的Java开发人员误入歧途。 ConcurrentMap
是简单的解决方案。
当从多个线程访问Map
,通常在存储键/值对之前使用containsKey()
或get()
来查找给定的键是否存在。 但是即使使用同步的Map
,线程也可能在此过程中潜入并抓住对Map
控制。 问题在于,在get()
的开始处get()
了该锁,然后在对put()
的调用中释放了该锁,然后可以再次获取该锁。 结果是争用条件:这是两个线程之间的争用,根据谁先运行,结果会有所不同。
如果两个线程在完全相同的时刻调用一个方法,则每个线程都会进行测试,并且每个线程都会放置,从而丢失进程中第一个线程的值。 幸运的是, ConcurrentMap
接口支持许多其他方法,这些方法设计为在单个锁下完成两件事:例如, putIfAbsent()
首先进行测试,然后仅在键未存储在Map
时才进行put 。
5.同步队列
根据Javadoc, SynchronousQueue
是一种有趣的生物:
一个阻塞队列,其中每个插入操作必须等待另一个线程进行相应的删除操作,反之亦然。 同步队列没有任何内部容量,甚至没有一个容量。
本质上, SynchronousQueue
是上述BlockingQueue
另一种实现。 它使用ArrayBlockingQueue
使用的阻塞语义为我们提供了一种非常轻巧的方式,可以将单个元素从一个线程交换到另一个线程。 在清单2中,我使用SynchronousQueue
而不是ArrayBlockingQueue
重写了清单1中的代码:
清单2. SynchronousQueue
import java.util.*;
import java.util.concurrent.*;
class Producer
implements Runnable
{
private BlockingQueue<String> drop;
List<String> messages = Arrays.asList(
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"Wouldn't you eat ivy too?");
public Producer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
for (String s : messages)
drop.put(s);
drop.put("DONE");
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
class Consumer
implements Runnable
{
private BlockingQueue<String> drop;
public Consumer(BlockingQueue<String> d) { this.drop = d; }
public void run()
{
try
{
String msg = null;
while (!((msg = drop.take()).equals("DONE")))
System.out.println(msg);
}
catch (InterruptedException intEx)
{
System.out.println("Interrupted! " +
"Last one out, turn out the lights!");
}
}
}
public class SynQApp
{
public static void main(String[] args)
{
BlockingQueue<String> drop = new SynchronousQueue<String>();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
实现代码看起来几乎相同,但是应用程序还有一个好处,即SynchronousQueue
仅当有线程在等待使用它时才允许插入队列。
实际上, SynchronousQueue
类似于Ada或CSP等语言中可用的“集合通道”。 在其他环境(包括.NET)中,有时也将它们称为“联接”(请参阅参考资料 )。
结论
当Java运行时库提供方便的,预先构建的等效项时,为什么还要在为您的Collections类引入并发性方面遇到麻烦? 本系列的下一篇文章将进一步探讨java.util.concurrent
命名空间。
翻译自: https://www.ibm.com/developerworks/java/library/j-5things4/index.html