Java中的集合类与线程安全的讨论
1.ArrayList
ArrayList是线程不安全的,可以在单线程中使用,在多线程中可以根据代码选择合适的时机进行加锁,实现线程安全的操作,但对代码能力要求较高。
2.Collections.synchronizedList(new ArrayList)
返回的List中的关键操作上都带有synchronized,但由于加锁较多,使得锁冲突发生的效率较高,代码阻塞的概率较大,对性能会有一定的影响。
3.CopyOnWriteArrayList
CopyOnWrite即写时拷贝,当我们在修改变量的同时进行变量的读取时,使用写时拷贝会降低线程安全问题;
现有如下需求,将arrayList1中的值进行修改,
当我们进行简单的修改操作并同时进行数据的读取,这时就会出现下面的结果:100, 200, 3,4,5,这样的结果显然不是我们向读到的,这属于半修改状态的数据,于是可以使用写时拷贝。
现有一个引用指向arrayList1,当我们进行数据的修改时,就会创建出另一个新的顺序表arrayList2,在将新的数据写入到arrayList2中时,若需要进行读数据操作,这时就会只读取arrayList1中的数据,这样读到的数据就会全部都是旧的数据,这样就不会出现读取到半新半旧的数据,当数据全部写入完后,就会将引用指向arrayList2,这样就完成了多线程下的数据修改操作,并且不会引发线程安全问题。
虽然数据的复制与更新不是原子的,但由于在修改完成之前保存了旧的数据,就不会影响数据的读取;当数据修改完成后,引用的赋值时原子的,,也不会引发线程安全问题。
但上述方案也会有明显的问题:
1)当数据量很大时,拷贝的时间就会非常长,效率低下;
2)当有多个线程进行数据的修改操作时,也会引发线程安全问题,因为在修改完成后引用指向哪个修改完的顺序表是不确定的,这样就会导致修改的数据可能达不到自己想要的状态,而上述的写时拷贝操作也仅仅是两个线程之间的操作,一个读一个修改。
4.阻塞队列
ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue均是线程安全的。
5.HashMap
HashMap在不涉及线程安全的操作时效率非常高,但无法避免线程安全问题。
6.Hashtable
Hashtable是线程安全的,但由于他的每一个public方法均使用synchronized修饰,并且只有一把锁,导致容易发生锁冲突,导致线程阻塞的概率大,效率较低。
7.ConcurrentHasshMap
这个是线程安全的,并且针对桶级别进行加锁,效率更高。
对于一般的哈希表,当发生哈希冲突时,有两种解决方案:
1)线性探测,但是这种方法效率较低,不推荐使用;
2)链表,将会引发哈希冲突的数据放到链表上,效率更高,推荐使用,如下图:
对于HashMap,在多线程操作中当我们涉及到以下操作时会引发线程安全问题:
1)修改一个链表上的同一个值;
2)修改同一个链表上的两个元素,如在一个数后插入两个元素,一个线程插入一个;
3)修改HashMap的size等......
于是我们可以进行加锁操作,但不能向Hashtable一样,只是用一个锁并且将所有的public方法加上synchronized,我们可以针对每一个链表进行加锁操作,每一个链表使用不同的锁对象,这样就能有效地减少所冲突地概率,并且发生上述地1、2情况时会避免线程安全问题,进行线程阻塞;
但由于链表的个数可能会很多,导致锁对象地开销较大,在java中,锁对象可以是任意对象,于是可以使用每一个链表的头节点作为锁对象,这样就会减少锁对象的空间消耗。
但对于情况3,可以对size单独加锁,但这种解决方式不是最好的,我们可以使用原子类来代替size,即创建AtomicInteger对象来代表size,代码如下:
AtomicInteger size = new AtomicInteger(0); //0为初始值
size.getAndIncrement(); //相当于size++
size.incrementAndGet(); //相当于++size
szie.addAndGet(n); //相当于size+=n
上述的三个针对size的操作均是原子的,是线程安全的;
对ConcurrentHasshMap进行扩容时,需要将原有的数据复制到新的哈希表中,当数据量较大时,一次复制所有的数据所需要的时间和空间开销都非常大,效率低下,于是可以分批次复制,一次只复制所有数据的百分之几,之后的每一次操作都会触发复制操作,几次之后就能完成数据的复制,这样的效率更高。