线程安全的集合类

原来的集合类,大部分都是线程不安全的.

Vector,Stack,HashTable,是线程安全的(不建议用),其它的集合类不是线程安全的.

多线程使用ArrayList

1.自己使用同步机制(Synchronized或者ReentrantLock),前面已经做过许多讨论了,这里不再展开.

2.Collections.synchronizedList(new ArrayList); 

相当于给ArrayList套壳->得到了新的对象,新对象里面的方法都是加锁的.

synchronizedList是标准库提供的一个基于synchronized进行线程同步的List.synchronizedList的关键操作上都带有synchronized.

3.使用CopyOnWriteArrayList.

CopyOnWrite容器即写时复制的容器,这个主要用于解决多个线程修改一个数据的问题.

当我们往一个容器添加元素的时候,不直接往当前容器里添加,而是先将当前容器进行Copy,复制出一个新的容器,然后向新的容器里添加元素,

添加完元素之后,再将原容器的引用指向新容器.

一旦有线程修改值时,把顺序表复制一份,修改这个顺序表的内容,并修改引用的指向(这个操作是原子的)

这样写的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素.

所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器.

比如服务器在加载配置文件时,就需要把配置文件解析出来放到内存数据结构.

优点:在读多写少的场景下,性能很高,不需要加锁竞争.

缺点:1.占用内存较多(因此修改不能太频繁). 2.新写的数据不能被第一时间读取到.

多线程环境使用队列

1.ArrayBlockingQueue  (基于数组实现的阻塞队列)

2.LinkedBlockingQueue (基于链表实现的阻塞队列)

3.PriorityBlockingQueue (基于堆实现的带优先级的阻塞队列)

4.TransferQueue (最多只包含一个元素的阻塞队列)

多线程环境使用哈希表

HashMap本身是不安全的.

在多线程环境下可以使用: HashTable, ConcurrentHashMap.

HashTable

只是简单的把关键方法加上了synchronized关键字.

public synchronized V put(K key, V value) {

public synchronized V get(Object key) {

这相当于直接针对HashTable对象加锁.

如果多线程访问同一个HashTable就会造成锁冲突.

size属性也通过synchronized来控制同步,也是比较慢的.

一旦触发扩容,就由该线程完成整个扩容过程,这个过程就会涉及到大量的元素拷贝,效率非常低.

线程安全的集合类_第1张图片

一个HashTable只有一把锁.两个线程访问HashTable中的任意数据都会出现锁竞争. 

我们认为这样的类仍然不好用.我们希望:如果修改两个不同链表上的元素,不涉及线程安全的问题(因为修改的是不同变量).但如果在同一个链表上修改,涉及线程安全问题.操作引用时,就可能涉及到操作同一引用.因此我们在这里引入ConcurrentHashMap.

ConcurrentHashMap

相比于HashTable做出了一系列的改进和优化.它就解决了HashTable出现的部分问题:即针对同一个链表操作再加锁,针对不同链表操作,不必加锁(不要产生锁冲突)->缩小了锁的粒度

下面来看一下详细介绍:

读操作没有加锁(但是使用volatile保证从内存中读取结果),只对写操作进行加锁.加锁的方式仍然是使用synchronized,但是不是锁整个对象,而是"锁桶"(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率.

充分利用了CAS的特性.比如size属性通过CAS来更新.避免出现重量级锁的情况.

优化了扩容方式:化整为零

    (1)发现需要扩容的线程,只需要创建一个新的数组,同时搬几个元素过去.

    (2)扩容期间,新老数组同时存在

    (3)后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程.每个操作负责搬运一小部分元素

    (4)搬完最后一个元素再把老数组删掉

    (5)这个期间,插入只往新数组中加.

    (6)这个期间,查找需要同时查新数组和老数组.

这种设定,不会产生更多的空间代价.因为java中任何一个对象都可以直接作为锁对象.本身,在哈希表中,就得有数组,数组元素都是存在的(每个链表的头节点),用其做对象加锁即可.

线程安全的集合类_第2张图片 

ConcurrentHashMap每个哈希桶上都有一把锁.只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突.

相关面试题

1.ConcurrentHashMap的读是否要加锁,为什么?

 读操作没有加锁.目的是为了进一步降低锁冲突的概率.为了保证读到刚修改的数据,搭配了volatile关键字.

2.介绍下ConcurrentHashMap的锁分段技术?

这个是Java1.7所采取的技术.Java1.8中已经不再使用了.简单的说就是把若干个哈希桶分成一个"段"(Segment),针对每个段分别加锁.

目的也是为了降低锁冲突的概率.当两个线程访问的数据恰好在同一个段上时,才会触发锁竞争

3.ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头节点对象作为锁对象).

将原来的数组 + 链表的实现方式改进成 数组 + 链表 /红黑树的方式.当链表较长的时候(大于等于8个元素)就转换成红黑树. 

4.HashMap和HashTable,ConcurrentHashMap之间的区别?

HashMap: 线程不安全.key允许为null

HashTable:线程安全.使用synchronized锁HashTable对象,效率较低.key不允许设置为null.

ConcurrentHashMap: 线程安全.使用synchronized锁每个链表的头节点,锁冲突概率较低,充分利用CAS机制,优化了扩容方式.key不允许为null. 

你可能感兴趣的:(java,jvm,开发语言)