数据结构
1 队列
- 非阻塞队列:ConcurrentLinkedQueue(无界线程安全),采用CAS机制(compareAndSwapObject原子操作)。
- 阻塞队列:ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界)、DelayQueue、PriorityBlockingQueue,采用锁机制;使用 ReentrantLock 锁。
Set/HashSet
注重独一无二的性质,该体系集合可以知道某物是否已近存在于集合中,不会存储重复的元素,用于存储无序且值不能重复的对象。
TreeSet
- 也就是元素需要实现Comparable接口,覆盖compareTo 方法。这种方式也作为元素的自然排序,也可称为默认排序。
- 让容器自身具备比较性,自定义比较器
LinkedHashSet
会保存插入的顺序
List/ArraryList
ArrayList是一个数组实现的列表,由于数据是存入数组中的,所以它的特点也和数组一样,查询很快,但是中间部分的插入和删除很慢。
Vector
Vector就是ArrayList的线程安全版,它的方法前都加了synchronized锁,其他实现逻辑都相同。
如果对线程安全要求不高的话,可以选择ArrayList,毕竟synchronized也很耗性能。
LinkedList
故名思意就是链表,LinkedList还是一个双向链表。LinkedList继承于AbstractSequentialList,和ArrayList一个套路。内部维护了3个成员变量,一个是当前链表的头节点,一个是尾部节点,还有是链表长度。
ArrayList是由数组实现的,方便查找,返回数组下标对应的值即可,适用于多查找的场景
LinkedList由链表实现,插入和删除方便,适用于多次数据替换的场景
HashMap
HashMap
没有对多线程的场景下做任何的处理,不用说别的,就两个线程同时put
,然后冲突了,两者需要操作一个链表/红黑树,这肯定就会有错误发生,所以HashMap
是线程不安全的。
HashTable
HashTable
线程安全,但是它的线程安全是依赖将所有修改HashTable
的代码块都用synchronized
修饰。在HashTable
中,一个线程在put
,其余的线程在get
的时候就会被阻塞,无法并行。所以不推荐使用HashTable
,虽然它线程安全。
ConcurrentHashMap
jdk1.7之前ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
jdk1.8之后抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next
都用了 volatile 修饰,保证了可见性。
ConcurrentSkipListMap
ConcurrentSkipListMap 的key是有序的。ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。注意,调用ConcurrentSkipListMap的size时,由于多个线程可以同时对映射表进行操作,所以映射表需要遍历整个链表才能返回元素个数,这个操作是个O(log(n))的操作。
ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现,该算法在Michael & Scott算法上进行了一些修改。
解决并发时的安全问题也有两种策略:
CAS
实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
TLAB
如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来进行配置(jdk5及以后的版本默认是启用TLAB的)。