一种常见的并发编程场景的处理

对于并发编程,大家想到总是多线程之间对等的临界资源竞争。然而经常会遇到下面这样的场景:

守护线程提供一个临界资源,多个子线程会并发改写该临界资源。大部分时候(99.9%的时间),主线程是不会干涉各个线程之间的竞争的,通常只要该临界资源自己内部处理好同步即可。但是偶尔主线程也会干预一下该临界资源,比如做一些统计,做一个快照,或者复制数据然后清空等。这个操作通常会耗时比较长,并且在此期间不希望有人改写临界资源。如果,主线程与各个子线程使用同样的锁或者synchronized同步,那么在主线程没有作该操作时,各个子线程之间会因为竞争而阻塞,这个阻塞开起来是没有必要的。

这里介绍一个利用volatile变量的特性解决该问题。试图在性能和数据保护上面达到最大平衡。Atomic变量采用的是寄存器变量实现的。用两个变量标志map表当前的状态。而不必在后台线程和众多业务线程之间加锁或者同步,由于在多数情况下volatile变量的性能优于锁(Java 理论与实践: 正确使用 Volatile 变量)。这里只以map表举例,其他保证线程安全的数据结构也适用。

试想,在addOneRecord方法和getAndClearAllRecords方法之间引入一个锁或者synchronized,那么每两个业务线程在添加数据的时候都要竞争,而这个竞争是不必要的。因为,只有在后台线程调用getAndClearAllRecords时,这个锁才有意义。这里引入两个寄存器变量,能有效降低一些锁竞争的问题。当然这里只是拿concurrenthashmap来举一个例子,也许在doSomeThingWithMap方法中根本没有什么需要独占整个map表的,那么整个同步机制都不需要了,只要map表自身的同步即可。

/**
    * 用于标志长时间独占表的线程是否正在工作。
    */
   private AtomicBoolean closed = new AtomicBoolean(false);

   /**
    * 用于标志当前的数据表是否正在插入数据。
    */
   private AtomicInteger busy = new AtomicInteger();

   /**
    * 用于存储数据的表格。
    */
   private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
   /**
    * 多个线程会并发调用该接口向
    * map表中插入数据。
    * @param key
    * @param value
    */
   public void addOneRecord(String key, String value)
   {
       /**
        * 大多数时候,该循环都不会占用性能资源。
        */
       while (closed.get())
       {
           waitAMoment();
       }

       if (map != null)
       {
           /**
            * 用一个同步变量在各个线程中计数。
            */
           busy.getAndIncrement();
           map.put(key, value);
           busy.getAndDecrement();
       }
   }

   /**
    * 后台线程会定期处理map表中的数据。
    */
   public void getAndClearAllRecords()

   {
       synchronized (this)
       {
           /**
            * 先将closed标志设置,以便独占该map表;
            * 避免操作过程中被修改。
            */

           closed.set(true);
           while (busy.get() != 0)
           {
               waitAMoment();
           }
           doSomeThingWithMap(map);
           closed.set(false);
       }
   }

   private void doSomeThingWithMap(ConcurrentHashMap<String, String> map)
   {
       // TODO Auto-generated method stub
   }

   private void waitAMoment()
   {
       // TODO Auto-generated method stub
   }

总觉得分析这个问题有点纠结的感觉,其实用java,这点性能损失是可以忽略不计的。并且外层两个寄存器变量的读写并不一定比加锁性能好,而且在外层加锁的情况下,map表自身的锁竞争就不存在了,实际也只有1个锁在起作用,似乎问题没有那么糟糕。不管怎么样,这个工作感觉还是有一些意义的,总归是利大于弊吧。

时间: 2024-10-19 08:47:13

一种常见的并发编程场景的处理的相关文章

iOS中有3种常见的多线程编程方法

1.NSThread 这种方法需要管理线程的生命周期.同步.加锁问题,会导致一定的性能开销 2.NSOperation和NSOperationQueue 是基于OC实现的.NSOperation以面向对象的方式封装了需要执行的操作,然后可以将这个操作放到一个NSOperationQueue中去异步执行.不必关心线程管理.同步等问题. 3.Grand Centeral Dispatch 简称GCD,iOS4才开始支持,是纯C语言的API.自iPad2开始,苹果设备开始有了双核CPU,为了充分利用这

