大数据模型中的线程安全

时间:2013-11-16 09:13:34

标签: java multithreading synchronization thread-safety

背景 我在内存中有一个(或多或少)巨大的数据模型。该模型包含大约3.150.000到12.600.000个可以直接修改的对象。此外,大约有75.000.000个对象只能通过3.150.000到12.600.000对象进行修改。

另一方面,大约有10个模块访问模型。这些模块可以分为:

  • 每250毫秒到1000毫秒读取和修改一些对象
  • 按需阅读和修改部分对象
  • 读取部分对象(如果已更改)

问题: 如何同步这样的数据模型?我脑子里有以下想法:

(1)锁定每个可以直接修改的类。 优点:只能锁定已修改的对象。 缺点:高同步工作量和大量锁定实例(3.150.000到12.600.000附加对象/锁)。在同步(死锁等)中做错事的风险很大。

(2)访问整个数据模型的中央界面。此接口将通过单个锁定锁定整个模型的每个修改。 优点:只有一个锁 - >减少同步工作量。 缺点:无论更改类型如何,整个模型都会被锁定。

(3)Dispatch Thread(如在AWT / Swing中)。处理任务(事件)的线程。 优点/缺点如想法(2)。但是,这将是一个基于事件的解决方案。我阅读了Graham Hamilton关于GUI-tollkits中多线程的文章。此外,John Ousterhout还谈到了“事件与线程”。当然,我的数据模型并不广泛,但文章深入到了问题的核心。

这里链接到Graham Hamilton的文章:https://weblogs.java.net/blog/kgh/archive/2004/10/multithreaded_t.html

那么,你有什么经历?也许你有更好的主意。

编辑:我在对象计算上犯了一个大错误。我刚刚更新了金额。

提前致谢:)

编辑2:这是我刚刚为演示目的创建的模型:

enum Ware { WOOD, COAL, STONE }
class Stock { Map<Ware, Integer> internalStock; }
class Coordinate { int x; int y; }
interface ILand {}

class World {
  Map<Coordinate, ILand> land;
  Map<Coordinate, Ship> ships;
}

class Island implements ILand { Stock resources; }
class Ship { Stock stock; }
class Building {Stock stock; }

class Colony implements ILand {
  Island builtOn;
  Set<Building> building;
}

class Character {
  Set<Colony> colonies;
  Set<Ship> fleet;
}

这将是数据模型的结构:

Model
   World     <>--- ILand
             <>--- Ship
   Character <>--- Colony <>--- Building <>--- Stock
                          <>--- Island   <>---Stock
             <>--- Ship   <>--- Stock

5 个答案:

答案 0 :(得分:2)

您可能需要考虑将数据模型转换为不可变的持久数据结构

这种方法在Scala和Clojure等语言中非常有效。如果您想更好地理解这种技术,以下视频非常值得关注:

http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

当您拥有重要的并发访问权限时,这通常是一个很好的策略:它具有各种优势:

  • 读者不需要任何锁定。在有许多读者的情况下,这可以获得巨大的成功。
  • 更新可能会发生原子 - 这意味着您永远不会冒着读者看到数据处于不一致状态的风险。
  • 您可以随时获取整个数据结构的“快照”。由于不可变数据结构无法更改,因此您可以自由地获取对它的引用,然后在休闲时检查它
  • 更新仍然相对便宜:结构共享意味着您可以在不复制整个数据模型的情况下,通过一些更改来创建新版本的数据模型。
  • 您可以根据自己的要求使用各种不同的更新语义。在这种情况下,听起来你有一个“读取和更新”语义与某种形式的变更通知相结合。

答案 1 :(得分:1)

a)*这是在真实社交游戏中使用的解决方案*如果您能想到对象的关键字或正确的equals / hashCode函数,您可以将它们放入{{ 1}}。此映射中的每个当前实体都表示对象的锁定状态。这将导致每个实体开销40个字节。

b)您可以优化以前的解决方案,并提出另一个散列函数,将所有对象拆分为一些合理大小的桶,即100个元素(您可以通过运行测试来测量所需的数量)。在这种情况下,整个存储桶将被锁定,这将节省一些额外的字节。这将导致每个实体产生大约12字节的开销,以便在桶中存储元素(即在ConcurrentHashMap中)。

c)第三个选项,AtomicBitSet implementation for java。这是对第二种方法的修改。铲斗可以通过紧凑的原子机锁定。这比第二个选项好一点,这个优点是你可以拥有更小的存储桶,因为它们需要更少的内存(ArrayList每桶约40个字节,而每个存储桶几个比特) ConcurrentHashMap)。

<强>锁

状态可能比仅锁定/未锁定更复杂。因此,而不是维护地图:

AtomicBitSet

或者

 lock map: objectId -> {true | false}

可以存储锁定信息:

 lock map: bucket of objectIds -> {true | false}

