Java5线程并发库之同步集合

同步集合

接下来讲,java5中提供的同步集合。传统的集合在并发访问时是有问题的。比如HashSet,HashMap,ArrayList,像这样的类,如果是多个线程在操作,多个线程在往它里面取数据,放数据,会出问题的。它们是线程不安全的,会把它里面的数据搞得乱七八糟的。

ConcurrentHashMap

下面我们来看一个关于HashMap同步的问题:

Race Condition引起的性能问题

Race Condition(也叫做资源竞争),是多线程编程中比较头疼的问题。特别是Java多线程模型当中,经常会因为多个线程同时访问相同的共享数据,而造成数据的不一致性。为了解决这个问题,通常来说需要加上同步标志“synchronized”,来保证数据的串行访问。但是“synchronized”是个性能杀手,过多的使用会导致性能下降,特别是扩展性下降,使得你的系统不能使用多个CPU资源。  这是我们在性能测试中经常遇见的问题。

可是上个星期我却遇见了相反的情况:因为缺少同步标志也同样会使性能受影响。

那是一个ERP系统,运行在我们的T2000服务器(8核32线程)上。当500个并发用户的时候居然把所有的CPU都压得满满的(90%以上的忙碌)。这是很少有的现象,在我测试的所有项目中很少有扩展性这么好的系统能把T2000的32个线程都占满的。我狠狠的夸了他们的应用。话音没落,却发现测试结果很差,平均响应时间很长。不可能呀,所有的CPU都在干活,而且都在用户态(如果在系统态干太多的活就有问题了),结果怎么还会差呢。CPU都在干嘛呢?

通过工具发现(Dtrace for Java),我们发现很多的CPU都在做一件事情,那就是不停的执行一条Java语句(HashMap.get())。象是进入了死循环。我们进行了进一步试验,让并发用户数量为1,不停的运行10分钟,结果没有发现这种情况;接着我们让50个并发用户同时运行,但是只运行在一个CPU上(通过psrset),结果也没有出现死循环状态。只要并发用户数量超过10个,运行的CPU超过两个,不到2分钟就出现死循环。一旦死循环出现,大量CPU资源被白白浪费,性能自然很差。

通过上面的试验我们可以很肯定的判断,是由于并发控制不好,导致数据的不一致,引起的死循环。值得一提的是,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) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

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

我们建议客户使用ConcurrentHashMap或在使用HanshMap的时候加上同步标志,问题得到解决!

在以前没有并发库的时候,没有ConcurrentHashMap的时候,人们是怎么解决map的同步问题的呢?人们用的是这个方法Collections.synchronizedMap(null);参数是一个Map<K,V> m,作用是返回由指定map支持的同步(线程安全的)map。他得到的是一个new SynchronizedMap<>(m);这个SynchronizedMap对象对Map做了装饰,把所有的方法中的代码都用synchronized代码块包起来了。对象锁是this,当有一个线程调用了这个对象里面的任何方法,其它的线程就不可以再调用这个对象里面的任何方法了,因为锁已经被再执行的线程拿走了,其它线程只能阻塞,等待锁,拿到锁后再操作。你原来的map是线程不安全的,我返回的map是线程安全的。当然,当有了java5的并发库之后,我们不再建议使用这个工具方法,而是建议使用ConcurrentHashMap,因为它内部做了优化,减小了锁的粒度,提高了多线程并发访问的运行效率。它将hashMap分为了若干个Segment<K, V>段,我们知道,hashMap内部其实就是一个数组Entry<K,V>[] table,那么,我们拿一个数组实现hashMap,对hashMap对象加锁的时候,就会对那个大数组加锁,每次只有一个线程可以进去操作这个数组,如果我用多个数组去维持一个hashmap,每一次进去,我们只对这个数组的一部分进行加锁,这样就减小了锁的粒度。ConcurrentHashMap会维护若干个Segment,每一个Segment都可以理解成是一个小的hashMap,它里面就会获得hashMap的Entry(表),做同步操作的时候,是先定位到这个Segment,然后锁定这一个Segment,执行put,如果有多个线程要来操作,比如说有两个线程,那么这两个线程分别定位到Segment1和Segment2,那么,这时候它们之间的操作是互不影响的。它们可以同时做这个操作,而不需要进行等待,这个竞争相对来说也就小了很多。

这里讲个题外话,讨论一下HashMap和HashSet的关系,HashSet是单列的,HashMap是双列的,有Key和value.在底层一点,HashMap和HashSet之间的关系,其实HashSet内部的实现用的就是一个HashMap。只是它只是用了HashMap的Key,就够了,我这个value部分从来不考虑,我从来不使用它。就把HashMap完全可以当作HashSet用。Key不能重复,完全符合HashSet的要求。所以说,HashSet内部使用的是HashMap.

Java5以后,提供了各种集合相关的同步类。如果你要用map,它提供了并发的HashMap,名叫ConcurrentHashMap.

