Java中的集合类与线程安全的讨论

时间:2024-11-16 11:58:21

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进行扩容时,需要将原有的数据复制到新的哈希表中,当数据量较大时,一次复制所有的数据所需要的时间和空间开销都非常大,效率低下,于是可以分批次复制,一次只复制所有数据的百分之几,之后的每一次操作都会触发复制操作,几次之后就能完成数据的复制,这样的效率更高。