如果此地图中没有对象,则没有人锁定它。在其他情况下,使用 lock map: objectId -> {ReadWriteLock lock, Thread owner, long writeLockGrantedAtMs} 描述的锁定策略锁定对象。 ReadWriteLock可以用来打断writeLockedAtMs,如果他持有它的时间太久了。

<强> ADDED

我不确定你是否需要这个,但是如果你为你的实体实现原子锁并且在锁定时按owner重新排序它们就可以完全避免死锁。这可以通过以超时顺序对每个对象应用锁来完成。简化的伪代码:

hashCode

数据模型的更新

我实际上为3个社交游戏实施了结构a),这些游戏在生产中运行了1 - 2年。最终的解决方案有点复杂,包括持久性,监视和死锁解析,但这是一项要求,并不是非常需要。

例如,如果要将void lockObjects (f, e, a) { reorder (f, e, a) if(!tryLock(a, timeout: 10ms)){ throw "could not lock a"; } if(!tryLock(e, timeout: 10ms)){ throw "could not lock e"; } if(!tryLock(f, timeout: 10ms)){ throw "could not lock f"; } // now these objects are locked, deadlocks avoided } 添加到Colony,则可以锁定字符。而且你应该确保你总是锁定你的对象/除了获得一个锁之外别无他法获取你的对象。

如果您要将Character添加到六个Colony,您可以非原子地执行此操作,即按顺序将Character添加到每个Colony(每个添加为Character原子)或实现原子锁并锁定所有七个对象。如果锁存在一些问题,可以注意到差异 - 在第一种情况下,您会得到更大的延迟,在第二种情况下,您可能会获得部分更新。

答案 2 :(得分:0)

写一些单元测试。对于模型,从高级锁开始,如synchronized方法。如果某些方法只是修改地图而不是其中的对象,请考虑使用ConcurrentHashMaps代替同步方法。

答案 3 :(得分:0)

变体2和3将并行性降低到零(每个时刻只有一个线程可以访问数据)。变体1最大化并行性(实际上每个对象一个锁,而不是每个类)。在这种情况下,同步工作很少(低争用)。锁定的内存被7500万背景对象的存在所掩盖。通过仔细的同步调度(避免资源图上的循环)可以(并且应该)避免死锁。

答案 4 :(得分:0)

  • 我想说的第一点非常重要,就是让你的课程尽可能地不变。随着不可变的线程安全免费。您可以通过同步共享您的对象,JMM将负责安全发布,即您可以在不进行同步的情况下解决可见性问题。

  • 一旦开始考虑不可变对象,就会想到Flyweight模式,它可以帮助您减少对象创建中的内存占用量,并且这种性能也会提高。因为你知道你的类是不可变的,并且只有一个对象存在于一种类型中,你可以缓存很多信息,如对象的hashCode,也可以懒惰地计算。

  • 您可以从synchronization界面转到tryLock,而不是使用Lock阻止,这可以帮助您防止死锁。

  • 您也可以使用ReadWriteLock。对于读取使用,读取锁定和写入使用写入锁定。

  • 您可以使用全局排序来消除死锁。假设您有这样的方法:


public void fun(MyClass1 o1, MyClass2 o2)
{
    synchronized(o1)
      {
        synchronized(o2){
            .........
            .........
            .........
      }
}

这里很有可能出现死锁,所以在这里你可以使用全局锁定顺序,如:


public void fun(MyClass1 o1, MyClass2 o2)
{
    long l1 = System.identityHashCode(o1);
    long l2 = System.identityHashCode(o2);
    if(l1>l2){
      synchronized(o1)
      {
        synchronized(o2)
        {
            .........
            .........
            .........
       }
    }
   }
   else if (l1<l2)
   {
      synchronized(o2)
      {
        synchronized(o1)
        {
            .........
            .........
            .........
       }
    }
   }
 else//if equal than resolve by another mutex
 {
     synchronize(o)
     {
          synchronize(o1)
          {
              synchronize(o2)
              {
                  .........
                  .........
                  .........

              }
          }
      }
 }
}

计算identityHashCode如果long1 > long2然后是一种类型的排序,如果long2 > long1比另一种排序类型,如果存在平局,则使用互斥o来解决。这有助于避免死锁。

  • 您可以使用ConcurrentLinkedQueue之类的并发数据结构,它使用compareAndSwap机器指令,它也是非阻塞和无锁定的。

  • 考虑使用ConcurrentHashMap,它在多线程环境中提供更好的可伸缩性,因为它使用锁定条带化。

  • 使用Lock界面时,请确保使用非公平锁定,因为它可以很好地扩展并在现实世界中提供更好的性能。 原因:假设您认为线程A刚刚完成一个作业,然后下一个线程B就可以获得CPU了。在给出cpu之前,应该将数据带入缓存,应该更改状态以及许多事情要做。如果那时线程C来自cpu,并且如果你有非公平锁定,那么新线程C将被赋予cpu并且它会提高整体系统性能。因为在B完成唤醒之前,这个新线程C可能已经完成了它的任务。这是所有人的胜利。