java 并发编程实战 笔记_Java并发编程实战笔记(4)-基础构建模块

主要介绍Java类库中并发基础构建模块 & 常用模式

同步容器类

这些容器类的关键在于:将他们的状态封装起来,并对每个公有方法进行同步,使得每次只有一个线程可以访问容器的状态

存在的问题

复合操作需要同步:包括 迭代、跳转、条件运算,虽然每个接口只能一个线程操作,但是存在线程交替操作的情况,所以不能保证没有其他线程修改状态

解决:正确的客户端加锁

迭代器 与 ConcurrentModificationException

在迭代器场景中,如果有状态被修改将会抛出 ConcurrentModificationException 并发修改异常

原理:将计数器的变化与容器关联起来,如果容器在迭代,计数器被修改,那么hasNext和next操作会抛出异常。缺点:对检查并没有同步 ; 原因:对性能的权衡妥协

方案:客户端加锁,等待时间过长。使用克隆容器的方式,在克隆的时候要加锁, 克隆会造成性能开销,需要从调用频率和响应时间、容器大小等考虑

隐藏的迭代器:

如果状态与保护她的同步代码之间相隔越远,开发者就越容易忘记在访问状态时使用同步

避免下述这种情况的方式: 使用 synchronizedSet来替代并且对同步代码进行封装

Set demoSet = new HashSet();

public void doSome(){

system.out.println("value is "+ demoSet);

}

hasCode / equals / containsAll / retainAll / removeAll 等方法,以及把容器作为构造器参数的场景, 都会对容器进行迭代。都有可能抛出并发修改异常。

并发容器类

设计的原因:提高伸缩性并降低风险

改进同步容器类的性能: 同步容器类将所有对容器状态的访问都串行化, 以实现线程安全,在大并发的情况下吞吐量将严重降低。

针对多个并发访问增加了一些特性: 例如条件运算的复合操作, 不会产生并发修改异常的迭代器

ConcurrentHashMap

同步容器类在执行每一个操作都会持有一个锁,而像迭代这样的操作会耗费很长的时间,导致其他线程挂起。

并发容器类在锁的粒度方面做了改进。这种机制称为 分段锁(Lock Striping),后面章节会介绍。

迭代器操作具有弱一致性,可以容忍并发修改。在迭代器被创建后,出现修改的操作会反映给容器。(但是不保证)

ConcurrentHashMap 相对于同步Map有更多的优势和更少的劣势,只有当需要对Map进行独占操作时,才放弃对ConcurrentHashMap的使用。

CopyOnWriteArrayList

迭代期间不需要加锁或复制,不会抛出并发修改异常

线程安全性在于:只要正确的发布一个事实不可变的CopyOnWriteArrayList对象,那么访问该对象时就不再需要进一步的同步(这个不太懂。。。)

每次修改时都会创建并重新发布一个新的容器副本

CopyOnWriteArrayList容器保留一个指向底层基础数组的引用,这个数组位于迭代器的起始位置,不会被修改(不支持可变 remove等操作),对其进行同步时只需要确保数组内容的可见性

开销:每次修改容器都会复制底层数组,当容器规模较大的时候需要较大开销,适用于 迭代操作远多于修改操作,比如 servlet过滤器/监听器注册表

阻塞队列 和 消费者-生产者模式

将复杂操作分解成两个组建,简化业务逻辑

常见场景:线程池、工作队列、Executor任务执行框架

当队列为空,take操作会阻塞,特别适合 服务器接收请求的场景

阻塞队列提供了 offer方法,当数据不能被添加到队列中,那么将返回一个失败状态,能够灵活的自定义处理负载的策略;

设置工作队列的时候尽量使用有界队列,防止架构更改

特殊的“队列”,并不会维护空间,直接交付给消费者,没有存储和FIFO操作

双端队列 与 工作密取(Work stealing)

双端队列: 在队列头和队列尾高效插入和移除。包含 Deque和BlockingDeque两种类型

工作密取模式:

为了高效:每个消费者都有自己的双端队列,当自己的队列任务完成之后,会从其他消费者的 双端队列的尾部(减少竞争关系)获取任务消费

既是消费者又是生产者的模式: 比如网络爬虫,在解析到网页内容的时候发现了其他超链接,将链接加到队列的尾部;其他场景:搜索图算法。

阻塞方法 和 中断方法

线程会因为一些原因阻塞,比如 IO操作、等待获得一个锁、从Thread.sleep中醒来等。

阻塞状态包括(BLOCKED / WAITING / TIMED_WAITING) , 运行状态(RUNNABLE)

InterruptedException:

受检查异常。表示一个阻塞方法被中断,它将努力提前结束阻塞状态

Interrupt方法,Thread中特有的: 用于中断线程 或者 查询线程是否中断

中断是一种协作机制,一个A线程不能强制另一个B线程马上中断,只能要求B线程在可以中断的地方停止它的操作(前提是B愿意停下来)。

当代码中调用了一个可能 抛出InterruptedException的方法时,你自己的代码也变成了阻塞方法,并且必须要处理对中断的响应。响应的方式有两种:

传递InterruptedException,避开异常,直接抛出给方法的调用者处理,或者捕获异常作简单的处理再抛出异常。

保持中断状态(恢复中断):有时不能直接抛出异常,比如当代码是 Runnable的一部分时。处理方式: 捕获异常,保持线程的中断状态,在调用栈中的更高层代码将看到该线程引发了中断

class Demo implements Runnalbel{

public void run(){

try{

queue.take();

}catch{

Thread.currentThread().interrupt();

}

}

}

