【Java 多线程与并发库】Java5 同步集合类的应用

851 阅读6分钟
原文链接: zhuanlan.zhihu.com

传统的集合,在并发访问的时候,是有问题的。如hashset、hashmap和arrayList,多个线程在对他们取数据、放数据的时候,是有问题的,因为他们是线程不安全的。由于没有控制并发,会导致数据的不一致,引起死循环。

为什么会引起死循环?拿HashMap来看,看一下HashMap的get函数的源代码:

public V get(Object key){  
    if(key == null){ return getForNullKey(); }  
    int hash = hash(key.hashCode());  
    for(Entry e = table[indexFor(hash,table.length)];  
    e != null;  
        e = e.next()){  
        if(e.hash == hash && ((k = e.key) == key||key.equals(k)))  
            return e.value;  
    }  
    return null;  
}  

get函数会根据key的hashCode来锁定多个对象,并且遍历这些对象来找到key所对应的对象。当多个线程不安全的修改HashMap数据结构的时候,有可能使得这个函数进入死循环。

再比如一个线程在遍历数据

while(hasnext()){  
   next(){cursor++;}  
}  

其中hasnext

hasnext(){  
  if(cursor == count){  
    return false;  
  }  
  return true;  
}  

此时还有一个线程在取数据

remove(){  
  count--;  
}  

也就是说,一个循环取,一个循环拿的时候,取的集合的next已经到底了,而拿的集合打断了取的过程,从集合中拿了数据,改变了标志位count的值,这个时候取的集合继续运行,发现count++还没到底,继续循环,此时又有其他取的线程打断它,那么这个循环就会永远运行下去不跳出来,这就又形成了死循环。

如果要使用到多个线程中去,需要加上自己的同步标志或者使用concurrent包下的同步集合。

  • 传统同步集合

要拿到一个传统同步集合很简单:

Collections.synchronizedMap(Map someMap);

给它一个普通的Map,它就会返回一个线程安全的同步Map。我们来看一下synchronizedMap的源码:

private static class SynchronizedMap  
        implements Map, Serializable {  
        private static final long serialVersionUID = 1978198479659022715L;  
  
        private final Map m;     // Backing Map  
        final Object      mutex;        // Object on which to synchronize  
  
        SynchronizedMap(Map m) {  
            if (m==null)  
                throw new NullPointerException();  
            this.m = m;  
            mutex = this;  
        }  
  
        SynchronizedMap(Map m, Object mutex) {  
            this.m = m;  
            this.mutex = mutex;  
        }  
  
        public int size() {  
            synchronized (mutex) {return m.size();}  
        }  
        public boolean isEmpty() {  
            synchronized (mutex) {return m.isEmpty();}  
        }  
        public boolean containsKey(Object key) {  
            synchronized (mutex) {return m.containsKey(key);}  
        }  
        public boolean containsValue(Object value) {  
            synchronized (mutex) {return m.containsValue(value);}  
        }  
        public V get(Object key) {  
            synchronized (mutex) {return m.get(key);}  
        }  
  
        public V put(K key, V value) {  
            synchronized (mutex) {return m.put(key, value);}  
        }  
        public V remove(Object key) {  
            synchronized (mutex) {return m.remove(key);}  
        }  
        public void putAll(Map extends K, ? extends V> map) {  
            synchronized (mutex) {m.putAll(map);}  
        }  
        public void clear() {  
            synchronized (mutex) {m.clear();}  
        }  
  
        private transient Set keySet = null;  
        private transient Set> entrySet = null;  
        private transient Collection values = null;  
  
        public Set keySet() {  
            synchronized (mutex) {  
                if (keySet==null)  
                    keySet = new SynchronizedSet<>(m.keySet(), mutex);  
                return keySet;  
            }  
        }  
  
        public Set> entrySet() {  
            synchronized (mutex) {  
                if (entrySet==null)  
                    entrySet = new SynchronizedSet<>(m.entrySet(), mutex);  
                return entrySet;  
            }  
        }  
  
        public Collection values() {  
            synchronized (mutex) {  
                if (values==null)  
                    values = new SynchronizedCollection<>(m.values(), mutex);  
                return values;  
            }  
        }  
  
        public boolean equals(Object o) {  
            if (this == o)  
                return true;  
            synchronized (mutex) {return m.equals(o);}  
        }  
        public int hashCode() {  
            synchronized (mutex) {return m.hashCode();}  
        }  
        public String toString() {  
            synchronized (mutex) {return m.toString();}  
        }  
        private void writeObject(ObjectOutputStream s) throws IOException {  
            synchronized (mutex) {s.defaultWriteObject();}  
        }  
}  

