java-同步集合Concurrent Collections应用

一、传统集合的缺陷

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

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

public V get(Object key){

    if(key == null){ return getForNullKey(); }

    int hash = hash(key.hashCode());

    for(Entry<k,v> 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;

}</k,v>

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

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

 

1

2

3

while(hasnext()){

   next(){cursor++;}

}

其中hasnext

 

1

2

3

4

5

6

hasnext(){

  if(cursor == count){

    return false;

  }

  return true;

}


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

1

2

3

remove(){

  count--;

}


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

传统方式下的Collection在迭代时,不允许对集合进行修改。使用Iterator对集合进行迭代时不能修改集合。

下面的代码在对集合修改时就会报异常.

public class CollectionModifyExceptionTest {

    public static void main(String[] args) {
        List<String> strs = new ArrayList<>();
        strs.add("aaa");
        strs.add("bbb");
        strs.add("ccc");
        Iterator iterator = strs.iterator();
        while(iterator.hasNext()){
            System.out.println(".....");
            String value = (String)iterator.next();
            if("aaa".equals(value)){
                strs.remove(value);
            }else{
                System.out.println(value);
            }
        }
    }

}

 

二、传统同步集合

要拿到一个传统同步集合很简单:,Collections.synchronizedMap(Map someMap);给它一个普通的Map,它就会返回一个线程安全的同步Map,线程同步的Map,其中的所有操作都是加了synchronized (mutex)约束的,保证了每一个操作都是互斥的、线程安全的。

源码如下:

private static class SynchronizedMap<k,v>
        implements Map<k,v>, Serializable {
        private static final long serialVersionUID = 1978198479659022715L;
 
        private final Map<k,v> m;     // Backing Map
        final Object      mutex;        // Object on which to synchronize
 
        SynchronizedMap(Map<k,v> m) {
            if (m==null)
                throw new NullPointerException();
            this.m = m;
            mutex = this;
        }
 
        SynchronizedMap(Map<k,v> 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<k> keySet = null;
        private transient Set<map.entry<k,v>> entrySet = null;
        private transient Collection<v> values = null;
 
        public Set<k> keySet() {
            synchronized (mutex) {
                if (keySet==null)
                    keySet = new SynchronizedSet<>(m.keySet(), mutex);
                return keySet;
            }
        }
 
        public Set<map.entry<k,v>> entrySet() {
            synchronized (mutex) {
                if (entrySet==null)
                    entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
                return entrySet;
            }
        }
 
        public Collection<v> 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();}
        }
}
</v></map.entry<k,v></k></v></map.entry<k,v></k></k,v></k,v></k,v></k,v></k,v>

三、并发库提供的同步集合

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

ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet,CopyOnWriteArrayList,CopyOnWriteArraySet

ConcurrentHashMap:

HashTable是HashMap的线程安全实现,但是HashTable使用synchronized来保证线程安全,这就会导致它的效率非常低下,因为当线程1使用put添加元素,线程2不但不能使用put添加元素,同时也不能使用get获取元素,竞争越激烈效率越低。

因此替代HashTable的ConcurrentHashMap就出现了,ConcurrentHashMap的优点在于容器里有多把锁,每一把锁用于锁容器其中一部分数据,当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率。它的原理是将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。有些方法需要跨段,如size()和containsValue(),他们可能需要锁定整个表而不仅是某个段,这需要按顺序锁定所有段,操作完毕后又按顺序释放所有段的锁。

CopyOnWriteArrayList:

Copy-On-Write是一种用于程序设计中的优化策略,基本思路是多个线程共享同一个列表,当某个线程想要修改这个列表的元素时会把列表中的元素Copy一份,然后进行修改,修改完后再讲新的元素设置给这个列表,是一种延时懒惰策略。好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加、移除任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。使用Copy-On-Write机制实现的并发容器有两个分别是:CopyOnWriteArrayList和CopyOnWriteArraySet。

下面来分析下CopyOnWriteArrayList的核心源码,首先看下add方法:

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

可以看到在添加的时候进行了加锁操作,否则多线程写的时候会Copy出N个副本出来。复制一份之后将新的元素设置到元素数组的len位置,然后再把最新的元素设置给该列表。

get方法:

    public E get(int index) {
        return get(getArray(), index);
    }

读不需要加锁,如果读的时候多个线程正在向容器内添加数据,还是会读到旧数据,因为写的时候不会锁住旧的元素数组。这种写时拷贝的原理优点是读写分离,并发场景下操作效率会提高,缺点是写操作时占用的内存空间翻了一倍,因此是以空间换时间。

使用同步集合实现迭代时对集合进行修改:

package cn.itcast.heima;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CollectionModifyExceptionTest {
	public static void main(String[] args) {
		Collection users = new CopyOnWriteArrayList();//在写的时候有一分拷贝
		users.add(new User("张三",28));	
		users.add(new User("李四",25));			
		users.add(new User("王五",31));	
		Iterator itrUsers = users.iterator();
		while(itrUsers.hasNext()){
			System.out.println("aaaa");
			User user = (User)itrUsers.next();
			if("李四".equals(user.getName())){
				users.remove(user);
				//itrUsers.remove();
			} else {
				System.out.println(user);				
			}
		}
	}
}	 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值