【有书共读】《JAVA并发编程实战》第5章+第6章
JAVA并发实战学习笔记
第五章 基础构建模块
-
同步容器类:Collections.synchronized**()方法得到的容器类对应的装饰器类,或者将容器类封装在代码中,或者Hashtable、Vector类,并对所有容器的访问操作都加锁。特点是没有对锁细粒度划分,所有对容器的加锁独占,访问串行进行。
- 古老的Vecter、HashMap容器类是线程安全的,但是复合操作(如:迭代、条件运算等)需要自行加锁
- Vecter、HashMap容器类允许客户端加锁
- 现代的容器类:
- 复合操作依旧不是线程安全的
- 支持客户端加锁
- 加锁的缺点:
- 迭代费时长,使其它线程长时间等待,降低吞吐率
- 饥饿、死锁
- 替代方案:克隆容器,在线程内部的副本容器上操作
-
<font color=#ff0000 size=5 face="黑体">如果想要将操作结果写回原容器的话,会出现不一致问题吧???</font>
-
- 加锁的缺点:
- 迭代器
- 迭代期间,迭代器会自行检查容器实例有没有被更改,若有更改,则抛出ConcurrentModificationException,但是该检查是非同步的,不能依赖这个检查。
- 一些隐藏在方法内部的迭代器容易被忽略,从而没有被同步。如: Set.toString()、hashCode()、addAll()、removeAll()、equals() 、contains() 方法遍历set中每一个元素拼接成字符串。
- 1.使用同步包装的容器
- 2.加锁————应兼执行这两种操作来保证线程安全
- 古老的Vecter、HashMap容器类是线程安全的,但是复合操作(如:迭代、条件运算等)需要自行加锁
-
并发容器类:与同步容器类对应,同步容器类将对容器状态的访问都串行化,吞吐率低。并发容器类针对并发访问设计。
- Queue(非阻塞)————基于 LinkedList实现
- ConcurrentLinekdQueue————并发,先进先出
- PriorityQueue ————非并发
- BlockingQueue(阻塞的)————
- 并发;消费者-生产者模式
- Map
- HashMap————基于散列,导致HashMap.contains()方法可能十分费时
- ConcurrentMap
- ConcurrentHashMap————基于散列
- 多粒度的分段锁,支持任意数量的同时读和一定数量的同时写。
- 不能客户端加锁以求得独占访问。支持对“若没有则添加”等符合操作的原子性
- 特点:
- 返回的迭代器有弱一致性,不保证会在迭代器构造后将修改操作反映给容器
- size(),isEmpty()方法均是估计值,非精确值
- 在并发环境中,以上两个特点是对增大并发支持的合理权衡。当需要独占访问时,则应该使用
- CopyOnWriteArrayList
- 当我们往容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
- 适用于对容器的修改远少于对容器的迭代操作的情况
- Deque————对Queue的扩展,双端队列,适用于工作密取模式。
- Queue(非阻塞)————基于 LinkedList实现
- 消费者-生产者模式
- 优点/特点
- 实现了消费者和生产者的解耦
- 常见的消费者-生产者设计模式是将线程池与工作队列组合,在Executor任务执行框架中就体现了这种模式。
- 将有界阻塞队列用于生产者消费者模式,可以作为一种资源管理的机制,使程序在高负荷的情况下健壮运行。
- 提高性能:如果生产者和消费者的并行度不不一样,将它们紧密耦合的并行度会是两者的最小并行度,而将它们用于该种模式,则会提高并行度。
- BlockingQueue
- ArrayBlockingQueue
- LinkedBlockingQueue ————两者的并发性能分别比同步的ArrayList和LinkedList更好
- PriorityBlockingQueue
- SynchronousBlockingQueue
- 不提供存储空间,消费者线程准备好交付时,生产者线程将工作直接给消费者
- put,take方***一直阻塞
- 串行线程封闭
- 串行线程封闭实际是指将一个自己线程内封闭的对象从一个线程(发布)“移交”给另一个线程,实现所有权的移交,即发布的对象不再访问这个对象。新线程可以对得到的对象做任何的修改。
- 通过阻塞队列机制可以实现这种串行线程封闭。
- 工作密取
- Dequeue————每一个消费者有专属的工作队列,从头部取工作;若自己的工作队列已经空,则去其它消费者的双端队列尾部取工作。
- 降低了获取工作时的竞争
- 每个消费者会确保处于忙碌状态
- Dequeue————每一个消费者有专属的工作队列,从头部取工作;若自己的工作队列已经空,则去其它消费者的双端队列尾部取工作。
- 优点/特点
- 阻塞方法与中断
- 阻塞方法的返回时间较无法预测,有可能会出现一直返回不了的情况。因此阻塞方法支持被其它线程中断它,这时它将不再等待外部事件,提前返回,被中断的事情发生的时候,会抛出InterruptedException异常,说明该线程被中断,所以阻塞方法一般都会有受检查异常InterruptedException需要处理。
- 中断发生,有三种做法
- 继续向上抛出异常
- 执行Thread.currentThread.interrupt()方法,使当前线程重新进入中断状态,以让上层的代码看到该中断。————这种做法通常出现在Runnable的方法中
- 捕获该异常,但是不做任何处理。——————<font face = "黑体" size=5 color=#ff0000>×</font> 此种做法很危险呐
- 同步工具类 (与并发容器类概念不一样哦)
- 闭锁(CountdownLatch————意思是定时器闭锁)
- 一个有助于理解闭锁的博客:<a>https://baijiahao.baidu.com/s?id=1594367204126688971&wfr=spider&for=pc</a>
- 适用于:游戏等待玩家全部就绪再开始执行;某个计算在所有需要的资源都初始化完成之后再开始执行等情况
- 使用方法:
- 初始化CountdownLatch(int)即初始化定时器计数值
- 被等待的线程在完成某个操作后便执行 CountdownLatch.countDown()方法,定时器减一
- 等待的线程执行 CountdownLatch.await(),处于阻塞状态,等到这个闭锁对象的计时器减到0,等待线程被唤醒
- 初始化的倒计时器计时到0后便不再使用,即不能重复倒计时————一次性的闭锁功能
- FutureTask类
- 带有返回值的任务类:FutureTask<V>类(用实现Callable接口的类实例初始化),返回值类型即为V
- FutureTask.get()是一个阻塞方法,其所有异常都抛出放入ExecutionException中,里面可能包含Error,RuntimeException和待检异常三种,需逐步判断类型
- FutureTask类本身不是线程,只是提供对线程中的任务的管理,具有等待运行,正在运行,运行完成三个状态。提供对状态的查询isDone(),isCanceled()方法;对任务执行返回值的获取方法get();取消当前任务的方法cancel()。
- FutureTask类中的任务若要执行,需要 new Thread(FutureTask()),或者ExecutorService.submit(FutureTask task)
- 下列代码实现了从Future任务中取得结果,若任务未在规定时间内执行完,则取消任务
<pre><code>
res = future.get(timeLimit, timeUnit);
catch(TimeoutException e){
future.cancel(true);
}
</code></pre>
- 信号量 Semaphore
- 可以为本身线程安全的容器类添加一个边界限制
- acquire(),release()方法分别获取、释放信号量。acquire是阻塞方法
- 闭锁(CountdownLatch————意思是定时器闭锁)
- Barrier(栅栏)
- CyclicBarrier
- 一次阻拦一批
- 栅栏 CyclicBarrier
- Object类中的阻塞机制
- Object obj对象,其方法 obj.wait(),obj.notify,obj.notifyAll方法调用都需要被synchronized(Object obj){}代码块包围,否则会出异常呢,原因可能因为Object.wait()没有同步机制;
- wait()方法执行后,该线程让出对象锁,进入 Blocked 阻塞状态,当被notify()或者notifyAll()方法唤醒后,则进入 wait状态,在等待队列中请求原先的对象锁。
- 补充一:
- 线程状态
- new:调用start后进入runnable
- runnable:只有runnable状态会被分配cpu时间,对应os的ready+running
- waiting:等待对应的notify
- blocked:获取对应的锁后进入runnable状态
- timed waiting:到时后会继续runnable
- terminated
- 一些同步机制
- object.wait()
- 调用时需持有object的锁
- 释放当前持有的所有锁
- 当前线程进入waiting状态
- 注:加参数则进入timed waiting
- object.notify()
- 调用时需持有object的锁
- 随机选取一个object上wait/timed waiting的线程A
- A置为blocked状态
- object.notifyAll()
- 调用时需持有object的锁
- 将所有在object上wait/timed waiting的线程置为blocked状态
- Thread.sleep
- 将当前线程置为timed waiting状态
- 注:没有在某个object上wait所以无法notify
- synchronized(object)
- 将当前线程置为blocked,等待object的锁
- thread.interrupt
- 如果thread在waiting/timed waiting
- 将线程置为runnable状态
- wait/sleep函数抛出InterruptedException
- 注:不会自动修改isInterrupted
- 如果不在
- 将的isInterrupted置为true
- 如果thread在waiting/timed waiting
- object.wait()
- 线程状态
- 补充二
- 自旋锁
- 在循环中,不断地测试是否满足状态,满足了再作对应操作。例如:AtomicReference<T> 中的 compareAndSet(T expect, T update) 方法。
- 但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。
- 阻塞锁
- 阻塞锁改变了线程的运行状态,让线程进入阻塞状态进行等待,当获得相应的信号(唤醒或者时间)时,才可以进入线程的准备就绪状态,转呗就绪状态的所有线程,通过竞争,进入运行状态。 如: synchronized使得线程进入blocked状态。
- 可重入锁
- 和阻塞所不是冲突的概念,synchronized即是可重入锁也是阻塞锁。
- ReentrantLock和synchronized相比,可以定制很多功能,如:
- tryLock()有时间锁等待,如果失败则会释放以获得的锁
- lockInterruptibly()可中断锁等待
- Reentrant.newCondition()生成的两个条件对象,可以通过await()和signal()方法协调线程之间的挂起、唤醒。类似于 synchronized<>
<pre><code>
synchronized(object){
if(!a){
object.wait;
}
}
</code></pre>
- 轻量级锁,重量级锁,偏向锁——————这三种锁的状态信息均存储在对象头中,成为"Mark Word",32bit或者64bit,如下表所示
- 自旋锁
第六章 任务执行
- 任务:任务之间应尽量相互独立,不依赖于其它任务的状态,结果
- 任务执行的方式
- 串行:因I/O操作或者数据库连接等产生阻塞,用户响应慢,阻塞过长甚至可能使服务器完全不可用
- 显示为任务创建线程:
- 在主线程中循环接受请求,分发每一个请求到一个新线程中执行,提高了程序吞吐率
- 缺点:
- 1 创建线程需较多的计算,消耗时间;若请求频繁且需要的处理时间不多,则创建线程的时间消耗占比太多
- 2 内存,cpu资源有限,线程太多增大了jvm负担,降低了整体性能
- 3 超出OS,jvm对线程的限制,产生OutOfMemoryError
- 4 以上三点说明线程数量需要控制在合理范围内才不至于降低系统性能
- 线程池
- Executor ————接口,带有 public void exec(Runnable runnable) 方法
- java.util.concurrent中提供的线程池作为Executor框架中的一部分
- Executor,即线程池,基于生产者-消费者模式。执行 Executor.exec()方法的线程是生产者,执行Executor内部Runnable任务的线程则为消费者
- Executors工厂方法如 newFixedThreadPool(int) 用于产生不同形式的线程池ExecutorService(是Executor的子接口)
- 四种工厂方法
- newFixedThreadPool
- 创建规定数量的线程
- newCachedThreadPool
- 线程数量随需求增减
- newSingleThreadExecutor
- 只有一个线程,若意外结束则会新创建另一个线程
- newScheduledPool
- 线程数量固定,以Timer(延迟、周期)的形式执行任务
- Timer.schedule(new TimerTask(){public void run(){}}, 5); 可以使任务5秒后执行,多个任务schedule都将由同一个线程执行,且遇到异常终止不会重新创建线程,且会出现多个任务相互影响无法实现定时、周期的功能。
- newSchedulePool可以解决上述问题
- newFixedThreadPool
- 四种工厂方法
- ExecutorService——————类扩展了Executor,提供对线程池状态的管理
- ExecutorService可以获取线程池的状态,有三种状态:运行,关闭状态,已终止状态
- 关闭分为两种形式,其一需要等待线程池中的其他已经开始的任务都运行结束;其二则是粗暴结束的方式
- isShutDown, shutDown, shutDownNow等方法
- ExecutorService的 submit()方法将任务加入任务队列以待执行,同时返回 Future<V>类型的实例
- CompletionService
- 构造方法可以以Executor作为参数
- take()方法从阻塞队列中取出最近完成的任务的返回值。
- 只有大量独立同构的任务才能带来真正的性能提升
- 如果各个任务的任务量分配不均提升不会很大
- Executor ————接口,带有 public void exec(Runnable runnable) 方法