通过锁定在Java中实现线程安全的ArrayList

时间:2017-09-26 22:50:58

标签: java multithreading arraylist concurrency synchronization

我想编写一个简单的线程安全arraylist,它支持:

add(),remove(int i),insert(int i),update(int i)和get(int i)

一个简单的实现是向内部数据结构添加锁(例如,一个对象数组),但它不够好,因为一次只有一个线程可以访问该列表。

因此,我的初步计划是为每个数据槽添加锁定,以便不同的线程可以同时访问不同索引中的元素。数据结构如下所示:

class MyArrayList {
    Lock listlock;
    Lock[] locks;
    Object[] array;
}

如果不需要调整大小(),则锁定应该如下:

  • for get(int i),线程需要获取锁[i]。
  • 对于insert(int i),线程需要获取j> = i和listlock的所有锁[j]。
  • for remove(int i),一个线程需要获取j> = i和listlock的所有锁[j]。
  • 对于add(),线程需要获取listlock。
  • 对于insert(),线程需要获取锁[i]。

我的问题是:

  • 如何在更多对象添加时调整大小时处理锁定,我需要创建一个新的更大的数组来保存所有对象。这很烦人,因为其他一些线程也可能会等待释放锁,
  • 有什么更好的建议来实现这种线程安全的arraylist?

1 个答案:

答案 0 :(得分:0)

一个简单的方法是使用read-write lock[Reentrant]ReadWriteLock),这么多线程可以同时读取,但一旦有人获得写锁定,其他人就无法访问该列表。

或者你可以做一些与你的想法有些相似的事情:每个插槽一个读写锁+一个全局("结构")读写锁+一个跟踪{{1案件。所以:

  • 要访问(读取或写入)任何内容,线程至少需要全局读锁定。
  • 只有尝试进行结构更改的线程(更改大小的线程)才会获得全局写锁定,但只能设置一个j >= i变量来指示其中的所有位置是否被锁定" (int modifyingFrom个案)。设置j >= i后,您将降级(请参阅docs)从写入锁定读取锁定,让其他人访问该列表。
  • 任何线程试图做一些不是结构改变的事情,一旦持有全局读锁定,就会检查它想要做什么与modifyingFrom的当前值冲突。如果发生冲突,请一直睡到设置modifyingFrom的线程完成并通知所有正在等待的人。必须同步此检查(仅在某个对象上使用modifyingFrom),因此在冲突线程调用synchronized (obj)并永久休眠(保持)之前,结构更改线程不会发生obj.notify()全局读锁!)。 :(
  • 如果没有发生结构性更改,您应该拥有obj.wait()或将boolean structuralChangeHappening = false设置为某个modifyingFrom(然后您可以检查x > <list size>到{{1} }或i < modifyingFrom)。完成结构更改的线程将get()设置回此值,此处必须同步以通知等待线程。
  • 想要在已经发生结构更改时进行结构更改的线程将等待,因为它需要全局写锁定并且至少有一个线程具有全局读锁定。实际上,它会等到没有人访问该列表。
  • 一个线程分配一个新的(更大......或更小,如果你有一个trimToSize()或某个东西)数组将在整个操作期间保存全局 write lock < / em>的

我很想将全局读写锁视为非常必要,但最后两点证明了这一点。

一些示例案例:

  • 某些线程尝试update()(每个线程都带有modifyingFrom,唯一或不唯一):每个线程都会获得全局读锁定,然后{ {1}}读取锁定,然后读取位置,没有人会等待。
  • 与其他线程尝试get(i)的情况相同:如果没有相等的i,则无人等待。否则,只有线程写入或读取冲突位置的线程才会等待。
  • 线程i启动update([index =] i, element),其他线程尝试i t设置insert([index =] 5, element)后发布全局写锁,所有读取的线程获取全局读锁,然后检查get(i)。那些t的人只能获得插槽的(读)锁;其他人等到modifyingFrom = 5完成并通知,然后锁定插槽。
  • 线程启动modifyingFrom并需要分配一个新数组:一旦获得全局写锁定,其他任何人都无法做任何事情,直到它完成。
  • 列表的大小为7,线程i < modifyingFrom调用insert(5),另一个线程add()调用t_a
    • 如果add(element)碰巧首先获得全局写锁定,它会设置t_g,一旦释放锁定,get([index =] 7)获取全局读锁定,就会看到{{1}睡觉直到t_a完成并通知它。
    • 如果modifyingFrom = 7首先获得全局读锁定,它将检查t_gindex (= 7) >= modifyingFrom,示例前的第4个点),然后抛出异常,因为t_a 释放锁定后!然后t_g能够获得全局写锁定并正常进行。

记住必须同步对7 < modifyingFrom的访问。

你说你只想要那五个操作,但是如果你想要一个迭代器,它可以通过其他方式(不是迭代器本身)来检查是否有某些东西被改变了,就像标准类一样。

现在,我不知道在哪种条件下,这比其他方法更好。另外,请考虑在实际应用程序中可能需要更多限制,因为这应该只确保一致性:如果您尝试读取和写入相同的位置,则可以在写入之前或之后进行读取。也许有一些像modifyingFrom > <list size> (== 7)这样的方法是有意义的,只有在调用方法时没有发生冲突的结构变化时才会这样做,或7 >= <list size>只有在列表位于满足谓词的状态(应该仔细定义,不要导致死锁)。

如果我错过了什么,请告诉我。可能有很多角落案件。 :)