同步工具类

只要对象可以根据自身的状态来协调线程的控制流,任何对象都可以是同步工具类

工具类包含一些特定的结构化属性:1、封装一些状态。这些状态将决定线程的执行阶段; 2、提供方法对状态进行操作; 3、一些方法用于高效的等待同步工具类进入到预期状态?

常见的工具类包括: 队列 / 信号量(semaphore) / 栅栏(Barrier) / 闭锁(Latch)

闭锁(Latch)

可以延迟线程的进度(阻塞进程,等待时机释放),直到闭锁到达终止状态,闭锁到达终止状态将不再更新状态; 可以用来确保某些活动在其他活动完成之后才执行。常见场景:

王者荣耀中兵线的初始化线程A,需要所有玩家加载游戏进度完成的线程都结束后,才会开始A

服务在其依赖的所有服务启动完成之后再启动

CountDownLatch是一种闭锁实现,包括一个计数器,需要等待的时间数量,countDown方法减少计数值,await方法一直阻塞直到计数器为0,或者事件中断或超时

void test(){

final CountDownLatch cd = new CountDownLatch(1);

for(int i = 0 ;i < 10; i++){

Thread thread = new Thread(){

public void run(){

cd.await();

...

}

}

}

cd.countDown();

}

闭锁(FutureTask)

FutureTask实现了Future的语义,表示一种可生成结果的计算(通过Callable类实现);

FutureTask相当于一种可生成结果的 Runnable,可以处于三种状态, 等待运行 、 正在运行 、 运行完成;运行完成包括三种:正常结束、取消而结束、异常结束

Future.get方法:如果任务完成马上返回结果,否则一直阻塞直到完成或者异常结束。结果从执行线程返回给获取结果的这个线程

FutureTask在Executor框架中表示异步任务; 此外还可以用来表示时间较长的计算,可以在使用结果之前启动

class Demo{

FutureTask future = new FutureTask(new Callable(){

public Prodcut call() throws LoadException{

return db.load();

}

});

Thread thread = new Thread(future);

void start(){

thread.start();

}

Product getPro() throws LoadException,InterruptedException{

try{

return future.get();

}catch(ExecutionException e){

Throwable th = e.getCause();

if(th instanceOf LoadException){

throw LoadException

}else{

throw launderThrowable(cause);

}

}

}

}

A线程任务未完成能够阻塞当前B线程,实现控制流

Callable表示的任务可以抛出 受检查或者未受检查的异常, 并且任务代码都可能抛出一个Error

无论代码中抛出什么异常,都会被封装到ExecutionException中,并在调用get方法的时候抛出;ExecutionException作为 Throwable类返回,可能的情况比较复杂需要额外处理

如果任务被取消,Get方法将会抛出 CancellationException

对异常的处理,逻辑如下:

检查已知的受检查异常,并重新抛出

Error:重新抛出

RuntimeException: 重新抛出

抛出 IllegalStateException 表示这是一个漏记错误

RuntimeException launderThrowable(Throwable cause){

if(cause instanceOf RuntimeException){

return (RuntimeException) cause;

}else if(cause instanceOf Error){

throw (Error) cause;

}else{

throw new IllegalStateException("checked",cause);

}

}

信号量

定义:计数信号量用来控制同时访问:

某个特定资源的操作数量

某个指定操作的数量

可以用来实现 资源池、容器的施加边界

原理:

Semaphore管理者一组虚拟许可(permit),初始数量在构造器中指定; 在执行操作的时候先获得许可,使用完成之后进行释放

获取许可使用 acquire方法(理解为消费),如果没有可用的许可将会阻塞。 release方法返回一个许可(理解为创建)

Semaphore 不会与线程关联起来,A线程消费的信号可以在B线程创建,

信号量不受限于构造过程中的创建的许可数量

适用场景:

实现资源池,比如数据库连接,固定初始大小,每次获取需要阻塞等待资源(最好用 BlockingDueue实现)

限定容器的大小。在容器外使用装饰器模式,并且在添加和删除的时候使用 Semaphore

class Demo{

private final Semaphore sem = new Semaphore(2);

public void test(){

sem.acquire();

...

sem.release();

}

}

栅栏(Barrier)

栅栏与闭锁有许多共性,一些区别如下:

闭锁可以控制一组相关操作,进入终止状态不能重置 | 栅栏状态可以重置

闭锁用于等待事件 | 栅栏用于等待其他线程

闭锁用于所有线程等待一个外部事件的发生 | 栅栏则是所有线程相互等待,直到所有线程都到达某一点时才打开栅栏,然后线程可以继续执行

单向栅栏:

常见场景:将问题分解成几个计算子任务,流程如下:

当线程到达栅栏位置的时候,将会调用await方法,这个方法将阻塞直到所有的线程都到达栅栏的位置

如果所有线程到达,栅栏将会打开,此时所有的线程将被释放,而栅栏将被重置等待下次使用

如果对调用await方法超时,或者await阻塞的线程被中断,栅栏的完整性将被打破,所有阻塞的await调用都将终止并抛出BrokenBarrierException

如果成功通过,await方法将会返回一个唯一索引,后续程序可以通过这个索引选举 leader,来执行一些特殊的操作

双向栅栏:

另一种形式的栅栏是 Exchanger: 当两方执行不对称操作的时候,例如一个线程向缓冲区写入数据,另一个线程向缓存区获取数据。这些线程可以用 Exchanger来汇合,将满的缓冲区和空的缓冲区进行交换。

练习,构建一个缓存,待完善。。。。

参考网站

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值