还提供了ConcurrentSkipListMap, ConcurrentSkipListSet, CopyOnWriteArrayList, CopyOnWriteArraySet.等同步集合。 下面我们来介绍一下这几个类:

ConcurrentSkipListMap

ConcurrentSkipListMap:它实现了SortedMap接口,它是一个排序的map,就是说,往这个map里面存的东西是有顺序的,排序要看比较规则。排序一定要传一个比较器进去的,告诉它排序的比较规则是什么。该map可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法。这个就类似于TreeMap. 但是是线程安全的。

ConcurrentSkipListSet

ConcurrentSkipListSet:set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法。这个就类实于TreeSet,但是是线程安全的。

CopyonWriteArrayListCopyonWriteArraySet

接下来CopyOnWriteArrayList, CopyOnWriteArraySet这两个要好好介绍一下。

再说这两个类之前,我们先看一下另外一个问题,就是说,我们说再java5以前的某些集合是线程不安全的,除了线程不安全,他还有另外一个隐患,在集合迭代的过程中不可以执行remove操作,在读的过程中不能进行写操作。当我们执行集合的迭代器的next方法时,会检查一个变量modCount,我们把它当作一个版本号,它会去检查这个版本号是否等于expectedModCount预期的版本号的值,如果不相等会抛出ConcurrentModificationException的异常,当我们刚获得迭代器对象的时候,它们二者是相等的,但是当我们调用了集合的增加,删除,删除所有等修改集合数据的方法之后,modCount的值就会++,因此它的值就改变了。modCount的值等于集合的操作次数,集合操作了几次,这个modelCount就等于几。也就是说,在迭代集合的过程中,不能对集合进行修改。修改的话,后果很严重。当然,如果我们调用的时迭代器的remove方法是不会有问题的,程序正常执行。

public class CollectionModifyExceptionTest {

    public static void main(String[] args) {

        Collection users = new ArrayList();

        users.add(new User("张三", 28));

        users.add(new User("李四", 25));

        users.add(new User("王五", 31));

        Iterator itrUsers = users.iterator();//当得到迭代器的时候,这时modelCount等于3

        while (itrUsers.hasNext()) {

            System.out.println("aaaa");

            User user = (User) itrUsers.next();//当循环回来检查时modelCount与预期的值不符合,抛异常

            if ("张三".equals(user.getName())) {

                users.remove(user);//执行了这次remove操作之后,此时modelCount等于4

                // itrUsers.remove();//调用迭代器的remove方法时没有问题的,不会抛异常,正常执行

            } else {

                System.out.println(user);

            }

        }

    }

}

public class User implements Cloneable {

    private String name;

    private int age;

    public User(String name, int age) {

        this.name = name;

        this.age = age;

    }

    public boolean equals(Object obj) {

        if (this == obj) {

            return true;

        }

        if (!(obj instanceof User)) {

            return false;

        }

        User user = (User) obj;

        // if(this.name==user.name && this.age==user.age)

        if (this.name.equals(user.name) && this.age == user.age) {

            return true;

        } else {

            return false;

        }

    }

    public int hashCode() {

        return name.hashCode() + age;

    }

    public String toString() {

        return "{name:'" + name + "',age:" + age + "}";

    }

    public Object clone() {

        Object object = null;

        try {

            object = super.clone();

        } catch (CloneNotSupportedException e) {

        }

        return object;

    }

    public void setAge(int age) {

        this.age = age;

    }

    public String getName() {

        return name;

    }

}

接下来我们要解决集合在迭代的时候不能修改的问题,怎么解决呢?

这里我们把ArrayList对象换成CopyOnWriteArrayList,就可以了。它在我们对集合进行写操作的时候会保留一份拷贝,在remove操作时,它会创建一个新的数组对象,这个新数组对象的引用赋给了集合的数组成员变量,这个新数组中移除了要移除的元素。也就是说,其实它并不担心一边迭代,一边执行写操作。它迭代的是一个不变的对象,而写操作会保留到另一个对象。然后它又是支持并发的。它允许迭代的时候修改集合,并且是线程安全的。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。

public class CollectionModifyExceptionTest {

    public static void main(String[] args) {

        Collection users = new CopyOnWriteArrayList();

        // new ArrayList();

        users.add(new User("张三", 28));

        users.add(new User("李四", 25));

        users.add(new User("王五", 31));

        Iterator itrUsers = users.iterator();//当得到迭代器的时候,这时modelCount等于3

        while (itrUsers.hasNext()) {

            System.out.println("aaaa");

            User user = (User) itrUsers.next();//当循环回来检查时modelCount与预期的值不符合,抛异常

            if ("张三".equals(user.getName())) {

                users.remove(user);//执行了这次remove操作之后,此时modelCount等于4

                // itrUsers.remove();//这里就不能调用迭代器的remove方法了,否则会抛出异常

            } else {

                System.out.println(user);

            }

        }

    }

}

猜你喜欢

转载自my.oschina.net/u/3512041/blog/1822504