我们可以看到,线程同步的Map,其中的所有操作都是加了synchronized (mutex)约束的,保证了每一个操作都是互斥的、线程安全的。

  • 并发库提供的同步集合

同样,java5的并发库中也提供了相应的同步集合:

Collection 实现:

ConcurrentHashMap

ConcurrentSkipListMap

ConcurrentSkipListSet

CopyOnWriteArrayList

CopyOnWriteArraySet

当期望许多线程访问一个给定collection时,ConcurrentHashMap通常优于同步的HashMap,ConcurrentSkipListMap通常优于同步的TreeMap。当期望的读数和遍历远远大于列表的更新数时,CopyOnWriteArrayList优于同步的ArrayList。

  • 实例

首先写一个User类,首先了Cloneable接口,并改写了equeals、hashCode、toString和clone方法:

package cn.edu.hpu.test;  
  
public class User implements Cloneable{  
    private String name;  
    private int age;  
      
    public User(String name, int age) {  
        super();  
        this.name = name;  
        this.age = age;  
    }  
      
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public int getAge() {  
        return age;  
    }  
    public void setAge(int age) {  
        this.age = age;  
    }  
      
    @Override  
    public boolean equals(Object obj) {  
        if(this == obj){  
            return true;  
        }  
        if(!(obj instanceof User)){  
            return false;  
        }  
        User user=(User)obj;  
        if(this.name.equals(user.name)  
                &&this.age==user.age){  
            return true;  
        }else{  
            return false;  
        }  
    }  
    @Override  
    public int hashCode(){  
        return name.hashCode()+age;  
    }  
      
    @Override  
    public String toString(){  
        return "{name:"+name+",age"+age+"}";  
    }  
      
    @Override  
    public Object clone(){  
        Object object = null;  
        try {  
            object = super.clone();  
        } catch (CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return object;  
    }  
}  

然后一个测试类:

package cn.edu.hpu.test;  
  
import java.util.ArrayList;  
import java.util.Collection;  
import java.util.Iterator;  
  
public class ConcurrentCollectionTestDemo {  
    public static void main(String[] args) {  
        Collection users=new ArrayList();  
        users.add(new User("张三",25));  
        users.add(new User("李四",28));  
        users.add(new User("王五",32));  
        Iterator itrUser = users.iterator();  
        while(itrUser.hasNext()){  
            User user = (User)itrUser.next();  
            if("张三".equals(user.getName())){  
                users.remove(user);  
            }else{  
                System.out.println(user);  
            }  
        }  
    }  
}  

首先向user的List集合中装入三个用户,然后从集合中除去名字叫“张三”的用户信息。

运行结果:

发现程序报错了,内容为“并发的修改异常”。我们把“张三”改成“王五”,结果:

我们把“张三”改成“李四”,结果:

分析:当取走倒数第二个元素时,程序没有报错,除此之外,取走任何其他元素时
都会报错。

我们看一下ArrayList的Itr源码
((ArrayList.java:859)和(ArrayList.java:831)行都在这个类中出的错):

private class Itr implements Iterator {  
       int cursor;       // index of next element to return  
       int lastRet = -1; // index of last element returned; -1 if no such  
       //modCount初始化为0,在修改数据的时候(不论增删改)都会加1  
       int expectedModCount = modCount;  
  
       public boolean hasNext() {  
           return cursor != size;  
       }  
  
