java.util.concurrent,第1部分

并发集合是Java™的一个巨大补充,但是许多Java开发人员避免使用此程序包,因为他们认为,像它要解决的问题一样,它必须很复杂。

实际上, java.util.concurrent包含许多类,这些类可以有效解决常见的并发问题,而无需您费劲。 继续阅读以了解诸如CopyOnWriteArrayListBlockingQueue类的java.util.concurrent类如何帮助您解决多线程编程的有害挑战。

1. TimeUnit

尽管它本身不是Collections类,但java.util.concurrent.TimeUnit枚举使代码更容易阅读。 使用TimeUnit可以使开发人员免于使用您的方法或API的烦恼。

TimeUnit包含所有时间单位,从MILLISECONDSMICROSECONDSDAYSHOURS ,这意味着它可以处理开发人员可能需要的几乎所有时间跨度类型。 而且,由于在枚举中声明了转换方法,所以当时间加快时,将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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值