1.ConcurrentHashMap
ConcurrentHashMap原理分析:https://my.oschina.net/hosee/blog/639352
不变模式,参考《Java高并发程序设计》
简要理解
Hashtable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了分段锁技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
如何确定元素位置
三次hash:
- 对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
- 将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
- 将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。
get操作
get操作大部分情况下不需要加锁。不变(Immutable)和易变(Volatile)ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
volatile HashEntry<K,V> next;
}
不变模式(immutable)是多线程安全里最简单的一种保障方式。通过volatile和final来确保数据安全。
put操作
put操作需要加锁,但是基于concurrencyLevel划分出多个Segment进行K-V存储,避免了每次put操场都锁住整个数组,理想情况下可以有16个线程同时并发无阻塞的操作集合对象。
size操作
size操作与put和get操作最大的区别在于,size操作需要遍历所有的Segment才能算出整个Map的大小,而put和get都只关心一个Segment。假设我们当前遍历的Segment为SA,那么在遍历SA过程中其他的Segment比如SB可能会被修改,于是这一次运算出来的size值可能并不是Map当前的真正大小。所以一个比较简单的办法就是计算Map大小的时候所有的Segment都Lock住,不能更新(包含put,remove等等)数据,计算完之后再Unlock。这是普通人能够想到的方案,但是牛逼的作者还有一个更好的Idea:先给3次机会,不lock所有的Segment,遍历所有Segment,累加各个Segment的大小得到整个Map的大小,如果某相邻的两次计算获取的所有Segment的更新的次数(每个Segment都有一个modCount变量,这个变量在Segment中的Entry被修改时会加一,通过这个值可以得到每个Segment的更新操作的次数)是一样的,说明计算过程中没有更新操作,则直接返回这个值。如果这三次不加锁的计算过程中Map的更新次数有变化,则之后的计算先对所有的Segment加锁,再遍历所有Segment计算Map大小,最后再解锁所有Segment。
初始化参数
- initialCapacity表示新创建的这个ConcurrentHashMap的初始容量,也就是上面的结构图中的Entry数量。默认值为static final int DEFAULT_INITIAL_CAPACITY = 16;
- loadFactor表示负载因子,就是当ConcurrentHashMap中的元素个数大于loadFactor * 最大容量时就需要rehash,扩容。默认值为static final float DEFAULT_LOAD_FACTOR = 0.75f;
- concurrencyLevel表示并发级别,这个值用来确定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。比如,如果concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。默认值为static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想情况下ConcurrentHashMap的真正的并发访问量能够达到concurrencyLevel,因为有concurrencyLevel个Segment,假如有concurrencyLevel个线程需要访问Map,并且需要访问的数据都恰好分别落在不同的Segment中,则这些线程能够无竞争地自由访问(因为他们不需要竞争同一把锁),达到同时访问的效果。这也是为什么这个参数起名为“并发级别”的原因。
hash算法
这里用到了Wang/Jenkins hash算法的变种,主要的目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。
2.CopyOnWriteArrayList
线程安全、读操作无锁的ArrayList。
可参考:http://ifeve.com/java-copy-on-write/
基本思想:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWrite并发容器用于读多写少的并发场景。缺点:(1)内存占用问题;(2)只能保证数据的最终一致性,不能保证数据的实时一致性。
3.CopyOnWriteArraySet
基于CopyOnWriteArrayList实现,原理类似,add操作要遍历数组以避免添加重复元素。
4.ArrayBlockingQueue
阻塞队列的基本概念以及简单实现 http://ifeve.com/blocking-queues/
如果队列是空的,消费者会一直等待,当生产者添加元素时候,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,让生产者和消费者能够高效率的进行通讯呢?让我们先来看看JDK是如何实现的。
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。通过查看JDK源码发现ArrayBlockingQueue使用了可重入锁ReentrantLock和Condition来实现。
阻塞队列深入可参考:http://www.infoq.com/cn/articles/java-blocking-queue/
5.AtomicInteger
基本原理:如果结果符合预期结果,就将结果返回,否则不断进行重试,并没有进行同步,兼顾了安全性和性能java.util.concurrent.atomic包下还有很多类,使用这些类可以保证对这些类的诸如“获取-更新”操作是原子性的,从而避发生竞态条件。(http://blog.youkuaiyun.com/hong0220/article/details/38958121)
可参考:http://www.cnblogs.com/timlearn/p/4127616.html
6.ThreadPoolExecutor、Executors
ThreadPoolExecutor并发包下的线程池服务。Executors提供几种静态方法创建常用线程池,但不推荐使用。
可参考:http://ifeve.com/java-threadpool/
7.FutureTask
用于异步获取执行结果或者取消执行任务的场景,通过传递Callable或Runnable任务给FutureTask,直接调用其run方法或者放入线程池中去执行,之后在外部通过其get方法异步获取执行结果。
8.Semphore
控制某资源被同时访问个数,是对锁的扩展,指定多个线程访问某一资源。对于临界区管理代码,程序会限制同时执行这段代码的线程数。
package com.dianping.gmkt.event.datapools.service.schedule;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* Semaphore实现限流案例
*/
public class Main {
/**
* 申请信号量准入数,即同时能申请多少个许可
*/
static Semaphore semaphore = new Semaphore(10);
public static void main(String[] args) throws Exception {
final Executor executor = Executors.newFixedThreadPool(1000);
for (int i = 0; i < 2000; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
executor();
}
});
}
}
public static void executor() {
try {
if (semaphore.getQueueLength() > 10) {
System.out.println("wait...");
return;
}
/**
* 尝试获得一个许可,若存在可用资源,直接返回
* 否则进入等待队列,不断尝试获得资源,而tryAcquire不会等待
*/
semaphore.acquire();
// 模拟耗时的业务逻辑
Thread.sleep(1000);
System.out.println("run...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 将信号量释放,以便其他请求可以获得空闲资源
semaphore.release();
}
}
}
9.CountDownLatch
等待多线程完成的CountDownLatch,实现异步转同步操作。
可参考:http://ifeve.com/talk-concurrency-countdownlatch/
10.CyclicBarrier
比CountDownLatch更强大,当await的数量到达指定数量后,才继续往下执行。
11.ReentrantLock、Condition
ReentrantLock reentrantLock = new ReentrantLock();
int i = 0;
// 重入示例,一个线程可以多次次获得同一把锁
reentrantLock.lock();
reentrantLock.lock();
try {
i++;
} finally {
reentrantLock.unlock();
reentrantLock.unlock();
}
synchronized 、wait()、notify()的增强版,具体体现在:
(1)重入锁提供了中断处理的能力
程序在等待锁的过程中,可以根据需要取消对锁的请求,对处理死锁有一定帮助。
(2)锁申请等待限时
tryLock()方法限时等待,避免死锁。
(3)公平锁
synchronized产生的锁是非公平的,重入锁允许进行设置,一般公平锁的实现成本较高,性能相对也非常低下,默认情况下采用非公平锁。
重入锁的好搭档:Condition条件,await(使当前线程等待,同时释放当前锁)、signal(唤醒一个在等待中的线程)方法、signalAll(唤醒所有等待中的线程)。
12.ReentrantReadWriteLock
读写锁分离,在读多写少的情景下大幅提升读的性能。类似还有ReadWriteLock。
13.volatile