多线程进阶——线程安全的集合类
目录
一、多线程环境使用ArrayList
(一)自行加锁
(二)Collections.synchronizedList(new ArrayList);
(三)使用 CopyOnWriteArrayList
二、多线程环境使用哈希表
(一)Hashtable
(二)ConcurrentHashMap
原来的集合类,大部分都不是线程安全的。
Vector, Stack, HashTable, 是线程安全的(不建议用),其他集合类不是线程安全的。
一、多线程环境使用ArrayList
(一)自行加锁
具体代码具体分析,要把那些代码打包到一起,成为一个“原子”操作。
(二)Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List. synchronizedList
的关键操作上都带有 synchronized,从而实现原子类。
这是不推荐的,加锁有代价。
(三)使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器,使用 CopyOnWriteArrayList就不用去加锁,运用到了“写时拷贝”的思想方法。
它的工作原理是:
- 当我们往一个容器中添加元素时,不直接往这个容器里面添加,而是先将当前容器进行copy,复制出一个新的容器,然后往新的容器里面添加元素。
- 添加完元素之后,再讲原容器的引用指向新的容器。
在多线程读取的时候,一旦某个线程进行写操作,比如修改 1 -> 100
复制过程中,如果其他线程在读,就直接读取旧版本的数据
虽然复制过程不是原子的(消耗一定的时间)
由于提供了旧版本的数据,不影响其他线程读取
新版本数组复制完毕之后,直接进行引用的修改。
引用的赋值是“原子” 的,确保读取过程中,要么读到的是旧版数据,要么读到的是新版数据,不会读到“修改一半”的数据。
二、多线程环境使用哈希表
HashMap本身不是线程安全的。
在多线程环境在使用哈希表可以使用:
- Hashtable
- ConcurrentHashMap
(一)Hashtable
Hashtable类中的各种public方法都加上了synchronized,这也相当于直接针对Hashtable对象本身加锁。
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步,也是比较慢的.
- 一旦触发扩容,就由该线程完成整个扩容过程。这个过程会涉及到大量的元素拷贝,效率会非常低.
一个Hashtable只有一把锁,两个线程访问Hashtable中的任意数据都会出现锁竞争。
(二)ConcurrentHashMap
ConcurrHashMap根据Hashtable做出了一系列的改进和优化。
- 读操作没有加锁,使用了volatile确保从内存中读取数据,只针对写操作进行加锁,但不是锁整个对象,而是“锁桶”:用每个链表的头结点作为锁对象,大大降低了锁冲突的概率。
- 充分利用 CAS 特性。比如 size 属性通过 CAS 来更新。避免出现重量级锁的情况.
- 优化了扩容方式:化整为零:
化整为零:
发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去.
扩容期间,新老数组同时存在.
后续每个来操作 ConcurrentHashMap 的线程,都会参与搬家的过程。每个操作负责搬运一小部分元素.
搬完最后一个元素再把老数组删掉.
这个期间,插入只往新数组加.
这个期间。查找需要同时查新数组和老数组
ConcurrentHashMap每个哈希桶都有一把锁,只有两个线程同时访问同一个哈希桶上的数据才会出现锁竞争。