基础构建模块
并发容器
同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。
ConcurrentHashMap
同步类容器在执行每个操作期间都持有一个锁。在一些操作中,例如
HashMap.get
或List.contains
可能包含大量工作;当遍历查找某个特定的对象时,如果 hashCode 不能很均匀的分布散列值,那么容器中的元素不会很均匀的分布在整个容器中。某些情况下,某个糟糕的散列函数还会把一个散列表编程线性链表。
与 HashMap
一样,ConcurrentHashMap
也是一个基于散列的 Map,但他使用了一种完全不同的加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap
并不是将每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制被称为分段锁
。
由于ConcurrentMap
不能被加锁来执行独占访问,因此我们无法使用客户端加锁来创建新的原子操作。但是像若没有则添加
、若相等则移除
、若相等则替换
等操作在ConcurrentMap
中都已经声明并实现为原子操作。
CopyOnWriteArrayList
用于替代同步
List
,在某些情况下提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制。
写入时复制
容器的线程安全性在于,只要正确的发布一个事实不可变的对象,那么在访问改对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。
显然复制策略会造成比较大的资源消耗,所以我们仅当迭代操作远远多于修改操作时,才应该使用写入时复制容器
。
阻塞队列和生产者-消费者模式
阻塞队列提供了可阻塞的
put
和take
方法,以及支持定时的offser
和poll
方法。如果队列已经满了,那么put
方法将阻塞直到有空间可用;如果队列为空,那么take
方法将会阻塞直到有元素可用。
Java中包含了BlockingQueue
的多种实现,其中,LinkedBlockingQueue
和ArrayBlocingQueue
时 FIFO队列。PriorityBlockingQueue
是一个按优先级排序的队列,当需要按照某种顺序而不是 FIFO 来处理元素时,这个队列非常有用。
还有一个是SynchronousQueue
,它不是真正的队列,因为它不会为队列中元素维护储存空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移除队列。
阻塞队列支持生产者-消费者模式(Producer-Consumer,简称PC)。PC 模式能简化开发过程,它能够消除生产者类和消费者类之间代码的依赖性,此外,该模式还将生产数据的过程与使用数据的过程解耦开来以简化工作负载的管理,因为这两个过程在处理数据的速率上有所不同。
双端队列与工作密取
Java6 增加了
Deque
和BlockingDeque
。Deque
是一个双端队列,实现了在队列头和队列尾的高效插入和删除,具体实现包括ArrayDeque
和LinkedBlockingDeque
。
正如阻塞队列适用于 PC 模式,双端队列适用于另一种相关模式,即工作密取。在 PC 中,所有消费者共享一个工作队列,而在工作密取中,每个消费者都拥有各自的双端队列。如果一个消费者完成了自己的双端队列中的全部任务,那么它可以从其他消费者双端队列末尾秘密的获取工作。
同步工具类
在容器类中,阻塞队列是一种独特的类:它们不仅能作为保存对象的容器,还能协调生产者和消费者等线程之间的控制流,因为 take 和 put 等方法将阻塞,直到队列达到期望的状态(队列既非空,也非满)
同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类,其他类型的同步工具类还包括信号量(semaphore)、栅栏(Barrier)以及闭锁(Latch)。
闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有线程通过。
闭锁可以用来确保某些活动直到其他活动都完成后才继续执行,例如:
* 确保某个计算在其需要的所有资源都被初始化之后才继续执行。
* 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
* 等待直到某个操作的所有参与者都就绪后,再继续执行。
简单示例代码
public class CountDownLatchTest {
private static final int TASK_SIZE = 10;
public static void main(String[] args) throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch endSignal = new CountDownLatch(TASK_SIZE);
for (int i = 0; i < TASK_SIZE; ++i) {
new Thread(new Worker(startSignal,endSignal)).start();
}
System.out.println("线程初始化,start调用完成,准备休眠1s");
Thread.sleep(1000);
startSignal.countDown();
System.out.println("开始信号已发起,等待任务自行完成");
endSignal.await();
System.out.println("任务执行完成");
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch endSignal;
public Worker(CountDownLatch startSignal, CountDownLatch endSignal) {
this.startSignal = startSignal;
this.endSignal = endSignal;
}
@Override
public void run() {
try {
startSignal.await();
System.out.println("Thread name : " + Thread.currentThread().getName());
Thread.sleep(1500);
System.out.println(" Done Thread id : " + Thread.currentThread().getId());
endSignal.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
FutureTask
FutureTask
实现了Future
。FutureTask
计算任务是通过Callable
来实现的,相当于一种可生成结果的Runnable
。
Future.get
的行为取决于任务的状态,如果任务已经完成,那么get
会立即返回结果,否则会阻塞直到任务进入完成状态。
示例代码:
public class FutureTaskTest {
@Test
public void primaryTest() throws ExecutionException, InterruptedException {
DataGet dataGet = new DataGet();
Callable userInfoCall = new Callable() {
@Override
public String call() throws Exception {
return dataGet.getUserInfo("Hello");
}
};
Callable userAddressCall = new Callable() {
@Override
public String call() throws Exception {
return dataGet.getUserAddress("Hello");
}
};
FutureTask userInfoTask = new FutureTask<>(userInfoCall);
FutureTask userAddressTask = new FutureTask<>(userAddressCall);
new Thread(userInfoTask).start();
new Thread(userAddressTask).start();
String userInfo = userInfoTask.get();
System.out.println("userInfo: " + userInfo);
String userAddress = userAddressTask.get();
System.out.println("userAddress: " + userAddress);
}
@Test
public void blockingTest() throws ExecutionException, InterruptedException {
PreLoader preLoader = new PreLoader();
System.out.println("current is " + System.currentTimeMillis());
preLoader.start();
String info = preLoader.get();
System.out.println(info);
}
}
class PreLoader {
private final FutureTask future = new FutureTask<>(new Callable() {
@Override
public String call() throws Exception {
DataGet dataGet = new DataGet();
return dataGet.getUserAddress("H");
}
});
private final Thread thread = new Thread(future);
void start() {
thread.start();
}
String get() throws ExecutionException, InterruptedException {
return future.get();
}
}
class DataGet {
String getUserInfo(String name) {
String userInfo;
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
userInfo = "userInfo is " + System.currentTimeMillis();
return userInfo;
}
String getUserAddress(String name) {
String address;
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
address = "address is " + System.currentTimeMillis();
return address;
}
}
信号量
信号量(semaphores)是 20 世纪 60 年代中期 Edgser Dijkstra 发明的。使用信号量的最初目的是为了给共享资源建立一个标志,该标志表示该共享资源被占用情况。这样,当一个任务在访问共享资源之前,就可以先对这个标志进行查询,从而在了解资源被占用的情况之后,再来决定自己的行为。
public class SemaphoreTest {
public static void main(String[] args) {
SemaphoreService semaphoreService = new SemaphoreService();
for (int i = 0; i < 5; i++) {
new Thread(){
@Override
public void run() {
try {
semaphoreService.working();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
class SemaphoreService {
//初始化信号量,并设置 permit 为1。permit是多少,表示同一个时刻,只允许多少个线程同时运行指定代码
private Semaphore semaphore = new Semaphore(3);
public void working() throws InterruptedException {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " : start");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " : end");
semaphore.release();
}
}
栅栏
栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏和闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier
CyclicBarrier
可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分成一系列相互独立的子问题。当所有线程达到栅栏位置时将调用 await 方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。
将被重置以便下次使用。如果对 await
调用超时,或者 await
阻塞的线程被中断,那么栅栏就被认为是打破了,所有阻塞的 await
调用都将终止并抛出 BrokenBarrierException
。如果成功通过栅栏,那么 await
将为每个线程返回一个唯一的索引号,我们可以利用这些索引来『选举』产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。
示例代码:BarrierTest.cyclicBarrierTest
Exchanger
Exchanger
它是一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称操作时,Exchanger
非常有用。例如一个线程(A)在完成一定事务后想与另一个线程(B)交换数据,在需要交换数据的地方设置栅栏(使用exchange()
)时使用。此时如果 B 线程数据未准备就绪,A 线程会阻塞至B 完成数据处理。
示例代码:BarrierTest.exchangerTest
BarrierTest
public class BarrierTest {
@Test
public void cyclicBarrierTest() throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("Barrier Action notify");
}
});
List threads = new ArrayList<>();
for (int i = 0; i < 5; i++) {
CyclicBarrierTask t = new CyclicBarrierTask(cyclicBarrier);
threads.add(t);
t.start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println("结束任务");
}
@Test
public void exchangerTest() throws InterruptedException {
Exchanger exchanger = new Exchanger<>();
List threads = new ArrayList<>();
UserA a = new UserA(exchanger);
UserB b = new UserB(exchanger);
threads.add(a);
threads.add(b);
a.start();
b.start();
for (Thread thread : threads) {
thread.join();
}
}
}
class UserA extends Thread {
private Exchanger exchanger;
public UserA(Exchanger exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.SECONDS.sleep(2);
int data = i;
System.out.println("User A before exchange : " + data);
data = exchanger.exchange(data);
System.out.println("User A after exchange : " + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class UserB extends Thread {
private Exchanger exchanger;
private static int data = 999;
public UserB(Exchanger exchanger) {
this.exchanger = exchanger;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
data = 999 + i;
try {
System.out.println("User B before exchange : " + data);
TimeUnit.SECONDS.sleep(4);
data = exchanger.exchange(data);
System.out.println("User B after exchange : " + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class CyclicBarrierTask extends Thread {
private CyclicBarrier cb;
public CyclicBarrierTask(CyclicBarrier cb) {
this.cb = cb;
}
@Override
public void run() {
super.run();
try {
Thread.sleep(1500);
System.out.println(getName() + "到达屏障 A");
cb.await();
System.out.println(getName() + "冲破屏障 A");
Thread.sleep(2000);
System.out.println(getName() + "到达屏障 B");
cb.await();
System.out.println(getName() + "冲破屏障 B");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
构建线程安全的缓存策略
如下代码报了了从
Memorizer1
、Memorizer2
、Memorizer3
的缺陷分析,到终极版Memorizer
构建
/**
* 构建高效且可伸缩的结果缓存
*
* @author lijie
* @create 2019-11-19 15:23
**/
public class CacheTest {
}
interface Computable {
V compute(A arg) throws InterruptedException;
}
class ExpensiveFunction implements Computable {
@Override
public BigInteger compute(String arg) {
return new BigInteger(arg);
}
}
/**
* 线程安全,但是 compute 方法同时只能由一个线程访问
*
* @param
* @param
*/
class Memorizer1 implements Computable {
private final Map cache = new HashMap<>();
private final Computable c;
public Memorizer1(Computable c) {
this.c = c;
}
@Override
public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}
/**
* 可以多线程同步访问,但是当并发重复计算时,会造成较大开销
* 例如我们正在计算一个开销很大的运算f(27),另一个线程也准备查询 f(27),当前策略会造成重复计算。
*
* @param
* @param
*/
class Memorizer2 implements Computable {
private final Map cache = new ConcurrentHashMap<>();
private final Computable c;
public Memorizer2(Computable c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}
/**
* Memorizer3 有比较好的并发性(基本上是源于 ConcurrentHashMap 高效的并发性),它只有一个缺陷,即两个线程计算出相同值的漏洞。
* 这个漏洞的发生概率要远小于 Memorizer2 发生的概率,
* 但由于 compute 中的 if 代码块是非原子的,因此两个线程仍有可能在同一时间内调用 compute 来计算相同的值
* 即二者都没有在缓存中找到期望的值,因此都开始计算。
*
* @param
* @param
*/
class Memorizer3 implements Computable {
private final Map> cache = new ConcurrentHashMap<>();
private final Computable c;
public Memorizer3(Computable c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
Future f = cache.get(arg);
if (f == null) {
Callable eval = new Callable() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask ft = new FutureTask<>(eval);
f = ft;
cache.put(arg, f);
ft.run();
}
try {
return f.get();
} catch (ExecutionException e) {
throw new InterruptedException(e.getCause().getMessage());
}
}
}
class Memorizer implements Computable {
private final ConcurrentMap> cache = new ConcurrentHashMap<>();
public Memorizer(Computable c) {
this.c = c;
}
private final Computable c;
@Override
public V compute(A arg) throws InterruptedException {
while (true){
Future f = cache.get(arg);
if (f == null){
Callable eval = new Callable() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask ft = new FutureTask<>(eval);
f = cache.putIfAbsent(arg,ft);
if (f == null){
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}