如果在迭代时同步,Collections.synchronizedMap 会是线程安全的吗?
Will Collections.synchronizedMap be thread safe if synchronized while iteration?
我在迭代 Collections.synchronizedMap
时尝试修复与 ConcurrentModificationException
相关的错误。
按照Javadoc的要求,地图上的迭代过程已经同步。
我检查了迭代过程,地图的大小没有明显的变化(方法调用trace很长,我会仔细检查)。
除了迭代过程中的修改,还有没有其他可能导致这个异常的可能?
由于迭代已经同步,据我了解,其他线程将无法进行 add()
或 remove()
等修改,对吗?
我对这些东西真的很陌生。非常感谢任何帮助。
正在更新
非常感谢大家的帮助,特别是@Marco13 的详细解说。我做了一个小代码来测试和验证这个问题,代码附在这里:
public class TestCME {
public static void main(String[] args){
TestMap tm = new TestMap();
for(int i = 0;i < 50;i++){
tm.addCity(i,new City(i * 10));
}
RunnableA rA = new RunnableA(tm);
new Thread(rA).start();
RunnableB rB = new RunnableB(tm);
new Thread(rB).start();
}
}
class TestMap{
Map<Integer,City> cityMap;
public TestMap(){
cityMap = Collections.synchronizedMap(new HashMap<Integer,City>());
}
public Set<Integer> getAllKeys(){
return cityMap.keySet();
}
public City getCity(int id){
return cityMap.get(id);
}
public void addCity(int id,City city){
cityMap.put(id,city);
}
public void removeCity(int id){
cityMap.remove(id);
}
}
class City{
int area;
public City(int area){
this.area = area;
}
}
class RunnableA implements Runnable{
TestMap tm;
public RunnableA(TestMap tm){
this.tm = tm;
}
public void run(){
System.out.println("Thread A is starting to run......");
if(tm != null && tm.cityMap != null && tm.cityMap.size() > 0){
synchronized (tm.cityMap){
Set<Integer> idSet = tm.getAllKeys();
Iterator<Integer> itr = idSet.iterator();
while(itr.hasNext()){
System.out.println("Entering while loop.....");
Integer id = itr.next();
System.out.println(tm.getCity(id).area);
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}
}
/*Set<Integer> idSet = tm.getAllKeys();
Iterator<Integer> itr = idSet.iterator();
while(itr.hasNext()){
System.out.println("Entering while loop.....");
Integer id = itr.next();
System.out.println(tm.getCity(id).area);
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}*/
}
}
}
class RunnableB implements Runnable{
TestMap tm;
public RunnableB(TestMap tm){
this.tm = tm;
}
public void run(){
System.out.println("Thread B is starting to run......");
System.out.println("Trying to add elements to map....");
tm.addCity(50,new City(500));
System.out.println("Trying to remove elements from map....");
tm.removeCity(1);
}
}
我正在尝试修复我的错误,所以代码有点冗长,对此我深表歉意。在线程 A 中,我在地图上进行迭代,而在线程 B 中,我试图在地图中添加和删除元素。通过在地图上进行正确的同步(如@Marco13 建议的那样),我不会看到 ConcurrentModificationException,如果没有同步或在 TestMap 对象上同步,则显示异常。我想我现在明白这个问题了。非常欢迎任何关于此的双重确认或建议。再次感谢。
一些一般性陈述,基于已添加的代码(同时出于某种原因再次删除)。
EDIT Also see the update at the bottom of the answer
您应该知道您实际同步的是什么。当您使用
创建地图时
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(map);
然后同步将在 synchronizedMap
上进行。这意味着以下是线程安全的:
void executedInFirstThread()
{
synchronizedMap.put(someKey, someValue);
}
void executedInSecondThread()
{
synchronizedMap.remove(someOtherKey);
}
虽然这两个方法可能被不同的线程同时执行,但是它是线程安全的:当第一个线程执行put
方法时,第二个线程将不得不等待 在执行 remove
方法之前。 put
和 remove
方法 永远不会 同时执行。这就是同步的目的。
但是,ConcurrentModificationException
的含义更广泛。关于特定情况,稍微简化一下:ConcurrentModificationException
表示地图已修改 而 地图上的迭代正在进行中。
因此,必须同步整个迭代,而不仅仅是单个方法:
void executedInFirstThread()
{
synchronized (synchronizedMap)
{
for (Key key : synchronizedMap.keySet())
{
System.out.println(synchronizedMap.get(key));
}
}
}
void executedInSecondThread()
{
synchronizedMap.put(someKey, someValue);
}
没有 synchronized
块,第一个线程可以 部分地 迭代地图,然后,在迭代的中间,第二个线程可以调用 put
在地图上(因此,执行 并发修改 )。当第一个线程想要继续迭代时,将抛出 ConcurrentModificationException
。
使用 synchronized
块,这种情况不会发生:当第一个线程进入此 synchronized
块时,第二个线程将不得不等待,然后才能调用 put
。 (它必须等到第一个线程离开 synchronized
块。之后,它可以安全地进行修改)。
但是,请注意,整个概念始终指的是在 上同步的对象。
在您的情况下,您有一个 class 类似于以下内容:
class TestMap
{
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(new HashMap<Key, Value>());
public Set<Key> getAllKeys()
{
return synchronizedMap.keySet();
}
}
然后,你是这样使用的:
TestMap testMap = new TestMap();
synchronized(testMap)
{
Set<Key> keys = testMap.getAllKeys();
for (Key key : keys)
{
...
}
}
}
在这种情况下,您在 testMap
对象上进行同步, 而不是 在它包含的 sychronizedMap
对象上进行同步。因此,迭代不会发生同步。一个线程可以执行迭代(在 testMap
对象上同步)。同时,不同的线程可以(同时)修改 synchronizedMap
,这将导致 ConcurrentModificationException
.
解决方案取决于整体结构。一种可能的解决方案是公开 synchronizedMap
...
class TestMap
{
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(new HashMap<Key, Value>());
...
public Object getSynchronizationMonitor()
{
return synchronizedMap;
}
}
并将其用于迭代的同步...
TestMap testMap = new TestMap();
synchronized(testMap.getSynchronizationMonitor())
{
...
}
...但这不是一般性建议,但只是为了传达这个想法:你必须知道对象是什么你必须同步。对于实际应用,很可能有更优雅的解决方案。
Update:
再说一句,现在又把"real"代码加到问题里了:你也可以考虑ConcurrentHashMap
。它 明确设计 并发使用,并在内部执行所有必要的同步 - 特别是,它在内部使用一些技巧来避免 "large" 同步块。在问题中当前显示的代码中,地图在迭代时将是 "locked",并且没有其他线程能够对地图进行任何修改,这可能会对整体性能产生负面影响。
我在迭代 Collections.synchronizedMap
时尝试修复与 ConcurrentModificationException
相关的错误。
按照Javadoc的要求,地图上的迭代过程已经同步。
我检查了迭代过程,地图的大小没有明显的变化(方法调用trace很长,我会仔细检查)。
除了迭代过程中的修改,还有没有其他可能导致这个异常的可能?
由于迭代已经同步,据我了解,其他线程将无法进行 add()
或 remove()
等修改,对吗?
我对这些东西真的很陌生。非常感谢任何帮助。
正在更新
非常感谢大家的帮助,特别是@Marco13 的详细解说。我做了一个小代码来测试和验证这个问题,代码附在这里:
public class TestCME {
public static void main(String[] args){
TestMap tm = new TestMap();
for(int i = 0;i < 50;i++){
tm.addCity(i,new City(i * 10));
}
RunnableA rA = new RunnableA(tm);
new Thread(rA).start();
RunnableB rB = new RunnableB(tm);
new Thread(rB).start();
}
}
class TestMap{
Map<Integer,City> cityMap;
public TestMap(){
cityMap = Collections.synchronizedMap(new HashMap<Integer,City>());
}
public Set<Integer> getAllKeys(){
return cityMap.keySet();
}
public City getCity(int id){
return cityMap.get(id);
}
public void addCity(int id,City city){
cityMap.put(id,city);
}
public void removeCity(int id){
cityMap.remove(id);
}
}
class City{
int area;
public City(int area){
this.area = area;
}
}
class RunnableA implements Runnable{
TestMap tm;
public RunnableA(TestMap tm){
this.tm = tm;
}
public void run(){
System.out.println("Thread A is starting to run......");
if(tm != null && tm.cityMap != null && tm.cityMap.size() > 0){
synchronized (tm.cityMap){
Set<Integer> idSet = tm.getAllKeys();
Iterator<Integer> itr = idSet.iterator();
while(itr.hasNext()){
System.out.println("Entering while loop.....");
Integer id = itr.next();
System.out.println(tm.getCity(id).area);
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}
}
/*Set<Integer> idSet = tm.getAllKeys();
Iterator<Integer> itr = idSet.iterator();
while(itr.hasNext()){
System.out.println("Entering while loop.....");
Integer id = itr.next();
System.out.println(tm.getCity(id).area);
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
}*/
}
}
}
class RunnableB implements Runnable{
TestMap tm;
public RunnableB(TestMap tm){
this.tm = tm;
}
public void run(){
System.out.println("Thread B is starting to run......");
System.out.println("Trying to add elements to map....");
tm.addCity(50,new City(500));
System.out.println("Trying to remove elements from map....");
tm.removeCity(1);
}
}
我正在尝试修复我的错误,所以代码有点冗长,对此我深表歉意。在线程 A 中,我在地图上进行迭代,而在线程 B 中,我试图在地图中添加和删除元素。通过在地图上进行正确的同步(如@Marco13 建议的那样),我不会看到 ConcurrentModificationException,如果没有同步或在 TestMap 对象上同步,则显示异常。我想我现在明白这个问题了。非常欢迎任何关于此的双重确认或建议。再次感谢。
一些一般性陈述,基于已添加的代码(同时出于某种原因再次删除)。
EDIT Also see the update at the bottom of the answer
您应该知道您实际同步的是什么。当您使用
创建地图时Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(map);
然后同步将在 synchronizedMap
上进行。这意味着以下是线程安全的:
void executedInFirstThread()
{
synchronizedMap.put(someKey, someValue);
}
void executedInSecondThread()
{
synchronizedMap.remove(someOtherKey);
}
虽然这两个方法可能被不同的线程同时执行,但是它是线程安全的:当第一个线程执行put
方法时,第二个线程将不得不等待 在执行 remove
方法之前。 put
和 remove
方法 永远不会 同时执行。这就是同步的目的。
但是,ConcurrentModificationException
的含义更广泛。关于特定情况,稍微简化一下:ConcurrentModificationException
表示地图已修改 而 地图上的迭代正在进行中。
因此,必须同步整个迭代,而不仅仅是单个方法:
void executedInFirstThread()
{
synchronized (synchronizedMap)
{
for (Key key : synchronizedMap.keySet())
{
System.out.println(synchronizedMap.get(key));
}
}
}
void executedInSecondThread()
{
synchronizedMap.put(someKey, someValue);
}
没有 synchronized
块,第一个线程可以 部分地 迭代地图,然后,在迭代的中间,第二个线程可以调用 put
在地图上(因此,执行 并发修改 )。当第一个线程想要继续迭代时,将抛出 ConcurrentModificationException
。
使用 synchronized
块,这种情况不会发生:当第一个线程进入此 synchronized
块时,第二个线程将不得不等待,然后才能调用 put
。 (它必须等到第一个线程离开 synchronized
块。之后,它可以安全地进行修改)。
但是,请注意,整个概念始终指的是在 上同步的对象。
在您的情况下,您有一个 class 类似于以下内容:
class TestMap
{
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(new HashMap<Key, Value>());
public Set<Key> getAllKeys()
{
return synchronizedMap.keySet();
}
}
然后,你是这样使用的:
TestMap testMap = new TestMap();
synchronized(testMap)
{
Set<Key> keys = testMap.getAllKeys();
for (Key key : keys)
{
...
}
}
}
在这种情况下,您在 testMap
对象上进行同步, 而不是 在它包含的 sychronizedMap
对象上进行同步。因此,迭代不会发生同步。一个线程可以执行迭代(在 testMap
对象上同步)。同时,不同的线程可以(同时)修改 synchronizedMap
,这将导致 ConcurrentModificationException
.
解决方案取决于整体结构。一种可能的解决方案是公开 synchronizedMap
...
class TestMap
{
Map<Key, Value> synchronizedMap =
Collections.synchronizedMap(new HashMap<Key, Value>());
...
public Object getSynchronizationMonitor()
{
return synchronizedMap;
}
}
并将其用于迭代的同步...
TestMap testMap = new TestMap();
synchronized(testMap.getSynchronizationMonitor())
{
...
}
...但这不是一般性建议,但只是为了传达这个想法:你必须知道对象是什么你必须同步。对于实际应用,很可能有更优雅的解决方案。
Update:
再说一句,现在又把"real"代码加到问题里了:你也可以考虑ConcurrentHashMap
。它 明确设计 并发使用,并在内部执行所有必要的同步 - 特别是,它在内部使用一些技巧来避免 "large" 同步块。在问题中当前显示的代码中,地图在迭代时将是 "locked",并且没有其他线程能够对地图进行任何修改,这可能会对整体性能产生负面影响。