java核心技术 基础知识<14章 并发 part2>

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 写数组的拷贝

CopyOnWriteArrayListCopyOnWriteArraySet 是线程安全的集合。
修改时对底层进行复制
构建迭代器时,保含对当前数组的引用。如果修改了,迭代器用的是旧的(可能过时的),但是不会有并发冲突。

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个方法,下面依次解释每个方法的作用:

  1. cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  2. isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  3. isDone方法表示任务是否已经完成,若任务完成,则返回true;
  4. get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  5. get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

也就是说Future提供了三种功能:
6. 判断任务是否完成;
7. 能够中断任务;
8. 能够获取任务执行结果。

FutureTask包装器是一种非常便利的机制, 可将Callable 转换成Future 和Runnable, 它同时实现二者的接口。
事实上,FutureTaskFuture接口的一个唯一实现类

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 执行器

执行器是类名,实现的是线程池的概念。

Java并发编程:线程池的使用
廖雪峰 使用线程池

上一节提到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 对象增加一个动作:

方法参数描述
thenApplyT-> U对结果应用一个函数
thenComposeT-> CompletableFuture<U>对结果调用函数并执行返回的future
handle(T, Throwable) -> U处理结果或错误
thenAcceptT-> void类似于thenApply, 不过结果为void
whenComplete(T, Throwable) -> void类似于handle, 不过结果为void
thenRunRunnable执行Runnable, 结果为void

表:组合多个CompletableFuture对象

方法参数描述
thenCombineCompletableFuture<U>, (T, U) -> V执行两个动作并用给定函数组合结果
thenAcceptBothCompletableFuture<U>, (T, U) -> void与thenCombine 类似, 不过结果为void
runAfterBothCompletableFuture<?>, Runnable两个都完成后执行_able
applyToEitherCompletableFuture<T>, T-> V得到其中一个的结果时, 传入给定的函数
acceptEitherCompletableFuture<T>, T-> void与applyToEither 类似, 不过结果为void
runAfterEitherCompletableFuture<?>, Runnable其中一个完成后执行runnable
static allOfCompletableFuture<?>...所有给定的future 都完成后完成, 结果为void
static anyOfCompletableFuture<?>...任意给定的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)CountDownLatchCyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
  CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
  而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值