1.多线程环境使用ArrayList
原来的集合类大部分都不是线程安全的,那在多线程情况下该如何使用呢?
- 自行加锁。分析清楚,要把哪些代码打包到一起,成为一个原子操作。
-
collections.synchronizedList(new AtrrayList) 这其实就是一个套壳,返回的List各个关键方法都是带有synchronized,不建议使用
- 使用CopyOnWriteArrayList,不去加锁就不会产生阻塞。但是这个方法也有明显的缺点,数组特别大,非常低效;如果多个线程同时修改也会容易出问题。在服务器进行重新加载配置的时候比较适合使用这种方法。什么是配置,配置就是我们打开一个程序的时候设置的画质、声音。但是此时服务器正在运行,正常来说修改的配置文件不能立即生效需要重启一下服务器。很多服务器提供配置重加载(reload),配置就是被读取到服务器的内存中,以数组、哈希存储,服务器代码中其他的逻辑就会读取到这些数组、哈希的值。此时程序员手动修改配置文件之后,手动触发reload功能,服务器就会创建新的数组、哈希加载新的配置。加载完毕之后使用新配置代替旧配置。
利用的是写时拷贝思想。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行copy,复制出一个新的容器,复制过程中如果有其他线程在读就直接读取旧版本的数据,虽然复制过程不是原子的但是提供的是旧版本不影响其他线程读取,然后在新的容器添加元素同时把引用由旧数组指向新数组。
2.多线程环境使用哈希表
HashMap本身不是线程安全的,在多线程环境下可以使用哈希如下:
1.Hashtable(线程安全的给各种public方法都加synchronized,但是不推荐使用)
2.ConcurrentHashMap(效率更高:按照桶级别进行加锁,而不是给整个哈希加一个全局锁,有效降低锁冲突的概率)优势如下:
- 读操作没有加锁,只对写操作进行加锁,加锁的方式仍然是用synchronized,但是不是整个锁对象而是锁桶(用每个链表的头节点作为锁对象),大大降低了锁冲突的概率。
我们先来介绍一下普通的hash,hash表的构造是通过取余的方式来获取数组下标,但是这样会出现锁冲突。为了解决这个冲突有两种方法,一是线性探测,而是链表。线性探测不太靠谱,在实际开发中一般采用链表的形式。如果链表太长了就需要扩容比如使用红黑树。在普通的hash链表如果有两个线程访问任意两个不同的元素(同一个链表中)都会产生锁竞争。在这种情况下,ConcurrentHashMap就在每个链表的头结点进行加锁。在实际开发中,用到的Hash表很可能比较大(桶有很多个),即使多线程访问上述的Hash表,同一时刻两个线程恰好访问同一个链表的可能性概率比较低。
- 充分利用CAS特性原子类,比如size通过CAS来更新,避免出现重量级锁的情况,比如两个线程同时修改size的情况
- 优化了扩容方式:化整为零,确保每个操作的加锁时间不要太长。扩容操作意味着需要更大的数组,把旧哈希中的所有元素搬运到新的哈希中(元素很长,耗时很长),假设某个插入操作,触发了扩容 进行搬运,搬运过程中就需要进行更长时间加锁了。但是这里不采用一口气搬运所有的元素,而是把整个搬运拆成多次来完成,一旦触发了“扩容”不是通过一次put来完成的而是通过多次put、get来操作完成。
1.发现需要扩容的线程,只需创建一个新的数组,同时只搬几个元素过去
2.扩容期间,新老数组同时存在
3.后续每个来操作ConcurrenthHashMap的线程,都会参与搬家的过程,每个操作负责搬运一小部分元素
4.搬完最后一个元素再把老元素删除,这个期间插入只往新数组加,这个期间查抄需要同时查新数组和老数组。