首先,HashTable 是一种线程安全的哈希表,它内部使用的是同步锁来保证线程安全。在并发读写的场景下,同步锁会导致线程的阻塞,从而影响性能。此外,HashTable 在扩容时需要对所有的元素重新计算哈希值,并重新分配到新的桶中,这个过程也会导致性能下降。
相比之下,ConcurrentHashMap 在设计时就考虑到了并发性能问题。它内部采用了一种更加高效的锁分离技术,将整个哈希表分成多个小的部分,每个部分使用独立的锁来保证线程安全,这种设计可以在不影响整个 ConcurrentHashMap 性能的情况下,实现更高的并发度。具体来说,ConcurrentHashMap 内部将数据分成多个 Segment,每个 Segment 维护着一个独立的散列表。在读写数据时,只需要锁住对应的 Segment,而不需要锁住整个 ConcurrentHashMap,这样就可以实现更好的并发度。
此外,ConcurrentHashMap 的扩容过程也与 HashTable 不同。ConcurrentHashMap 在扩容时只需要对部分桶进行重新分配即可,这样就可以避免重新计算所有元素的哈希值,从而提高了性能。
总的来说,ConcurrentHashMap 在并发性能方面优于 HashTable,这是由于它采用了更加高效的锁分离技术和更加优化的扩容算法。
在 JDK1.7 中,ConcurrentHashMap 内部采用了分段锁技术来保证并发读写的线程安全。具体来说,ConcurrentHashMap 将数据分成多个 Segment,每个 Segment 维护着一个独立的散列表,并使用独立的锁来保证线程安全。这种设计可以实现更好的并发度,但是在高并发的场景下,仍然存在性能问题。具体来说,当多个线程同时读写同一个 Segment 时,会导致线程的竞争,从而影响性能。
为了解决这个问题,JDK1.8 对 ConcurrentHashMap 进行了优化,主要有以下几个方面:
使用 CAS 操作替代锁来更新散列表。在 JDK1.7 中,ConcurrentHashMap 在更新散列表时需要获取锁,这样会导致线程的阻塞,从而影响性能。在 JDK1.8 中,ConcurrentHashMap 使用了 CAS 操作来更新散列表,避免了锁的竞争,从而提高了性能。
减少锁的粒度。在 JDK1.8 中,ConcurrentHashMap 采用了一种更加细粒度的锁设计,可以将锁的粒度降低到每个元素级别,从而避免了线程竞争,提高了性能。
使用红黑树代替链表。在 JDK1.8 中,ConcurrentHashMap 在散列表中的桶内存储的元素采用了一种新的数据结构:当链表长度超过一定阈值时,将链表转化为红黑树。这种设计可以提高散列表的查询效率,从而提高性能。
减少重试次数。在 JDK1.8 中,ConcurrentHashMap 的 put 操作中,为了避免出现并发冲突,需要进行多次重试。在 JDK1.8 中,减少了重试的次数,从而提高了性能。
综上所述,JDK1.8 对 ConcurrentHashMap 进行了一系列的优化,主要是通过减少锁竞争、使用 CAS 操作、采用更加细粒度的锁设计以及使用红黑树等方式来提高性能,并且解决了 JDK1.7 中存在的线程竞争问题。
在 JDK1.7 中,ConcurrentHashMap 的实现原理是基于分段锁(Segment),也被称为“锁分离”技术。
具体来说,ConcurrentHashMap 将整个哈希表分成多个 Segment,每个 Segment 维护着一个独立的散列表,并使用独立的锁来保证线程安全。
散列表的实现
初始长度:16
每次扩容翻倍
最大长度:2^30
初始 Segment 数量:16
每个 Segment 内部散列表的初始长度:2^4
每次扩容翻倍
每个 Segment 内部散列表最大长度:2^30
Segment 的实现
每个 Segment 内部元素数量一般不超过 1-2 百个
count 字段:记录当前 Segment 内部元素数量
modCount 字段:用于迭代器快速失败机制的计数器
table 字段:存储键值对的散列表
lock() 方法:获取 Segment 内部的锁
unlock() 方法:释放 Segment 内部的锁
tryLock() 方法:尝试获取 Segment 内部的锁,如果成功返回 true,否则返回 false
并发控制的实现
读操作不需要加锁,写操作需要加锁
读写分离提高并发度,避免线程阻塞
读写操作都是原子的,不需要额外的同步措施
扩容机制的实现
扩容阈值为当前 Segment 内部元素数量的 3/4
扩容的过程需要将整个 ConcurrentHashMap 中的所有元素重新分配到新的散列表中
扩容过程比较耗时,会影响性能
扩容过程中,读操作可以继续进行,写操作需要等待扩容完成
在 JDK1.7 中,一旦 ConcurrentHashMap 被初始化之后,Segment 的数量就不能再改变了。这是因为 ConcurrentHashMap 在内部使用了一个数组来存储 Segment,这个数组在初始化的时候就被创建好了,并且数组的大小也不会改变。这意味着,如果需要调整并发度,就需要创建一个新的 ConcurrentHashMap,并将原有的数据转移到新的 ConcurrentHashMap 中。这个过程比较耗时,也会对性能造成一定的影响。
在 JDK1.7 中,ConcurrentHashMap 的 put 操作主要包括以下几个步骤:
根据给定的 key 计算出其 hash 值,并根据 hash 值定位到相应的 Segment。
在 Segment 中,使用 synchronized 关键字获取锁,以保证线程安全。
判断 key 是否已经存在于当前 Segment 中。如果存在,则更新其对应的 value 值,并返回旧的 value。
如果 key 不存在,则创建一个新的 Entry 对象,并将其插入到 Segment 中的散列表中。
如果插入后,当前 Segment 中的元素数量超过了扩容阈值,则触发扩容操作。
使用 synchronized 关键字释放锁。
值得注意的是,在 JDK1.7 中,ConcurrentHashMap 的 put 操作并不是完全无锁的。在插入新元素的过程中,需要使用 synchronized 关键字获取锁,以保证线程安全。这意味着在高并发场景下,多个线程可能会竞争同一个锁,导致一些线程需要等待锁的释放。这可能会影响性能。
ConcurrentHashMap 在 JDK1.7 中的扩容可以通过 rehash ,即将原有散列表中的元素重新计算 hash 值,并将其插入到新的散列表中。这个过程需要对每个 Segment 进行单独的处理,并且需要使用 synchronized 进行同步。因此,在高并发环境下,这个过程可能会影响 ConcurrentHashMap 的性能。
ConcurrentHashMap 在 JDK1.8 中的实现原理相比于 JDK1.7 有所改变,主要包括两个方面:散列算法和数据结构(数组+链表+红黑树+CAS)。
首先,JDK1.8 中的 ConcurrentHashMap 改变了散列算法,使用了一种称为 "位运算" 的方法来计算元素的哈希值。这种算法比 JDK1.7 中的算法更快,同时也减少了哈希冲突的数量。
其次,JDK1.8 中的 ConcurrentHashMap 采用了一种名为 "CAS" (Compare-And-Swap) 的原子操作来保证线程安全,而不是像 JDK1.7 中一样使用 synchronized 关键字进行同步。在 JDK1.8 中,每个 Segment 被分成了多个大小相等的桶(bucket),每个桶都对应着一个链表或者红黑树。当多个线程同时访问 ConcurrentHashMap 时,它们会同时访问多个不同的桶,从而实现了更高的并发度。
在 JDK1.8 中,ConcurrentHashMap 的扩容机制也发生了变化。当某个桶的元素数量超过阈值时,ConcurrentHashMap 会将整个桶升级为红黑树,以提高元素的查找效率。
JDK1.8 中的 ConcurrentHashMap 在多线程环境下具有更高的性能和更好的扩展性,因为它采用了更为高效的散列算法和原子操作,并且可以实现更高的并发度。
JDK1.8 中 ConcurrentHashMap 扩容的具体步骤:
当一个 Segment 中的某个桶(bucket)中的元素数量超过一定阈值时,就会触发这个桶的扩容操作。具体来说,如果这个桶中的元素数量超过了 TreeBins 阈值(默认为 8),则将这个桶升级为红黑树;否则,如果这个桶中的元素数量超过了链表的阈值(默认为 8),则将这个桶里的元素全部转化为红黑树,否则就直接扩容。
对于需要扩容的桶(bucket),ConcurrentHashMap 会为其分配一个新的桶数组。新桶数组的大小是原数组的两倍,并且每个桶(bucket)中的元素数量都被重新计算了一遍,从而能够更好地适应新的散列函数。
然后,ConcurrentHashMap 会将原来桶(bucket)中的元素重新散列到新的桶数组中。具体来说,它会遍历原来的桶(bucket),将每个元素的 hash 值重新计算,并将其插入到新桶数组中的一个桶(bucket)中。这个过程中,ConcurrentHashMap 会使用 CAS 操作来保证并发安全。
最后,当所有的元素都被重新散列到新的桶数组中之后,原来的桶(bucket)就可以被丢弃了,这样就完成了扩容操作。
需要注意的是,在扩容期间,ConcurrentHashMap 会对新旧两个桶数组进行并行更新。这样,即使在扩容期间有线程访问 ConcurrentHashMap,它们也可以同时读取旧桶数组中的元素,同时写入新桶数组中的元素,从而提高了并发度。
ConcurrentHashMap JDK1.8中,当某个桶(bucket)中的元素个数超过阈值8时,会将这个桶(bucket)中的元素全部转换为红黑树;当这个桶(bucket)中的元素个数小于等于6时,会将这个桶(bucket)中的红黑树转换为链表结构。阈值为8的原因是实验表明,在链表元素个数超过8时,红黑树的查找操作速度已经超过了链表。
ConcurrentHashMap的数据迁移是通过将每个桶(bucket)中的元素复制到新的桶数组中,重定位(rehash)每个元素并将其插入新的桶中实现的。为了提高并发性能,JDK1.8采用了无锁分段算法,锁住一小段数据进行操作,从而减少锁竞争,提高性能。
CopyOnWriteArrayList的实现原理主要是利用“写时复制”的技术,即在进行写操作时,会先复制一份当前数组,然后在新的数组上进行修改操作,完成之后再将新数组替换旧数组,以此保证读取操作和写入操作之间的互不干扰。
下面是CopyOnWriteArrayList的部分源码实现,帮助理解它的实现原理:
java
public class CopyOnWriteArrayList implements List, RandomAccess, Cloneable, Serializable {
// 数据数组
private transient volatile Object[] array;
// 对数据数组进行修改的锁
final transient ReentrantLock lock = new ReentrantLock();
// 添加元素
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 获取锁
try {
Object[] elements = getArray(); // 获取数据数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements); // 设置数据数组为新数组
return true;
} finally {
lock.unlock(); // 释放锁
}
}
// 获取数据数组
final Object[] getArray() {
return array;
}
// 设置数据数组为新数组
final void setArray(Object[] a) {
array = a;
}
}
在上面的代码中,CopyOnWriteArrayList内部使用了一个Object[] array数组来存储数据。在添加元素时,首先会获取锁,然后通过getArray()方法获取当前的数据数组,接着复制一份当前的数组,并在新的数组上进行修改,最后通过setArray()方法将新数组设置为当前的数据数组。
需要注意的是,由于在进行写操作时需要复制一份数组,因此CopyOnWriteArrayList在内存使用和性能方面的开销较大,适用于读操作远远多于写操作的场景。
CopyOnWriteArrayList是一种线程安全的List容器,它的迭代器也是弱一致性迭代器。在CopyOnWriteArrayList的迭代器被创建时,它会对容器进行一次快照,将容器当前的状态保存下来。在迭代器进行遍历时,它所访问到的元素就是这个快照中的元素,而不是容器当前的元素。当容器中的元素被其他线程进行修改或删除时,这些操作不会影响到迭代器中的快照。
由于CopyOnWriteArrayList的迭代器是弱一致性迭代器,因此它不会抛出ConcurrentModificationException异常,即使在迭代期间容器中的元素被其他线程修改或删除也不会影响迭代器的操作。但是需要注意的是,由于CopyOnWriteArrayList的迭代器只是快照中的元素,并不是容器的当前元素,因此在对容器进行修改后,可能需要重新获取一个新的迭代器,才能遍历到最新的元素。
CopyOnWriteArrayList和Vector都是线程安全的List容器,但它们的实现方式不同。CopyOnWriteArrayList采用了"写时复制"的策略,即在对容器进行修改时,会先将容器中的元素复制一份,对这份副本进行修改,然后再用修改后的副本替换原来的容器。这种策略保证了对容器的读操作不需要加锁,因此不会发生线程争用和阻塞,从而提高了并发访问的效率。
另一方面,Vector的实现采用了同步锁的方式来保证线程安全。在对Vector进行读写操作时,都需要先获取同步锁,因此会存在线程之间的竞争和阻塞,导致效率较低。
由此可见,CopyOnWriteArrayList通过减少锁的粒度,避免了同步锁带来的竞争和阻塞,从而提高了并发访问的效率。虽然它的写操作可能会比Vector更慢,因为需要进行一次复制操作,但由于读操作是非常高效的,适合读多写少的场景,因此在大多数情况下,CopyOnWriteArrayList的性能要优于Vector。
虽然CopyOnWriteArrayList具有很好的并发安全性和读取性能,但也存在一些缺陷,主要体现在以下几个方面:
内存占用较大。CopyOnWriteArrayList在进行写操作时需要对容器进行一次复制,因此会占用额外的内存空间,对于数据量较大的场景,可能会导致内存不足。
写操作性能较差。由于CopyOnWriteArrayList的写操作需要进行一次复制操作,因此对于写操作较频繁的场景,性能可能不如其他的并发容器。
不适合实时性要求高的场景。由于CopyOnWriteArrayList的迭代器是弱一致性迭代器,因此在迭代器被创建之后,容器中发生的修改操作并不会立即反映在迭代器中,这可能会导致在实时性要求较高的场景中出现问题。
CopyOnWriteArrayList的应用场景主要是在读多写少的场景中,例如缓存、事件监听器等。在这些场景中,容器的读操作远远多于写操作,因此CopyOnWriteArrayList的读取性能可以得到很好的发挥。同时,由于这些场景对实时性要求并不是非常高,因此CopyOnWriteArrayList的弱一致性迭代器也不会成为问题。但如果在一些写操作较频繁的场景中使用CopyOnWriteArrayList,可能会导致性能问题和内存占用过大的问题。
Java中提供了多种线程安全的队列实现,常用的有以下几种:
ConcurrentLinkedQueue:基于链表实现的线程安全队列,适用于多线程并发操作,性能比较高。
LinkedBlockingQueue:基于链表实现的阻塞队列,具有容量限制,支持多线程并发操作,可用于实现生产者-消费者模型。
ArrayBlockingQueue:基于数组实现的阻塞队列,具有容量限制,支持多线程并发操作,可用于实现生产者-消费者模型。
SynchronousQueue:一个特殊的队列,每次插入操作必须等待一个相应的删除操作,反之亦然,适用于直接传递任务的场景。
选择何种线程安全队列,需要根据实际的业务场景和需求来选择,如果需要高并发的队列,可以选择ConcurrentLinkedQueue;如果需要在生产者-消费者模型中使用队列,可以选择LinkedBlockingQueue和ArrayBlockingQueue;如果需要在多个线程之间传递任务,可以选择SynchronousQueue。
ConcurrentLinkedQueue是一个基于链表实现的线程安全队列,它采用无锁的CAS算法来保证并发安全。其内部实现是一个无界的链表结构,队列中的每个元素都包含一个指向下一个元素的引用。
ConcurrentLinkedQueue的队头和队尾都是由指针来维护的,因此入队和出队操作都只需要修改指针即可。同时,由于CAS的比较和交换操作不会阻塞线程,因此ConcurrentLinkedQueue可以实现高效的并发操作。
由于ConcurrentLinkedQueue是一个无界队列,因此它不会出现因队列已满而无法继续添加元素的情况。但是,需要注意的是,由于队列是无界的,因此在使用ConcurrentLinkedQueue时需要格外小心,以防止队列中元素的数量无限增长,导致内存占用过高。
ConcurrentLinkedQueue的入队操作add()和offer()方法是通过调用内部的offerLast()方法实现的。offerLast()方法使用CAS操作来在链表尾部添加元素。当一个线程执行offerLast()时,它会先将新元素链接到队列的末尾,然后使用CAS操作来将tail指针指向新元素。如果CAS操作失败,说明有其他线程已经修改了tail指针,那么当前线程就需要重试操作。
ConcurrentLinkedQueue的出队操作poll()和remove()方法是通过调用内部的pollFirst()方法实现的。pollFirst()方法使用CAS操作来从链表头部移除元素。当一个线程执行pollFirst()时,它会先将head指针指向队列头部的下一个元素,然后使用CAS操作来将head指针指向新的队列头部元素。如果CAS操作失败,说明有其他线程已经修改了head指针,那么当前线程就需要重试操作。
// 反射机制
private static final sun.misc.Unsafe UNSAFE;
// head域的偏移量
private static final long headOffset;
// tail域的偏移量
private static final long tailOffset;
说明: 属性中包含了head域和tail域,表示链表的头节点和尾结点,同时,ConcurrentLinkedQueue也使用了反射机制和CAS机制来更新头节点和尾结点,保证原子性。
ConcurrentLinkedQueue的核心方法包括以下几个:
add(E e) 和 offer(E e): 添加元素到队列的尾部。
poll() 和 peek(): 获取并移除队列头部的元素。如果队列为空,poll() 返回null,peek() 返回null或者抛出NoSuchElementException异常。
size(): 返回队列中元素的数量,这个方法不保证实时准确。
isEmpty(): 判断队列是否为空。
iterator(): 返回队列迭代器,用于遍历队列中的元素。
ConcurrentLinkedQueue的这些方法都是线程安全的,并且具有较好的性能和并发能力,可以满足并发场景下的需求。需要注意的是,ConcurrentLinkedQueue并没有提供一些队列中常见的方法,如remove()和contains()等方法,如果需要使用这些方法,需要自行实现。
ConcurrentLinkedQueue使用的是一种延迟更新的策略,即HOPS(Hand-Off-Point-Sentinel)。这个策略可以提高队列在高并发下的性能,减少锁的争用,降低线程之间的竞争。
HOPS的核心思想是,在ConcurrentLinkedQueue中,每个节点都包含一个指向下一个节点的指针,同时节点还包含一个特殊的标记位(Sentinel),这个标记位表示当前节点的下一个节点是否有效。当一个线程要添加或者删除一个节点时,它会先找到当前节点的后继节点,并尝试获取它的标记位。如果这个标记位是有效的,那么这个线程就将这个节点设置为无效的,并且将这个节点的指针指向后继节点的后继节点(即跳过了一个无效的节点)。这样,这个节点就被成功删除了。如果这个标记位是无效的,那么这个线程就会将当前节点的指针指向后继节点,并将后继节点的标记位设置为无效的,然后尝试删除这个节点的后继节点。
这种延迟更新的策略可以减少锁的争用,降低线程之间的竞争。这是因为在并发访问下,多个线程可能同时尝试删除或者添加节点,而使用HOPS策略后,这些线程只需要检查节点的标记位,就可以确定当前节点的下一个节点是否有效,从而避免了锁的竞争。这种策略可以使ConcurrentLinkedQueue在高并发场景下的性能得到大幅度的提升。
ConcurrentLinkedQueue适合那些需要高效并发访问的生产者消费者模型,特别是那些生产者和消费者并发量相对较大的情况。由于ConcurrentLinkedQueue是基于链表实现的无界队列,可以支持任意数量的元素插入和删除操作,同时保证并发安全性。
ConcurrentLinkedQueue适合用于需要高效地将任务提交给线程池进行处理,因为任务提交的并发量通常很大。ConcurrentLinkedQueue也适合用于消息队列的实现,它可以保证消息的顺序性和并发性,避免了因为锁竞争而导致的性能瓶颈。
总之,ConcurrentLinkedQueue适合需要高效的并发访问和高吞吐量的应用场景,例如高并发的任务处理、消息队列等。
Java中的BlockingQueue大家族包括以下几种队列:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界(默认大小为Integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中取出元素。
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,相比于LinkedBlockingQueue提供了更高的并发性能和扩展性。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
这些队列都实现了BlockingQueue接口,它提供了线程安全的队列操作,同时支持线程的阻塞等待。
put(E e):向队列尾部添加一个元素,如果队列已满则会阻塞直到有空间可用。
take():从队列头部取出一个元素,如果队列为空则会阻塞直到有元素可用。
offer(E e, long timeout, TimeUnit unit):向队列尾部添加一个元素,在给定的等待时间内等待空间可用,如果超时则返回false。
poll(long timeout, TimeUnit unit):从队列头部取出一个元素,在给定的等待时间内等待元素可用,如果超时则返回null。
remainingCapacity():返回队列中剩余的可用空间。
size():返回队列中的元素数量。
clear():从队列中移除所有元素。
toArray():返回一个包含队列中所有元素的数组。
BlockingDeque是Java中的阻塞双端队列,它是ConcurrentLinkedDeque的子类,提供了更多的阻塞操作。阻塞双端队列支持在队列的两端插入和删除元素,可以在队列的两端进行元素的插入和删除操作,因此比普通的阻塞队列功能更加强大。
BlockingDeque适用于那些需要同时进行插入和删除操作,并且需要阻塞等待的场景。例如生产者消费者模式中,生产者需要将数据插入队列的尾部,消费者需要从队列的头部获取数据。当队列已满时,生产者需要等待队列腾出空间;当队列为空时,消费者需要等待生产者往队列中插入数据。
BlockingDeque和BlockingQueue都是Java并发包中提供的阻塞队列,它们都支持在队列为空或已满时阻塞线程的操作。
区别在于,BlockingDeque是一个双端队列,支持在队列两端进行插入和移除操作,而BlockingQueue只支持在队列的一端进行插入和移除操作。
以下是它们的一些方法对比:
插入元素:
BlockingDeque:
addFirst(E e):将元素插入到队列的开头,如果队列已满则抛出异常。
addLast(E e):将元素插入到队列的末尾,如果队列已满则抛出异常。
offerFirst(E e):将元素插入到队列的开头,如果队列已满则返回false。
offerLast(E e):将元素插入到队列的末尾,如果队列已满则返回false。
BlockingQueue:
add(E e):将元素插入到队列的末尾,如果队列已满则抛出异常。
offer(E e):将元素插入到队列的末尾,如果队列已满则返回false。
移除元素:
BlockingDeque:
takeFirst():移除并返回队列开头的元素,如果队列为空则阻塞线程。
takeLast():移除并返回队列末尾的元素,如果队列为空则阻塞线程。
pollFirst():移除并返回队列开头的元素,如果队列为空则返回null。
pollLast():移除并返回队列末尾的元素,如果队列为空则返回null。
BlockingQueue:
remove():移除并返回队列头部的元素,如果队列为空则抛出异常。
poll():移除并返回队列头部的元素,如果队列为空则返回null。
其他的方法如remainingCapacity、peek等都类似。
由于BlockingDeque支持双向操作,因此它比BlockingQueue更加灵活,在一些需要同时支持队列和栈操作的场景中,使用BlockingDeque可以更加方便地实现功能。
Java并发包中提供了以下几种BlockingDeque:
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
ArrayBlockingDeque:一个由数组结构组成的双向阻塞队列。
这两种BlockingDeque都是有界队列,它们在创建时需要指定队列的容量大小。
ConcurrentLinkedDeque:一个无界的、非阻塞的双向队列,内部采用CAS和乐观锁的机制实现线程安全。
LinkedTransferQueue:一个由链表结构组成的、支持优先级和无界阻塞队列。它支持异步非阻塞的元素传输,可以用于实现生产者消费者模式。
需要注意的是,LinkedTransferQueue虽然不是BlockingDeque的子类,但它继承了BlockingQueue接口,并且其操作类似于BlockingDeque。