【专家坐堂】四种并发编程模型简介

本文来自网易云社区 概述 并发往往和并行一起被提及,但是我们应该明确的是"并发"不等同于"并行" ?       并发 :同一时间 对待 多件事情 (逻辑层面) ?       并行 :同一时间 做(执行) 多件事情 (物理层面) 并发可以构造出一种问题解决方法,该方法能够被用于并行化,从而让原本只能串行处理的事务并行化,更好地发挥出当前多核CPU,分布式集群的能力. 但是,并发编程和人们正常的思维方式是不一样的,因此才有了各种编程模型的抽象来帮助我们更方便,更不容

《java并发编程实战》笔记(一)

最近在看<java并发编程实战>,希望自己有毅力把它读完. 线程本身有很多优势,比如可以发挥多处理器的强大能力.建模更加简单.简化异步事件的处理.使用户界面的相应更加灵敏,但是更多的需要程序猿面对的是安全性问题.看下面例子: public class UnsafeSequence { private int value; /*返回一个唯一的数值*/ public int getNext(){ return value++; } } UnsafeSequence的问题在于,如果执行时机不对,那么

日志系统数据采集客户端的实现--并发编程容器选型

一个集中的日志系统,第三方应用每次写日志,都需要发送一个远程的rpc或者http请求,造成写日志的延时比较大. 改进的做法是:提供一个写日志调用包,第三方应用写日志时,先把日志缓存到一个线程安全的容器里,然后后台线程实时消费容器内的日志,如果有持久化的需求,就可以实时的把日志flush到文件中,然后再用另外一个线程消费文件真正把日志发送到日志服务器. 所以我们需要一个线程安全的容器,以下是常见的并发编程容器. CopyOnWriteArrayList是线程安全的, 且处理读操作不需要进行同步和加

高级程序员需知的并发编程知识(二)

说明 本篇是继上一篇并发编程未讨论完的内容的续篇.上一篇传送门: Java并发编程一万字总结(吐血整理) 活跃性问题 在上一篇我们讨论并发编程带来的风险的时候,说到其中 一个风险就是活跃性问题.活跃性问题其实就是我们的程序在某些场景或条件下执行不下去了.在这个话题下我们会去了解什么是死锁.活锁以及饥饿,该如何避免这些情况的发生. 死锁 我们一般使用加锁来保证线程安全,但是过度地使用加锁,可能导致死锁发生. 哲学家进餐问题 "哲学家进餐"问题能很好地描述死锁的场景.5个哲学家去吃火锅,坐

python 并发编程 多进程 目录

python multiprocessing模块 介绍 python 开启进程两种方法 python 并发编程 查看进程的id pid与父进程id ppid 原文地址:https://www.cnblogs.com/mingerlcm/p/11029215.html

java并发编程常见锁类型

锁是java并发编程中最重要的同步机制.锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息.锁是解决并发冲突的重要工具.在开发中我们会用到很多类型的锁,每种锁都有其自身的特点和适用范围.需要深刻理解锁的理念和区别,才能正确.合理地使用锁.常用锁类型乐观锁与悲观锁悲观锁对并发冲突持悲观态度,先取锁后访问数据,能够较大程度确保数据安全性.而乐观锁认为数据冲突的概率比较低,可以尽可能多地访问数据,只有在最终提交数据进行持久化时才获取锁.悲观锁总是先获取锁,会增加很多额外的开销,

深入浅出 Java Concurrency (38): 并发总结 part 2 常见的并发场景[转]

常见的并发场景 线程池 并发最常见用于线程池,显然使用线程池可以有效的提高吞吐量. 最常见.比较复杂一个场景是Web容器的线程池.Web容器使用线程池同步或者异步处理HTTP请求,同时这也可以有效的复用HTTP连接,降低资源申请的开销.通常我们认为HTTP请求时非常昂贵的,并且也是比较耗费资源和性能的,所以线程池在这里就扮演了非常重要的角色. 在线程池的章节中非常详细的讨论了线程池的原理和使用,同时也提到了,线程池的配置和参数对性能的影响是巨大的.不尽如此,受限于资源(机器的性能.网络的带宽等等

19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition

Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权.因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去.因此,一般情况下,当队列满时,会让生产者交出对