       @SuppressWarnings("unchecked")  
       public E next() {  
           checkForComodification();  
           int i = cursor;  
           if (i >= size)  
               throw new NoSuchElementException();  
           Object[] elementData = ArrayList.this.elementData;  
           if (i >= elementData.length)  
               throw new ConcurrentModificationException();  
           cursor = i + 1;  
           return (E) elementData[lastRet = i];  
       }  
  
       public void remove() {  
           if (lastRet < 0)  
               throw new IllegalStateException();  
           checkForComodification();  
  
           try {  
               ArrayList.this.remove(lastRet);  
               cursor = lastRet;  
               lastRet = -1;  
               expectedModCount = modCount;  
           } catch (IndexOutOfBoundsException ex) {  
               throw new ConcurrentModificationException();  
           }  
       }  
  
       final void checkForComodification() {  
           if (modCount != expectedModCount)  
               throw new ConcurrentModificationException();  
       }  
}  

原因:可以看到上面源码中,有int expectedModCount = modCount;这一句,其中modCount初始化为0,在修改数据的时候(不论增删改)都会加1。然后在每次做遍历next()和移出remove()操作的时候,都会先检查目前的modCount与expectedModCount的值是否相等,如果相等了,才会继续进行,如果不相等(即有可能与之前已经不是一次操作,中间有其它操作了),就会抛出ConcurrentModificationException的异常。

一开始插入三个数据的时候,modCount与expectedModCount都是3,然后遍历的时候,出现了一次remove操作,导致modCount++,然后modCount与expectedModCount值不再相等,那么遍历下一个的时候checkForComodification()方法就会抛出异常。

为什么删除倒数第二个时,没有报告异常呢?看源码中的hasnext方法:

public boolean hasnext(){
    return cursor != size;//cursor是遍历器的游标,也是从0开始,一次操作就加1
}

这是因为移出第二个的时候,cursor的值为2(从0开始,也就是有三次操作,分别是张三的遍历、李四的遍历和李四的移出),而size也为2(从0开始),所以while循环的hasnext()返回了false,导致next不会被调用。next不被调用,就不会进行checkForComodification()方法检查,进而就不会抛出异常。

当删除最后一个,就导致与cusor>size()了,所以,还会有next()方法的调用,故也出了问题。

用CopyOnWriteArrayList测试一下,就没有这个问题了:

package cn.edu.hpu.test;  
  
import java.util.Collection;  
import java.util.Iterator;  
import java.util.concurrent.CopyOnWriteArrayList;  
  
public class ConcurrentCollectionTestDemo {  
    public static void main(String[] args) {  
        Collection users=new CopyOnWriteArrayList();  
        users.add(new User("张三",25));  
        users.add(new User("李四",28));  
        users.add(new User("王五",32));  
        Iterator itrUser = users.iterator();  
        while(itrUser.hasNext()){  
            User user = (User)itrUser.next();  
            if("张三".equals(user.getName())){  
                users.remove(user);  
            }else{  
                System.out.println(user);  
            }  
        }  
    }  
}  

结果:

看一下CopyWriteArrayList的reomve的源码:

public E remove(int index) {  
        final ReentrantLock lock = this.lock;  
        lock.lock();  
        try {  
            Object[] elements = getArray();  
            int len = elements.length;  
            E oldValue = get(elements, index);  
            int numMoved = len - index - 1;  
            if (numMoved == 0)  
                setArray(Arrays.copyOf(elements, len - 1));  
            else {  
                Object[] newElements = new Object[len - 1];  
                System.arraycopy(elements, 0, newElements, 0, index);  
                System.arraycopy(elements, index + 1, newElements, index,  
                                 numMoved);  
                setArray(newElements);  
            }  
            return oldValue;  
        } finally {  
            lock.unlock();  
        }  
    }  

可以看到,在进行remove操作的时候,前后都加了线程锁,等操作完成之后才进行其他操作,所以不需要使用“游标”来保证数据的一致性,也不会发生动态删除时抛错的情况了。

这个就适用于多个线程访问或者在遍历的过程中动态操作集合的情景。


——————————————————————————————————————————————————————————————————————————————————

在学习过程如果有任何疑问,请来极乐网(http://www.dreawer.com)提问,或者扫描下方二维码,关注极乐官方微信,在平台下方留言~