线程安全
多线程并发一定会带来线程安全问题,其中,保证线程安全的知识体系如下所示:
1 线程安全控制总述
线程安全控制有三个级别:
- JVM级别:大多数CPU对并发对某一硬件级别提供支持,通常以compare-and-swap(CAS)指令形式。CAS是一种低级别的、细粒度的技术,允许多个线程更新一个内存位置,同时能够检测其他线程的冲突并进行恢复,是许多高性能并发算法的基础。
- 低级实用程序类:锁定和原子类(ReentrantLock和synchronized)。
- 高级使用程序类:这些类实现并发构建块,包括线程池、线程安全集合类等。
常见的线程安全操作(按优先级):
- 栈封闭
- ThreaLocal
- 同步
2 并发集合类
原始集合框架Collection包含List、Map、Set这三个常用的接口,但是这三个接口存在线程安全问题,因此才引入java.util.concurrent包提供一组安全可靠的并发构建块(CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet)。
2.1 hashmap & hashtable & ConcurrentHashMap
在多线程环境下,使用HashMap是不安全的,为了保证HashMap的线程安全性,最初通过使用内置同步锁构造Hashtable来实现线程安全,但是由于使用全局的同步锁,当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法可能会进入阻塞或轮询状态,因此性能较低。
Hashtable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁。假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。另外对于读操作,concurrentHashMap完全是非阻塞的,允许多线程并发读取,相比于hashtable性能更高。
2.2 CopyOnWriteArrayList
原始集合类中的迭代器都是fail-fast迭代器,这意味着它们假设在集合内容上进行迭代时,集合不会更改它的内容,但是在迭代过程中不更改集合的要求对并发应用程序而言是不可能的,一旦fail-fast迭代器检测到在迭代过程中进行了更改操作,那么就会抛出ConcurrentModificationException,这是不可控异常。
CopyOnWrite容器即写时复制的容器。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器Copy,复制出一个新容器,然后往新容器里添加元素,添加完后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWriteArrayList是线程安全的List类,是写时复制容器的一种。但CopyOnWrite容器由于存在副本存储,因此最终会带来内存占用问题,特别是当数据量比较大时,容易引起频繁的Full GC,其次,CopeOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性,适用于读多写少的场景。
2.3 CopyOnWriteArraySet
CopyOnWriteArraySet是线程安全的集合类,采用CopyOnWriteArrayList实现,只是不允许元素重复。
3 线程池管理
Executor框架是java.util.concurrent包中提供的线程池实现,用于管理Runnable、Callable任务执行。提供了四种常用模型:
- newFixedThreadPool创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数则将提交的任务存入到线程池队列中。
- newCachedThreadPool创建一个可缓存的动态线程池。默认60s检测线程状态,若未被使用则自动被回收。
- newSingleThreadExecutor创建一个单线程化的Executor来执行任务,最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。
- newScheduleThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行。