背景 我在内存中有一个(或多或少)巨大的数据模型。该模型包含大约3.150.000到12.600.000个可以直接修改的对象。此外,大约有75.000.000个对象只能通过3.150.000到12.600.000对象进行修改。
另一方面,大约有10个模块访问模型。这些模块可以分为:
问题: 如何同步这样的数据模型?我脑子里有以下想法:
(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
答案 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
可能已经完成了它的任务。这是所有人的胜利。