文章目录
java核心技术 基础知识<14章 part2>
14章 part2
14.7 线程安全的集合
包前缀:java.util.concurrent.XXX
ConcurrentLinkedQueue<E>
// 无边界非阻塞的队列
ConcurrentSkipListSet<E>
// 有序集要求元素实现Comparable接口。
ConcurrentHashMap<K, V>
// 散列映射表 initialCapacity loadFactor concurrencyLevel
ConcurrentSkipListMap<K, V>
// 有序的映像表
集合返回弱一致性( weakly consistent) 的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改, 但是, 它们不会将同一个值返回两次,也 不会拋出ConcurrentModificationException异常。
14.7.2 map条目的原子更新
ConcurrentXX集合的并发性体现在对于内部结构的并发修改,即put\get不会造成并发问题。对于条目的更新是有可能非线程安全的(put get 前后的操作不能保证线程安全)。
比如对于ConcurrentHashMap<String, Long>
:
Long oldValue = map.get(word);
Long newValue = oldValue == null ? 1: oldValue + 1;
map.put(word, newValue); // Error-might not replace oldValue
是线程不安全的。get/put没问题,但是其他的可能有问题。
传统做法:用replace
操作,它会以原子方式用一个新值替换原值。
或者用ConcurrentHashMap<String,AtomicLong>
,将Value设置为原子操作的类型。
map.putIfAbsent(word, new LongAdder());
// 确保有一个LongAdder 可以完成原子自增
map.get(word).increment();
// 获取并自增
或者
map.putlIfAbsent(word, new LongAdder()).increment();
// 有一系列的原子ConcurrentMap操作,见API
computeIfAbsent Present
merge
14.7.3 并发散列的批操作
3种操作
- 搜索(search)
- 规约(reduce)
- forEach
4个版本
- operationKeys: 处理键。
- operatioriValues : 处理值。
- operation: 处理键和值。
- operatioriEntries: 处理Map.Entry 对象。
对于上述各个操作, 需要指定一个参数化阈值(parallelism threshold,多少个元素放一个线程操作。如果映射包含的元素多于这个阈值, 就会并行完成批操作。如果希望批操作在一个线程中运行, 可以使用阈值Long.MAX_VALUE
。如果希望用尽可能多的线程运行批操作,可以使用阈值1。
举例:
search 找出第一个出现次数超过1000 次的单词。需要搜索键和值。
String result = map.search(threshold, (k, v) -> v > 1000 ? k : null );
forEach 方法有两种形式。第一个只为各个映射条目提供一个消费者函数;第二种形式还有一个转换器函数, 这个函数要先提供, 其结果会传递到消费者。
map.forEach(threshold, (k, v) -> System.out.println(k + " -> " + v));
map.forEach(threshold,
(k, v)-> k + " -> " + v, // Transformer
System.out::println); // Consumer
// 用作过滤器,转换器返回null , 这个值就会被悄无声息地跳过。
map.forEach(threshold,
(k, v) -> v > 1000 ? k + "-> " + v : null , // Filter and transformer
System.out::println); // The nulls are not passed to the consumer
reduce 操作用一个累加函数组合其输入。
例如,可以如下计算所有值的总和:
也可以提供转换函数。
Long sum = map.reduceValues(threshold, Long::sum) ;
// 计算最长的键的长度,使用length函数,累积之前的计算
Integer maxlength = map.reduceKeys(threshold,
String::length, // Transformer
Integer::max) ; // Accumulator
14.7.4 并发Set视图
没有ConcurrentHashSet,可以用ConcurrentHashMap替代。
ConcurrentHashMap的静态方法:
newKeySet
会生成一个Set<K>
Set<String> words = ConcurrentHashMap.<String>newKeySet() ;
如果原来有一个Map对象,可以使用:
Set<String> words = map.keySet(1L); // 默认值1L
words.add("Java");
如果"Java” 在words 中不存在, 现在它会有一个值1。
14.7.5 写数组的拷贝
CopyOnWriteArrayList
和CopyOnWriteArraySet
是线程安全的集合。
修改时对底层进行复制。
构建迭代器时,保含对当前数组的引用。如果修改了,迭代器用的是旧的(可能过时的),但是不会有并发冲突。
14.7.6 并行数组算法
Arrays类提供大量并行化操作。静态Arrays.parallelSort
可对数组排序。
parallelSetAll
:法会用由一个函数计算得到的值填充一个数组。这个函数接收元素索引,然后计算相应位置上的值。
Arrays.parallelSetAll(values, i ->i % 10);
// Fills values with 0 12 3 4 5 6 7 8 9 0 12 . . .
parallelPrefix
:用对应一个给定结合操作的前缀的累加结果替换各个数组元素。【前缀和】!
这个算法在特殊用途硬件上很常用, 使用这些硬件的用户很有创造力,会相应地调整算法来解决各种不同的问题。
14.7.7 较早的线程安全集合
Vector和Hashtable提供了线程安全的实现,现在被弃用了。
取而代之是ArrayList和HashMap,这些线程不安全,用同步包装器(synchronization wrapper)变成线程安全。
包装后,集合的方法就用了锁保护,提供了线程安全访问。
List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>()) ;
Map<K,V> synchHashMap = Collections.synchronizedMap(new HashMap<K , V>());
如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“ 客户端” 锁定。
最好使用java.util.Concurrent 包中定义的集合, 不使用同步包装器中的。
14.8 Callable与Future
Runnable
封装一个异步任务。
Callable
封装一个带返回值的异步计算任务。
public interface Callable<V> {
V call() throws Exception;
}
Future
就是对于具体的Runnable
或者Callable
任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get
方法获取执行结果,该方法会阻塞直到任务返回结果。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
在Future接口中声明了5个方法,下面依次解释每个方法的作用:
- cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数
mayInterruptIfRunning
表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning
为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning
设置为true,则返回true,若mayInterruptIfRunning
设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning
为true还是false,肯定返回true。 isCancelled
方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。isDone
方法表示任务是否已经完成,若任务完成,则返回true;get()
方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;get(long timeout, TimeUnit unit)
用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
也就是说Future提供了三种功能:
6. 判断任务是否完成;
7. 能够中断任务;
8. 能够获取任务执行结果。
FutureTask
包装器是一种非常便利的机制, 可将Callable 转换成Future 和Runnable, 它同时实现二者的接口。
事实上,FutureTask
是Future
接口的一个唯一实现类。
Callable<Integer> myComputation = . . .;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
// task同时实现了Future和Runnable接口
// 作为Runnable
new Thread(task).start();
// 作为Future
Integer result = task.get();
一般搭配执行器用。
14.9 执行器
执行器是类名,实现的是线程池的概念。
上一节提到Callable,那么怎么使用Callable呢?一般情况下是配合ExecutorService来使用的,在ExecutorService 接口 中声明了若干个submit方法的重载版本:
java.util.concurrent.ExecutorService
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
// 可以使用这样一个对象Future<?>来调用
// isDone、cancel 或isCancelled。但是,它的get方法在完成的时候只是简单地返回null。
void shutdown()
// 关闭服务,会先完成已经提交的任务而不再接收新的任务。
第一个submit方法里面的参数类型就是Callable。Callable一般是和ExecutorService配合来使用的。一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。
类继承结构:
class ThreadPoolExecutor:
java.lang.Object
java.util.concurrent.AbstractExecutorService
java.util.concurrent.ThreadPoolExecutor
// All Implemented Interfaces:
interface Executor, ExecutorService
// Direct Known Subclasses:
class ScheduledThreadPoolExecutor
接口:
interface ExecutorService
// All Superinterfaces:
interface Executor
// All Known Subinterfaces:
interface ScheduledExecutorService
// All Known Implementing Classes:
class AbstractExecutorService, ForkJoinPool, ScheduledThreadPoolExecutor, ThreadPoolExecutor
几个常用的创建线程池的静态方法 of class ThreadPoolExecutor
ExecutorService newCachedThreadPool()
// 带缓存,必要时创建,60秒后终止
ExecutorService newFixedThreadPool(int threads)
// 线程数有参数决定
ExecutorService newSingleThreadExecutor()
// 单个线程,依次执行
下面总结了在使用连接池时应该做的事:
1 ) 调用Executors 类中静态的方法newCachedThreadPool 或newFixedThreadPool。
2 ) 调用submit 提交Runnable 或Callable 对象。
3 ) 如果想要取消一个任务,或如果提交Callable 对象,那就要保存好返回的Future对象。
4 ) 当不再提交任何任务时,调用shutdown。
ScheduledThreadPool
还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool
.
创建一个ScheduledThreadPool仍然是通过Executors类:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
我们可以提交一次性任务,它会在指定延迟后只执行一次:
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
如果任务以固定的每3秒执行,我们可以这样写:
// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
如果任务以固定的3秒为间隔执行,我们可以这样写:
// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
14.9.3 控制任务组
invokeAny:完成一个任务就算完成(类似于搜索)
invokeAll:完成所有任务。
ExecutorCompletionService:按可获得的顺序保存。
java.util.concurrent.ExecutorCompletionService<V> // class
ExecutorCompletionService(Executor executor)
// 构造函数:构建一个执行器完成服务来收集给定执行器的结果。
Future<V> poll()
Future<V> submit(Callable<V> task)
Future<V> take()
14.9.4 Fork-Join框架
设计模式:模板方法 template method
Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。
Fork/Join线程池在Java标准库中就有应用。Java标准库提供的java.util.Arrays.parallelSort(array)
可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。
需要继承RecursiveTask<T>
// 重写 T compute()
class SumTask extends RecursiveTask<Long> {
@Override
protected Integer compute()
{
if (to - from < THRESHOLD)
{
// 小于某个阈值,直接计算
solve problem directly
}
else
{
// 大于某个阈值,递归计算并合并
int mid = (from + to) / 2;
Counter first = new Counter(va1ues, from, mid, filter) ;
Counter second = new Counter(va1ues, mid, to, filter);
invokeAll(first, second):
return first.join() + second.join();
}
}
}
14.9.5 可完成的Future
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true,这两种方法都不是很好,因为主线程也会被迫等待。
类似于Transaction
,表达一系列前后相关的事务。
CompletableFuture的优点是:
异步任务结束时,会自动回调某个对象的方法;
异步任务出错时,会自动回调某个对象的方法;
主线程设置好回调后,不再关心异步任务的执行。
CompletableFuture更强大的功能是,多个CompletableFuture可以串行执行,例如,定义两个CompletableFuture,第一个CompletableFuture根据证券名称查询证券代码,第二个CompletableFuture根据证券代码查询证券价格,
public static void main(String[] args) throws Exception {
// 第一个任务:
CompletableFuture<String> cfQuery = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油");
});
// cfQuery成功后继续执行下一个任务:
CompletableFuture<Double> cfFetch = cfQuery.thenApplyAsync((code) -> {
return fetchPrice(code);
});
// cfFetch成功后打印结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(2000);
}
还可以用anyOf
并行执行,类似于Transaction
里的concurrent
public static void main(String[] args) throws Exception {
// 两个CompletableFuture执行异步查询:
CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://finance.sina.com.cn/code/");
});
CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
// 两个CompletableFuture执行异步查询:
CompletableFuture<Double> cfFetchFromSina = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://finance.sina.com.cn/price/");
});
CompletableFuture<Double> cfFetchFrom163 = cfQuery.thenApplyAsync((code) -> {
return fetchPrice((String) code, "https://money.163.com/price/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfFetch = CompletableFuture.anyOf(cfFetchFromSina, cfFetchFrom163);
// 最终结果:
cfFetch.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
除了anyOf()
可以实现“任意个CompletableFuture
只要一个成功”,allOf()
可以实现“所有CompletableFuture
都必须成功”,这些组合操作可以实现非常复杂的异步流程控制。
最后我们注意CompletableFuture
的命名规则:
xxx()
:表示该方法将继续在已有的线程中执行;
xxxAsync()
:表示将异步在线程池中执行。
利用可完成future
,可以指定你希望做什么, 以及希望以什么顺序执行这些工作。当然,这不会立即发生,不过重要的是所有代码都放在一处。
可以为CompletableFuture<T>
对象增加一个动作,或者组合多个组合对象。
表:为CompletableFuture 对象增加一个动作:
方法 | 参数 | 描述 |
---|---|---|
thenApply | T-> U | 对结果应用一个函数 |
thenCompose | T-> CompletableFuture<U> | 对结果调用函数并执行返回的future |
handle | (T, Throwable) -> U | 处理结果或错误 |
thenAccept | T-> void | 类似于thenApply, 不过结果为void |
whenComplete | (T, Throwable) -> void | 类似于handle, 不过结果为void |
thenRun | Runnable | 执行Runnable, 结果为void |
表:组合多个CompletableFuture对象
方法 | 参数 | 描述 |
---|---|---|
thenCombine | CompletableFuture<U>, (T, U) -> V | 执行两个动作并用给定函数组合结果 |
thenAcceptBoth | CompletableFuture<U>, (T, U) -> void | 与thenCombine 类似, 不过结果为void |
runAfterBoth | CompletableFuture<?>, Runnable | 两个都完成后执行_able |
applyToEither | CompletableFuture<T>, T-> V | 得到其中一个的结果时, 传入给定的函数 |
acceptEither | CompletableFuture<T>, T-> void | 与applyToEither 类似, 不过结果为void |
runAfterEither | CompletableFuture<?>, Runnable | 其中一个完成后执行runnable |
static allOf | CompletableFuture<?>... | 所有给定的future 都完成后完成, 结果为void |
static anyOf | CompletableFuture<?>... | 任意给定的future 完成后则完成, 结果为void |
14.10 同步器
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
类 | 它能做什么 | 说明 |
---|---|---|
CyclicBarrier | 允许线程集等待直至其中预定数目的线程到达一个公共障栅( barrier),然后可以选择执行一个处理障栅的动作 | 当大量的线程需要在它们的结果可用之完成时 |
Phaser | 类似于循环障栅, 不过有一个可变的计数 | Java SE 7 中引人 |
CountDownLatch | 允许线程集等待直到计数器减为0 | 当一个或多个线程需要等待直到指定数目的事件发生 |
Exchanger | 允许两个线程在要交换的对象准备好时交换对象 | 当两个线程工作在同一数据结构的两个实例上的时候, 一个向实例添加数据而另一个从实例清除数据 |
Semaphore | 允许线程集等待直到被允许继续运行为止 | 限制访问资源的线程总数。如果许可数是1,常常阻塞线程直到另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显式同步的情况下, 当两个线程准备好将一个对象从一个线程传递到另一个时 |
CountDownLatch
:利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。类似golang的WaitGroup。构造参数指定任务个数,countDown()
表达完成了一个任务,await()
阻塞在某处。
CyclicBarrier
:实现让一组线程等待至某个状态之后再全部同时执行。barrierAction
为当这些线程都达到barrier状态时会执行的内容。可重用。
Semaphore
:信号量。acquire()
用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。release()
用来释放许可。注意,在释放许可之前,必须先获获得许可。
下面对上面说的三个辅助类进行一个总结:
1)CountDownLatch
和CyclicBarrier
都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch
一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier
一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch
是不能够重用的,而CyclicBarrier
是可以重用的。
2)Semaphore
其实和锁有点类似,它一般用于控制对某组资源的访问权限。