wanchaopeng 2019-06-25
转载请注明出处 http://www.paraller.com
原文排版地址 http://www.paraller.com/2016/02/03/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E6%88%98%E7%AC%94%E8%AE%B0%284%29-%E5%9F%BA%E7%A1%80%E6%9E%84%E5%BB%BA%E6%A8%A1%E5%9D%97/
主要介绍Java类库中并发基础构建模块 & 常用模式
这些容器类的关键在于:将他们的状态封装起来,并对每个公有方法进行同步,使得每次只有一个线程可以访问容器的状态
复合操作需要同步:包括 迭代、跳转、条件运算,虽然每个接口只能一个线程操作,但是存在线程交替操作
的情况,所以不能保证没有其他线程修改状态
解决:正确的客户端加锁
在迭代器场景中,如果有状态被修改将会抛出 ConcurrentModificationException 并发修改异常
原理:将计数器的变化与容器关联起来,如果容器在迭代,计数器被修改,那么hasNext和next操作会抛出异常。缺点:对检查并没有同步 ; 原因:对性能的权衡妥协
方案:客户端加锁,等待时间过长。使用克隆
容器的方式,在克隆的时候要加锁, 克隆会造成性能开销,需要从调用频率和响应时间、容器大小等考虑
隐藏的迭代器:
如果状态与保护她的同步代码之间相隔越远,开发者就越容易忘记在访问状态时使用同步
避免下述这种情况的方式: 使用 synchronizedSet来替代并且对同步代码进行封装
Set<Integer> demoSet = new HashSet(); public void doSome(){ system.out.println("value is "+ demoSet); }
hasCode / equals / containsAll / retainAll / removeAll 等方法,以及把容器作为构造器参数的场景, 都会对容器进行迭代。都有可能抛出并发修改异常。
设计的原因:提高伸缩性并降低风险
同步容器类在执行每一个操作都会持有一个锁,而像迭代这样的操作会耗费很长的时间,导致其他线程挂起。
分段锁(Lock Striping)
,后面章节会介绍。迭代期间不需要加锁或复制,不会抛出并发修改异常
每次修改容器都会复制底层数组
,当容器规模较大的时候需要较大开销,适用于 迭代操作远多于修改操作,比如 servlet过滤器/监听器注册表线程池
、工作队列、Executor任务执行框架双端队列: 在队列头和队列尾高效插入和移除。包含 Deque
和BlockingDeque
两种类型
工作密取模式:
InterruptedException:
当代码中调用了一个可能 抛出InterruptedException
的方法时,你自己的代码也变成了阻塞方法
,并且必须要处理
对中断的响应。响应的方式有两种:
class Demo implements Runnalbel{ public void run(){ try{ queue.take(); }catch{ Thread.currentThread().interrupt(); } } }
控制流
,任何对象都可以是同步工具类可以延迟线程的进度(阻塞进程,等待时机释放),直到闭锁到达终止状态,闭锁到达终止状态将不再更新状态; 可以用来确保某些活动在其他活动完成之后才执行。常见场景:
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(); }
相当于一种可生成结果的 Runnable
,可以处于三种状态, 等待运行 、 正在运行 、 运行完成;运行完成包括三种:正常结束、取消而结束、异常结束class Demo{ FutureTask<Product> future = new FutureTask<Product>(new Callable<Product>(){ 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); } } } }
ExecutionException
中,并在调用get方法的时候抛出;ExecutionException作为 Throwable类返回,可能的情况比较复杂需要额外处理对异常的处理,逻辑如下:
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); } }
定义:计数信号量用来控制同时访问:
原理:
适用场景:
class Demo{ private final Semaphore sem = new Semaphore(2); public void test(){ sem.acquire(); ... sem.release(); } }
栅栏与闭锁有许多共性,一些区别如下:
闭锁可以控制一组相关操作,进入终止状态不能重置 | 栅栏状态可以重置
闭锁用于等待事件 | 栅栏用于等待其他线程
闭锁用于所有线程等待一个外部事件
的发生 | 栅栏则是所有线程相互等待
,直到所有线程都到达某一点
时才打开栅栏,然后线程可以继续执行
单向栅栏:
常见场景:将问题分解成几个计算子任务,流程如下:
BrokenBarrierException
双向栅栏:
另一种形式的栅栏是 Exchanger: 当两方执行不对称操作的时候,例如一个线程向缓冲区写入数据,另一个线程向缓存区获取数据。这些线程可以用 Exchanger来汇合,将满的缓冲区和空的缓冲区进行交换。