在开发多线程并发的程序时,对列表进行遍历是一个很常见的操作。比如说在观察者模式中,当某个事件发生时,就需要通知到对应的观察者进行事件的处理,这里就需要对观察者列表进行遍历,逐一触发观察者进行事件的处理。那么,如何保证并发中的遍历操作的原子性呢?大概有下面几种方式:
1. 首先,最容易想到的肯定是使用JAVA内置的同步机制-synchronized,把整个遍历操作当作一个原子操作。
synchronized(lock)
{
for(Observer ob:observers)
{
//trigger event
ob.update(event);
}
}
这里使用的锁对象-lock,应该是和对观察者列表进行增加删除操作时使用的锁是同一个。采用这种方式,如果观察者列表比较少而且观察者进行事件的处理时间比较短的时候,是可接受的。如果观察者列表变的很大或者其中某几个观察者进行事件的处理占时比较长的时候,那么就会引发并发的liveness问题,从而引起性能的下降。
2.索引访问
针对第一种遍历出现的问题,我们可以通过减少synchronized的范围来进行优化。每当我们看到synchronized块的时候,我们要问问自己,synchronized块保护的是什么?很明显,上面保护的是observers列表,保证对该列表并发访问时数据的正确性。也就是说,对于ob.update(event),根本不需要进行保护。从而出现了Doug Lea所说的索引访问:
Observer ob=null;
for(int i=0;true;i++)
{
synchronized(observers)
{
if(i<observers.size())
ob=observers.get(i);
else
break;
}
//下面update调用不需要同步
ob.update(event);
}
虽然这种方式对性能有所提升,但是有可能会出现这样的问题,如果在遍历过程中,对observers列表进行删除,则有可能出现被删除的Observer没有机会进行事件的触发,或者对observers列表进行增加一个相同的元素,而observers本身就允许增加相同元素的时候,那么同一个Observer可能会对同一事件进行两次处理。而对于第一种遍历方式的另外一种改进,则是通过在持有锁的情况下,复制一份observers列表,然后在不持有锁的情况下,对observers列表中的每个observer进行事件的通知:
Object[] tempObservers=null;
synchronized(observers)
{
tempObservers=Arrays.copyOf(observers,observers.length);
}
for(Observer ob:tempObservers)
ob.update(event);
3.Fail-fast
如果不允许第2种遍历方式出现的问题,那么可以通过使用Fail-fast模式来进行强制避免。Fail-fast模式经常出现在java的Iterator中。在Fail-fast模式中,每个列表维护一个int型的版本变量,每次对这个列表进行更新操作都会使这个版本变量加1。
public class List
{
int version;
public synchronized void add(Object obj)
{
//add object
array[cursor++]=obj;
version++;
}
public synchronized void remove(int index)
{
//remove
array[index]=null;
version++;
}
//其余略
}
而在创建一个Iterator时,会将当前的版本变量保存,在通过Iterator进行遍历的时候,会每次都将Iterator内部保存的版本变量和List列表维护的版本变量进行比较,如果不相等,则表示有线程对当前的List列表进行了更新操作,那么Iterator的遍历方式会抛出一个异常进行中止遍历。
public class List
{
int version;
....
public Iterator iterator()
{
return new ListIterator();
}
//inner class
class ListIterator implements Iterator
{
int iteratorVersion;
public ListIterator()
{
iteratorVersion=version;
}
public boolean hasNext()
{
synchronized(List.this){
if(iteratorVersion!=version)
throw new RuntimeException();
//check if the next element exists
}
}
public Object next()
{
synchronized(List.this){
if(iteratorVersion!=version)
throw new RuntimeException();
//return the next element
}
}
}
如果你看过JDK的Iterator实现,你就会发现JDK中提供的大多数Iterator都不是同步的,即使是使用Collections.synchronizedList()后的list,这也就导致了我们常常在使用JDK提供的Iterator时,要自己考虑遍历时的同步,否则会出现这样的问题,iterator的遍历是在一个线程,而对集合进行更新操作的是在另一个线程,那么,由于iterator遍历的过程没有同步,而version变量也不是violate,这样就没法保证version变量在并发中的可见性从而引起问题。而上述的实现了一个有锁的版本。
其实,上面这个synchronized版本的iterator可以进一步优化,可以将version变量声明为violate,从而将所有的的锁给去掉。虽然Fail-fast模式的遍历可以保证并发遍历操作时数据的正确性,但是这种遍历方式很不友好,特别是在大并发的情况下,Fail-fast遍历抛出异常中止的机率很高,试想下,如果在观察者中使用Fail-fast的遍历方式来通知事件的发生,那么在大并发的情况下,一件事件的发生并不保证会通知到所有的观察者,这样有可能就造成了事件的丢失,这个问题导致了Fail-fast的遍历方式的使用业务场景会比较窄。
4.copy on write
而对于copy on write方式,其原理是,每次在对集合进行更新操作的时候,都会对底层的数组进行拷贝。比如说add,remove操作。
public class CopyOnWriteList
{
public synchronized void add(Object obj)
{
Object[] newArray=Arrays.copyOf(oldArray,oldArray.length+1);
newArray[oldArray.length]=obj;
oldArray=newArray;
}
.....
}
那么就可以在进行遍历操作的时候,不需要加锁来保证同步。
public class CopyOnWriteList
{
Object[] oldArray;
....
public Iterator iterator()
{
return new CopyOnWriteIterator(oldArray);
}
public CopyOnWriteIterator implements Iterator
{
private Object[] iteratorArray;
private int cursor;
public CopyOnWriteIterator(Object[] array)
{
iteratorArray=array;
}
//不需要加锁
public boolean hasNext()
{
return cursor++<iteratorArray.length;
}
//不需要加锁
public Object next()
{
return iteratorArray[cursor];
}
}
虽然说copy on write的方式避免了遍历时加锁的问题,但是这种方式只适用于对于集合更新操作比较少,但遍历使用场景比较多的情况下, 否则频繁的COPY操作也会使性能下降。这种方式现在广泛的应用于观察者模式中,因为观察者模式更多的是进行一个事件的通知(即遍历集合),而不是增加/删除观察者。
5.链表-通过对add,remove等更新操作分别进行特殊处理,使得遍历不加锁。
这种实现方式没有锁,也没有violate变量,而是通过维护一个单向链表,并分别对Add,Remove操作进行一些额外的限制。对于Add操作来说,新加入的节点必须加到原先链表头结点之前,新加入的节点变成新的链表头节点:
通过这样的方式,在遍历过程中进行节点的增加操作也不会有并发问题,而对应的remove操作则需要复制被删除节点之前的所有节点,并与被删除的节点后面的节点关联起来,如果要删除的节点是最后一个节点,则相当于复制了整个链表。
如果在遍历过程中进行删除,也不会有并发问题,原先的遍历会一直执行,新的遍历会从新复制的节点开始。因为这里的删除操作是对原来的节点进行了复制,而原先的节点在遍历结束后被GC回收掉。
- 大小: 4.8 KB
- 大小: 8 KB
分享到:
相关推荐
在代码实现中,可能会用到Java的`java.io.File`类来操作文件和目录,`java.util.Queue`接口来实现队列,以及`java.util.concurrent`包中的线程和并发工具类。Python中可以使用`os`和`queue`模块,或者其他编程语言也...
### Java中遍历某个目录下的所有文件及文件夹中的文件 在Java开发中,经常会遇到需要遍历指定目录及其子目录下所有文件的情况。本文将详细介绍如何使用Java标准库中的`java.io.File`类来实现这一功能。我们将通过一...
在目录遍历爬虫中,我们可以用`requests.get()`函数发送GET请求到目标网站的不同URL,以探索其文件和目录。 4. **目录遍历爬虫原理** 目录遍历爬虫的基本思路是构造一系列URL,尝试访问目标网站的不同路径。通常从...
"EPROCESS遍历进程模块"这个主题涉及到的是如何在内核驱动中获取并遍历当前系统中所有进程的加载模块信息。EPROCESS结构体在Windows内核中代表一个进程,而模块则是进程执行时加载的动态链接库(DLL)或其他可执行...
在游戏开发领域,遍历怪物源码通常是指用于在游戏运行时检索、访问或操作游戏中怪物数据的程序代码。这个过程对于游戏调试、修改、数据分析或者制作mod(模组)等有着重要的作用。以下是关于“遍历怪物源码”这一...
在编程领域,线程遍历文件下文件是一个常见的任务,特别是在处理大量数据或者需要实时更新文件系统状态的应用中。这个任务通常涉及到多线程技术、文件I/O操作以及用户界面的交互。以下是对这个主题的详细说明: 1. ...
- 对于大型文件系统,为了提高效率,可以考虑使用并行或并发遍历。这通常需要多线程或多进程的支持,每个线程或进程负责一部分目录的遍历。 7. 错误处理和权限问题: - 在遍历过程中,可能会遇到权限错误、文件不...
在ASP.NET中,遍历指定文件夹的所有文件是一项常见的任务,尤其在处理文件上传、下载、备份或文件操作时。这个任务可以通过使用System.IO命名空间中的类来实现,特别是Directory和FileInfo这两个类。下面我们将详细...
在“易语言多线程遍历目录”这个主题中,我们将深入探讨如何使用易语言来实现一个功能,即在多线程环境下遍历指定目录及其子目录中的所有文件。 首先,我们要理解“多线程”的概念。在单线程程序中,执行任务是串行...
在IT领域,尤其是在Web...在实际操作中,还需要考虑到错误处理(如文件不存在、权限问题等)、性能优化(如限制并发线程数量)、代码的可读性和可维护性等因素。希望以上内容能帮助你理解这个任务所涉及的IT知识点。
在本实例中,我们关注的是“vc cmarkup 遍历 xml 树”,这表明我们将讨论如何在Visual C++(VC6)中使用CMARKUP类来解析并遍历XML树结构。XML(eXtensible Markup Language)是一种用于存储和传输结构化数据的标准...
iOS 遍历方法总结 ...这种遍历方法可以实现并发遍历,充分利用系统的资源,对大型数据集也适用。 iOS 中遍历方法有多种,每种方法都有其特点和应用场景。开发者可以根据实际情况选择合适的遍历方法。
文件遍历是C#编程中一个基础且实用的功能,它允许开发者访问指定目录及其子目录中的所有文件和子目录。这个功能在处理文件系统操作时非常常见,比如备份、搜索、分析或清理文件等。 在描述中提到的“线程操作控件”...
FTP(File Transfer Protocol)是一种广泛使用的互联网协议,用于在客户端和服务器之间进行文件传输。在C++编程环境中,我们可以利用FTP库,...在实际应用中,可能还需要考虑并发处理、进度显示、中断恢复等高级特性。
在Java编程中,遍历文件夹是一项常见的任务,特别是在处理大量数据或进行文件操作时。这个过程涉及到对文件系统...在实际应用中,你可能需要根据项目需求进行适当的调整,例如添加错误处理、优化性能或者支持并发遍历。
在“易语言多线程遍历目录源码”中,你可以学习到如何创建线程、如何将遍历目录的任务分解到多个线程、如何处理遍历过程中的文件和目录,以及如何在易语言中实现线程间的同步和通信。通过分析和理解这段源码,你将...
5. **同步与异步**:根据应用场景,可以选择同步遍历(等待每个操作完成再进行下一个)或异步遍历(并发处理多个任务),以平衡响应速度和资源消耗。 对于"ext.rar"这个压缩包,我们可以假设它包含了一个示例程序,...
如果在遍历过程中需要移除元素,必须使用`it.remove()`,直接调用`list.remove()`会导致并发修改异常(ConcurrentModificationException)。 ### 第二种:增强型for循环(foreach) ```java for (A a : list) { /...
标题提到的"遍历文件夹中每个文件,寻找并修改某个具体的字段"是一个典型的文件处理任务,常见于数据处理、文本挖掘或日志分析等场景。这里我们将深入探讨如何使用Node.js来实现这个功能,以及涉及的相关技术知识点...
通过递归调用`readFile`函数,程序能够深入到每一个子目录中继续遍历,对于每一个文件,程序会使用`fs.statSync`同步获取文件的状态信息,从而得到文件的大小。 统计文件大小时,程序首先创建一个数组`filesList`,...