Java中提供的大部分常见的集合类在高并发环境下使用都会产生线程安全问题。究其根本:都是由于并发时线程执行的顺序导致线程安全问题的产生
List接口:ArrayList、LinkedList
Set接口:HashSet、TreeSet、LinkedHashSet
Map接口:HashMap、TreeMap、LinkedHashMap
Queue接口:ArrayDeque 、LinkedList、PriorityQueue
接下来会说明一些Java并发包自带的线程安全类 以及通过自定义去实现高效线程安全类的方法
List接口
常见的实现了List接口的类有:ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList、SynchronizedList。
- 其中ArrayList和LinkedList会有线程安全问题
- Vector和Stack都是在add()、get()、remove()方法上都加了synchronized保证线程安全,锁的是整个对象,高并发场景下性能较差。
剩下的两个都保证了线程安全问题,适用于不同的场景
- CopyOnWriteArrayList
学习了写时复制的思想,读操作时读取当前数组,写操作时会复制出一个新的数组,写操作完成之后替换旧数组,读操作可能看到旧数据,但保证最终一致。
读操作无锁,写操作加锁syn,适合在读多写少的场景使用
public void add(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
}
}
public E get(int index) {
return elementAt(getArray(), index);
}
- SynchronizedList
在读操作和写操作时都会上锁,锁的是实例本身this,所有方法共用同一把锁,写竞争激烈时性能差
适用于低频写 + 简单操作。其和Vector差不多,只不过其可以通过构造方法包装任意List的实现
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
List<String> synchronizedList = Collections.synchronizedList(new LinkedList<>());
在有线程安全问题的场景时更推荐使用CopyOnWriteArrayList,性能更高
Set接口
常见实现类有: HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet、ConcurrentSkipListSet
其中HashSet、LinkedHashSet、TreeSet都会有线程安全问题
HashSet底层是HashMap的key、LinkedHashSet是在HashSet基础上添加链表结构,记录插入顺序、TreeSet基于TreeMap的key,按自然顺序或自定义比较器排序
- CopyOnWriteArraySet
线程安全、基于CopyOnWriteArrayList实现。
将所有操作委托于CopyOnWriteArrayList,通过CopyOnWriteArrayList的addIfAbsent(方法保证唯一性
既然基于CopyOnWriteArrayList实现,也会有写时复制,适合用于多线程环境中读操作频繁,写操作较少的情况
- ConcurrentSkipListSet
线程安全,基于跳表实现。
介绍一下跳表:
- 多层链表结构:每一层都是有序链表,高层链表是低层链表的“快速通道”
- 概率平衡:通过随机算法决定节点的高度,避免像红黑树那样严格的平衡调整
- 查找效率:平均时间复杂度 O(log n),最坏情况 O(n)(但概率极低)
使用 CAS和 volatile 变量 实现无锁并发:
- 节点插入/删除:通过 CAS 原子操作更新指针,避免全局锁。
- 内存可见性:所有节点指针用
volatile
修饰,保证多线程间的可见性。 - 使用putIfAbsent()保证唯一性
和Redis中的Zset一样,都是有序集合
// ConcurrentSkipListMap 的节点定义
static final class Node<K,V> {
final K key;
volatile Object value;
volatile Node<K,V> next; // volatile 保证可见性
}
// 插入逻辑(CAS 更新指针)
private V doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z; // 新节点
// ... 查找插入位置 ...
if (casNext(pred, succ, z)) { // CAS 更新 pred.next 指针
// 插入成功
}
}
多线程下,根据读写频率选择CopyOnWriteArraySet(读多写少)或ConcurrentSkipListSet(需要有序)
Map接口
常见实现类有:HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap、HashTable。
其中线程安全的有:HashTable、ConcurrentHashMap。而其他则是非线程安全的
线程安全:
- Hashtable:数组 + 链表。通过对整个hashtable加全表锁syn保证线程安全,锁的粒度非常大,不推荐使用
- ConcurrentHashTable:数组 + 链表/红黑树(分段)。JDK不同版本有不同实现,1.7使用的是分段锁、1.8使用的是cas+syn锁当个桶,锁粒度更细,并发性能较高,高并发场景下首选
非线程安全:
- HashMap:数组+链表/红黑树
- LinkedHashMap:哈希表+双向链表(维护全局顺序,如LRU的使用)
- TreeMap:红黑树实现,没有桶的概念,基于键值对进行排序,支持范围查询
Queue接口
常见实现类有:LinkedList、PriorityQueue、ArrayDeque、阻塞队列
上面只有阻塞队列是线程安全的,其是JUC包下的线程安全队列,
- LinkedList:双向链表组成,支持快速头尾操作
- PriorityQueue:最小堆实现,可定义排序规则自由实现优先顺序
- ArrayDeque:基于动态扩容的循环数组实现,有头尾指针
阻塞队列
- ArrayBlockingQueue(有界阻塞队列):基于固定大小的数组 + 单锁(
ReentrantLock
)容量固定,初始化后不可扩容。全局锁,同一时间仅允许一个线程操作 - LinkedBlockingQueue(无界/有界链表队列):基于单向链表,默认无界(可指定容量)。头尾两把锁
- PriorityBlockingQueue(无界优先级队列):基于堆,动态扩容。CAS + 自旋锁
- SynchronousQueue(无缓冲队列):无存储空间,直接传递任务给消费者。CAS 无锁算法