Java并发编程实践
第一章 简介
1.1 并发简史
- 不同进程之间可以通过一些粗粒度的通信机制进行数据交换,包括:套接字,信号处理器,共享内存,信号量以及文件等
- 线程允许在同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,例如内存句柄,文件句柄,但每个线程都有各自的程序计数器,栈以及局部变量等。
1.2 线程带来的优势
- 多核处理器
- 建模简单
- 异步事件简化处理
- 响应灵敏
1.3 线程带来的风险
- 安全性
- 活跃性
某件正确的事情最终会发生, 活跃性问题包括死锁、饥饿、活锁
- 性能问题
CPU将更多的时间花在线程调度而不是线程运行上
第二章 线程安全性
编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的和可变状态的访问
共享意味着可以有多个线程访问
可变意味着变量的值在其生命周期内可以发生变化
Java主要的同步机制包括synchronized关键字,volatile变量,显示锁以及原子变量
如果多个线程访问同一个可变的变量,且没有合适的同步,那么就会出现错误
修复手段:
- 不在线程之间共享该变量
- 将状态变量修改为不可变
- 在访问状态变量时使用同步
设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到一定的帮助
完全由线程安全类构成的程序不一定就是安全的,而在线程安全类中也可以包含非线程安全的类。
线程安全性是一个在代码上使用的术语,但它只是与状态相关的,因此只能应用于封装其状态的整个代码,这可能时一个对象,也可能时整个程序
2.1 什么是线程安全性
线程安全性的核心概念就是正确性。
正确性的含义时,某个类的行为与其规范完全一致。
对于单线程,正确性近似定义为,所见即所知
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类时线程安全的
当多个线程访问某个类时,不管运行时采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步或者协同,这个类都表现出正确的行为,那么就称这个类时线程安全的。
无状态对象一定是线程安全的
2.2 原子性
++操作时一种紧凑的语法,但实际上操作并非原子操作。由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况——竞态条件(Race condition
2.2.1 竞态条件
最常见的竞态条件就是先检查后执行(Check Then Act), 即通过一个可能失效的观测结果来决定下一步的操作
2.2.2 延迟初始化中的竞态条件
@NotThreadSafe
public class LazyInitRace{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance = null){
instance = new ExpensiveObject();
}
return instance;
}
}
竞态条件并不总会产生错误,还需要某种不恰当的执行顺序
2.2.3 复合操作
原子操作是指,对于访问同一个状态的所有操作,这个操作是以原子方式执行的
2.3 加锁机制
要保持状态一致性,就需要在单个原子操作中更新所有相关的状态变量
2.3.1 内置锁
java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)
同步代码块包括两部分
- 一个作为锁的对象引用
- 一个有这个锁保护的代码块
静态的synchronized方法以Class对象作为锁
synchronized(lock) {
//访问或修改由锁保护的共享状态
}
每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)
线程在进入同步代码块之前会自动获得锁,并且在推出代码块时自动释放锁,而不论时通过正常的控制路径退出,还是通过从代码块中抛出异常退出。
获得内置锁的唯一途径就时进入由这个锁保护的同步代码块或方法。
2.3.2 重入
重入意味着获取锁的操作的粒度时线程,而不是调用
这与pthread(POSIX线程)互斥体默认的枷锁行为不同,pthread互斥体的获取操作时以调用为粒度
public class Widget{
public synchronized void doSomethind(){
}
}
public class LoggingWidget extends Widget{
public synchronized void doSomethind(){
System.out.println(this.toString() + ": classing doSomething");
super.doSomething();
}
}
子类改写了父类的synchronized方法,如果没有可重入锁,那么这段代码将产生死锁。
由于Widget和LoggingWidget中doSomething方法都时synchronized方法,因此每个doSomething方法在执行前都会获取Widget上的锁,如果锁时不可重入的,那么在调用super.doSomething时将永远无法获得Widget上的锁。
2.4 用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们常状态变量时由这个锁保护的
对象的内置锁与其状态之间没有内在的关联,虽然大多数类都将内置锁用作一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,并不能你组织其他线程访问该对象,某个线程在获得对象的锁之后,只能组织其他线程获得同一个锁。
每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道时哪一个锁。
一种常见的额加锁约定是,将所有的可变状态都封装在对象内部,并通过对对象的内置锁对所有的可变状态的代码路径进行同步。
当类的不变性条件设计多个状态变量时,还有另一个要求:不变性条件中的每个变量必须由同一个锁来保护。因此可以在单个原子操作中访问或更新这些变量,从而确保不变性条件不被破坏。
2.5 活跃性与性能
两种不同的同步机制不仅会带来混乱,也不会在性能或安全性上带来任何好处。
当执行时间较长的计算或者可能无法快速我弄成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁
第三章 对象的共享
3.1 可见性
在在多个线程执行读写操作时,我们无法保证执行读操作的线程能适时的看到其他线程写入的值。
在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确结论。
3.1.1 失效数据
3.1.2 非原子的64位操作
非volatile类型的64位数值变量, Java内存模型要求变量的读取和写入都是原子操作。对于long和double,虚拟机允许将64位的读取和写入分为两个32位的操作。 当读取一个非volatile的long变量时,如果对这个变量的操作在不同的线程中执行,那么很可能会出现错误
3.1.3 加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值
3.1.4 Volatile变量
- volatile: 编译器和运行时都会注意到这个变量是共享的,因此不会将该变量的操作与其他内存操作一起排序
- volatile时一种比synchronized关键字更轻量级的同步机制
volatile在代码中用来公职状态的可见性,通常比锁的代码更脆弱
volatile的正确用法包括:确保自身状态的可见性,确保他们所引用对象的状态的可见性,以及标识一些重要程序生命周期时间的发生
3.2 发布与逸出
发布 一个对象的意思是指:时对象能够在当前作用域之外的代码中使用
如:将一个指向该对象的引用保存早其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用。或者将引用传递到其他类的方法中。
发布内部状态可能会破坏封装性,并使得程序难以维持不变条件。
如果在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被称为逸出
假定有一个类C,对于C来说,外部方法(Alien method)是指行为并不完全由C来规定的方法,包括其它类中定义的方法以及类中可以被改写的方法
当把一个对象传递给某个外部方式的时候,就相当于发布了这个对象。
当某个对象逸出后,你必须假设由某个类或线程可能会无用该对象。这正是需要使用封装的最主要原因:封装能够使得对程序的正确性进行分析变得可能。
public class ThisEscape{
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
});
}
}
ThisEscape发布EventListener时,也隐含发布了ThisEscape,因为在这个内部类实例中包含了对ThisEscape实力的隐含引用。
不要在构造过程中使this逸出
在构造过程中使this引用逸出的常见错误是,在构造函数中启动一个线程。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法。
//使用工厂方法防止this引用在构造过程中逸出
pubilc class SafeListener{
private final EventListener listener;
private SafeListener(){
listener = new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
}
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(sfae.listener);
return sfae;
}
}
3.3 线程封闭
访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方法就是不共享数据。
Thread Confinement
java语言中并没有强制规定某个变量必须由锁保护,同样在java语言中也无法强制将对象封闭在某个线程中。
java语言提供了一些机制来帮助维持线程封闭性,例如局部变量ThreadLocal类
3.3.1 Ad-hoc线程封闭
Ad-hoc线程封闭是指维护线程封闭性的职责完全由程序实现来承担。
但是线程封闭技术的脆弱 程序中尽可能少使用
3.3.2 栈封闭
局部变量的固有属性之一就是封闭在执行线程中。
位置对象引用的栈封闭性时,程序员需要多做一些工作已确保引用不会逸出
3.3.3 ThreadLocal类
ThreadLocal类能使线程中的某个值与保存值的对象关联起来。每个线程都存有一份独立的副本
ThreadLocal对象通常用于防止对可变的单实例变量或局部变量进行共享。
从概念上可以将ThreadLocal
ThreadLocal的实现
3.4 不变性
对象不可变一定是线程安全的
满足以下条件对象才是不可变的
- 对象创建以后其状态不能修改
- 对象的所有域都是final类型
- 对象时正确创建的(在对象创建期间,this引用没有逸出)
3.4.1 Final域
在java内存模型中,final域还有着特殊的语义。final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无需同步
通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。
3.4.2 用volatile
3.5 安全发布
//可见性 导致的不安全发布
public Holder holder;
public void initialize(){
holder = new Holder(42);
}
3.5.1 不正确的发布:正确的对象被破坏
3.5.2 不可变对象域初始化安全性
即使某个对象的引用对其他线程是可见的,不意味着对象状态对于使用该对象的线程来说一定是可见的。
任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象的时候没有使用同步
如果final类型的域所指向的对象是可变的,那么在访问这些域所指的对象的状态是仍需要同步
3.5.3 安全发布的常用模式
可以通过以下方式来安全的发布
- 在静态初始化函数中初始化一个对象引用
= 将对象的引用保存早volatile类型或者AtomicReferance对象中
- 将对象的引用保存早某个正确构造对象final类型域中
- 将对象的引用保存早一个由锁保护的域中
静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全的发布
3.5.3 事实不可变对象
在没有额外的同步的情况下,任何线程都可以安全的使用被安全发布的事实不可变对象
3.5.4 可变对象
不可变对象可以通过任意机制发布
事实不可变都西昂必须通过安全方式发布
可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
3.5.6 安全地共享
- 线程封闭
- 只读共享
- 线程安全共享
- 保护对象
第四章 对象的组合
4.1 设计线程安全的类
包含三个基本要素
- 找出构成对象状态的所有变量
- 找出约束状态变量的不变性条件
- 建立对象状态的并发方位管理策略
同步策略定义了如何在不违背对象不变性条件
4.1.2 以来状态的操作
4.1.3 状态的所有权
许多情况下 所有与封装性是相互关联的:对象封装它拥有的状态。反之也成立,即它对它封装的状态拥有所有权。
容器类通常便显出一种所有权分离的状态, 其中容器类拥有自身的状态,而客户代码则拥有容器中各个对象的状态。
4.2 实例封闭
Instance confinement
当一个对象封闭到另一个对象中是,能够访问被封装对象的路径都是可知的。
- 对象可以被封闭在实例中
- 也可以封闭在作用域中
- 或者线程内
封闭机制更容易构造线程安全的类,因为当封闭类的状态时,分析类的线程安全性就无须检查整个程序
4.3 线程安全性的委托
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量
如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量
第五章 基础构建模块
5.1 同步容器类
同步容器类包括 Vector/ Hashtable 还包括JSK中添加的功能相似的类
他们实现线程安全的方式:将状态封装起来,每个公有方法都进行同步,每次只有一个线程能访问容器的状态
5.1.1 同步容器类的问题
并发操作修改容器时可能会出现意料之外的行为(迭代器操作)
可以通过客户端佳作来实现不可靠迭代问题,但是要牺牲掉一些伸缩性。
5.1.2 迭代器与ConcurrentModificationmException
不希望对迭代器加锁 ---解决办法 克隆容器, 缺点就是会存在显著的性能开销
5.1.3 隐藏的迭代器
编译器将字符串的连接操作转换为调用StringBuilder.append(Object)
正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制有助于确保实施同步策略
5.2 并发容器
- ConcurrentHashMap
- CopyOnWriteArrayList
- BlockingQue
5.2.1 ConcurrentHashMap
ConcurrentHashMap采用一种更细粒度的加锁机制——分段所(Locking Striping)
带来的结果就是并发访问环境下将实现更高的吞吐量,但线程中损失性能非常小。
ConcurrentHashMap与其他并发容器一起增强了同步容器类,它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要对容器枷锁
返回的迭代器具有弱一致性,并非及时失败,弱一致性的迭代器可以容忍并发的修改。
尽管有这些改进,但仍有一些需要权衡的因素,如size/isEmpty, 这些方法的语义被减弱,以反映容器的并发特性。
5.2.2 额外的原子Map操作
put if absent
remove if equal
5.2.3 CopyOnWriteArrayList
5.3 阻塞队列和生产者消费者模式
5.4 阻塞方法与中断方法
在代码中Dion共用一个将抛出InterruptedException异常的方法时,你自己的方法也就变成了一个阻塞方法,
- 传递InterruptedException
- 恢复中断
Thread.currentThread().interrupt();
5.5 同步工具类
- 信号量
- 栅栏
- 闭锁
5.5.1 闭锁 latch
可以用来确保某些活动知道其他活动都完成之后才执行
- CoundDownLatch
public long timeTasks(int nThreads, final Runnable task) thorw InterruptedException{
final CountDownLatch startGate = new CountDownLatch();
final CountDownLatch endGate = new CountDownLatch();
for (int i = i < nThreads; i++){
Thread t = new Thread(){
public void run(){
try{
startGate.await();
try{
task.run();
}finally {
endGate.countDown();
}
} catch(InterrupetedException ignored) {}
}
}
t.start();
}
long start = System.nanoTIme();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end - start;
}
5.5.2 FUtureTask
FutureTask 表示的计算是通过Callable实现的相当于一种可生成结果的Runnable
FutureTask在Executor框架中表示异步任务
public class Preloader{
private final FutureTask future = new FutureTask(new Callable(){
public ProductInfo call() throws DataLoadException{
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start(){
thread.start();
}
public ProductInfo get() throws DataLoadException, InterruptedException{
try{
return future.get();
}catch(ExecutionException e){
Throwable cause = e.getCause();
if(cause instanceof DataLoadException)
throw (DateLoadException) cause;
else
throw launderThrowable(cause);
}
}
}
5.5.3 信号量
public class BoundedHashSet{
private final Set set;
private final Semaphore sem;
public BoundedHashSet(int bound){
this.get = Collections.synchronizedSet(new HasSet());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException{
sem.acquire();
boolean wasAdded = false;
try{
wasAdded = set.add(o);
return wasAdded;
}
finally {
if(!wasAdded)
sem.release();
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if(wasRemoved)
sem.release();
return wasRemoved;
}
}
5.5.4 栅栏
闭锁时一次性对象,一旦进入终止状态就不能被重置Barried类似于闭锁,区别在于,所有线程必须同时到达栅栏位置才能继续执行。 闭锁用域等待时间,栅栏用域等他其他线程。
当线程到达栅栏位置时,调用await方法,这个方法将阻塞,直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开。所有线程都被释放。此时栅栏被重置,以便下次使用。
如果对await调用超市,或者await阻塞的线程被中断,那么栅栏就被认为时打破了。所有阻塞的线程终止,并抛出BrokenBarrierException
CyclicBarrier还会是你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时,会执行它。
public class CellularAutomata{
private final Board mainBoard;
private final CyclicBarrier barrier;
private final Worker[] workers;
public CellularAutomata(Board board){
this.mainBoard = board;
int count = Runtime.getRuntime().availableProcessors();
this.barrier = new CyclicBarrier(count,
new Runnable(){
public void run(){
mainBoard.commitNewValues();
}
});
this.workers = new Worker[count];
for(int i = 0; i < count; i++){
workers[i] = new Worker(mainBoard.getSubBoard(count,i));
}
}
private class Worker implements Runnable{
private final Board board;
public Worker(Board board){
this.board = board;
}
public void run(){
while (!board.hasConverged()){
for(int x = 0; x < board.getMaxX();x++){
for(int y = 0, y < board.getMaxY(); y++){
board.setNewValue(x,y,computeValue(x,y));
}
}
tyr{
barrier.await();
}catch(InterruptedException e){
return;
}catck(BrokenBarrierException e){
return;
}
}
}
}
public void start(){
for(int i = 0; i < workers.length;i++){
new Thread(worker[i]).start();
}
mainBoard.waitForConvergence();
}
}
第六章 任务执行
#### 6.1.3 无限制创建线程的不足
- 现成的生命周期的开销非常高
- 资源消耗
= 稳定性
### 6.2 Executor框架
public interface Executor{
void execute(Runnable command);
}
Executor是基于生产者消费者模式
6.2.3 线程池
线程池的优势
- 减小开销
= 提高响应
- 调整线程池的大小,可以创建足够多的线程以是处理器保持忙碌同时还避免了竞争
- newFixedThreadPool 创建固定长度的线程池
- newCachedThreadPool 创建一个可缓存的线程池
- newSingleThreadExecutor 创建一个单线程的Executor
- newScheduledThreadPool 创建一个固定长度的线程池,而且可以延迟或定时的方式执行任务,类似于Timer
6.2.4 Executor生命周期
public interface ExecutorService extends Executor{
void shutdown();
List shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) thorws InterruptedException;
}
运行中、关闭、终止
6.2.5 延迟任务与周期任务
6.3 找出可利用的并行性
6.3.2 携带结果的任务Callable与Future
6.3.4 在异构任务并行化中存在的局限
只有当大量相互独立且同构的任务可以并发处理时才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升
第七章 取消与关闭
行为良好的软件能很完善的处理失败、关闭和取消等过程
7.1 任务取消
-在java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作机制,使请求取消的任务和代码都遵循一种协商好的协议。
- 如使用volatile变量
7.1.1 中断
通常中断是实现取消最合理的方式
在java的api或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作使用中断,都是不合适的,并且很难支撑其更大的应用???
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息
7.1.2 中断策略
每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这线程
7.1.3 响应中断
- 传递异常
- 恢复中断
只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求
join 指定线程加入当前线程
yield 暂停当前线程
7.1.4 通过Future实现取消
当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务
7.1.6 处理不可中断的阻塞
并非所有的可阻塞方法或者阻塞机制都能响应中断
例如 一个线程由于执行同步的socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外并没有其他任何作用。
对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但是要求我们必须知道线程阻塞的原因。
- Java.io包中的同步socket I/O
- java.io包中的同步I/O
- Selector的异步I/O
7.2 停止基于线程的服务
7.2.2 关闭ExecutorService
shutdown正常关闭
shutdownNow 强行关闭
7.3 处理非正常的线程终止
7.4 JVM关闭
正常关闭的触发方式有多种
- 当最后一个正常(非守护)线程结束
- 调用了System.exit时
- 或者通过其他特定于平台的方法
7.4.1 关闭钩子
在正常关闭中,JVM首相调用所有已注册的关闭钩子(Shutdown Hook)
关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程,可以用于实现服务或者应用程序的清理工作。
public void start(){
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
try{
LogService.this.stop();
}catch(InterruptedException ingnore){}
}
});
}
7.4.2 守护线程
Daemon Thread
除了主线程意外,所有的线程都是守护线程(垃圾回收线程和其他的辅助线程)
普通线程和守护线程的区别在于,当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么jvm会正常退出操作。
当JVM停止时,所有仍然存在的守护线程将被抛弃,既不会执行finally也不会执行回卷栈,而jvm只是直接退出
7.4.3 终结器
finalize
避免使用终结器
第八章 线程池的使用
8.1 在任务与执行策略之间的隐形耦合
8.1.1 线程饥饿死锁
8.1.2 运行较长时间的任务
8.2 设置线程的大小
Runtime.availableProcessors
观察CPU利用水平来设置线程数
N_cpu = number of CPUs
U_cpu = target CPU utilization, 0 <= U_cpu <= 1
W/C = ratio of wait time to compute time
N_threads = N_cpu * U_cpu *(1+W/C)
CPU并不是唯一影响线程大小的资源,还报错内存,文件句柄,套接字句柄和数据库链接等
8.3 配置ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectExecutionHandler handler){……}
8.3.1 线程的创建与销毁
8.3.2 管理队列任务
8.3.3 饱和策略
- AbortPloicy
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
8.3.4 线程工厂
每当线程池需要创建一个线程时,都是通过一个县城工厂方法来完成的。
默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置信息。
第十章 避免活跃性危险
10.1.1 锁顺序死锁
如果所有线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题
10.1.2 动态的锁顺序死锁
查看是否存在嵌套的锁获取操作
10.1.3 在协作对象之间发生死锁
10.1.4 开放调用
在调用某个方法是不需要持有锁,那么这种调用被称为开放调用
通过开放调用来避免死锁,类似于采用封装机制提供线程安全的方法
10.1.5 资源死锁
10.2 死锁的避免与诊断
10.2.1 支持定时的锁
10.2.2 通过线程转储来分析思索
10.3 其他活跃性问题
10.3.1 饥饿
10.3.2响应性差
10。3.3活锁
活锁通常发生在处理事务消息的应用程序中,如果不能成功地处理某个消息,那么消息处理机制将会回滚整个事务,并将它重新放在队列的开头。会导致重复调用,虽然县城并没有阻塞,但是也无法继续进行下去。
通常是由于过度的错误恢复代码造成的,因为错误的将不可修复的错误作为可修复的错误
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。解决这种问题,可以引入随机性(Raft 中的随机性)
第十一章 性能与可伸缩性
可伸缩性指的是,增加计算资源是,程序的吞吐量或者处理能力响应增加
11.3 线程引入的开销
11.3.1 上下文切换
11.3.2 内存同步
11.3.3 阻塞
11.4 减少锁的竞争
- 缩小范围
- 缩小锁的粒度
- 锁分段
- 避免热点域
- 一些替代独占锁的方法
原子变量、读写锁
- 检测CPU的利用率
第十三章
ReentrantLock
并不是一种替代内置加锁的方法,而时当内置加锁机制不适用时,作为一种可选择的高级功能。
13.1 Lock与ReentrantLock
Lock提供了一种无条件的,可轮询的、定时的以及可中断的锁获取操作
Lock的视线中必须提供与内部锁象通的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能方面有所不同
public interface Lock{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeOut, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
为什么要创建一个与内置锁如此相似的加锁机制?
大多是情况下,内置锁都可以满足工作,但是功能上存在局限性,例如,无法中断一个正在等待获取锁的线程,无法在请求获取一个锁时,无限等待下去。内置锁必须在获取该锁的代码块中释放,这简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。
Lock lock = new ReentrantLock();
....
lock.lock();
try{
//doSomethind()
}finally{
lock.unlock();
}
13.1.1 轮询锁与定时锁
定时锁与轮询所提供了避免死锁发生的另一种选择
13.1.2 可中断的所
public boolean sendOnSharedLine(String message) throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(message);
}finally{
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message) throws InterruptedException{
...
}
13.1.3 非块结构的加锁
13.2 性能
13.3 公平性
公平性将由于挂起线程和回复线程时存在的开销极大降低性能。在实际情况中,统计上的公平性保证——确保被阻塞的线程最终获得锁,通常已经够用
13.4 synchrinized和ReentrantLock之间的选择
与显示锁相比,内置锁仍然具有很大的优势。内置锁为许多开发人员锁熟悉,并且简洁紧凑。
在一些内置锁无法满足需求的情况下 ReentrantLcok可以作为一种高级工具。当需要一些高级功能是才应该使用ReentrantLock,这些功能包括可定时的,可轮询的与可中断的锁获取操作,公平队列以及非块结构的锁,否则,还是应该优先使用synchronized
另外synchronized在线程转储中能给出在那些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。
synchronized时JVM的内置属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步。
13.5 读写锁
public interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}
读写锁中的一些可选实现
- 释放优先
- 读线程插队
- 重入性
- 降级
- 升级
第十四章 构建自定义的同步工具
14.1 状态依赖性的管理
14.1.3 条件队列
Object.wait会自动释放锁,并请求操作系统挂起当前线程
@ThreadSafe
public class BoundedBuffer extends BaseBoundedBuffer{
//条件谓词: not-full(!isFull())
//条件谓词: not-emptu(!isEmpty())
public BoundedBuffer(int size){
super(size);
}
public sunchronized void put(V v) throws InterruptedException{
while(isFull()){
wait();
}
doPut(v);
notifyAll();
}
public synchronized V take() throws InterruptedException{
while(isEmpte()){
wait();
}
V v = doTake();
notifyAll();
return v;
}
}
14.2 使用条件队列
14.2.1 条件谓词
条件谓词:使某个操作成为状态依赖操作的前提
在有界缓存中,只有当缓存不为空take方法才能执行, 对于take来说,他的条件谓词就是缓存不为空
在条件等待中存在一种重要的三元关系, 包括加锁,wait方法和一个条件谓词
每一次wait调用都会隐式的与特定的条件谓词关联起来,当调用某个特定的条件谓词的wait时,调用者必须已经持有与条件队列相关的锁,并且这个锁必须保护着构成条件谓词的状态变量
14.2.2 过早唤醒
wait方法的返回并不一定意味着线程正在等待的条件谓词成真了
内置条件队列可以与多个条件谓词一起使用。
使用条件等待时 如 Object.wait或者 Condition.wait
- 通常都有一个条件谓词——包括一些对象状态的测试,线程在执行前必须首先通过这些测试
- 在调用wait之前测试条件谓词,并且从wait中返回时再次进行测试
- 在一个循环中调用wait
- 确保使用与条件队列相关的锁来保护构成条件谓词的哥哥状态变量
- 当条用wait、notify、nitofyAll时一定要持有与条件队列相关的锁
- 在检查条件谓词之后以及开始执行响应的操作之前,不要释放锁
14.2.3 丢失的信号
丢失信号是指:线程必须等待一个已经为真的条件,但是在开始等待之前没有检查条件谓词
14.2.4 通知
由于在调用notify或nitofyAll时必须持有条件队列对象的锁,而如果这些等待中的线程此时不能重新获得锁,那么无法从wait返回,因此发出通知的线程应该尽快释放锁,从而确保正在等待的线程尽可能快的解除阻塞
只有同时满足以下两个条件时,才能用单一的nofify而不是notifyAll
- 所有等待线程的类型都象通
- 单进单出
14.3 显式的Condition对象
Condition是一种广义的内置条件队列
public interface Condition{
void await() throws InterruptedException;
boolean await(long timeOut, TimeUnit unit) throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUniterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
Lock.newCondition
@TreadSafe
public class ConditionBoundedBuffer{
protected final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
@GuardBy("lock")
private final T[] items =(T[]) new Object[BUFFER_SIZE];
@GuardBy("lock")
private int tail, head, count;
public void put(T x) throw InterruptedException{
lock.lock();
try{
while (count == items.length){
notFull.await();
}
items[tail] = x;
if(++tail == items.length){
tail = 0;
}
++count;
notEmpty.signal();
}finally{
lock.unlock();
}
}
public T take() throws InterruptedException{
lock.lock();
try{
while(count == 0){
notEmpty.await();
}
T x = items[head]
items[head] == null;
if(++head == items.length){
head = 0;
}
--count;
notFull.signal();
return x;
} finally{
lock.unlock();
}
}
}
通过将两个条件谓词分别放到两个等待线程集中
14.4 Synchronizer剖析
实际上 ReentrantLock和 Semaphore的实现都使用了一个共同的基类,即AbstractQueuedSynchronizer(AQS)
AQS是一个用域构建锁和同步器的框架,许多同步器都可以通过AQS构造
//AQS中获取操作和释放操作的基标准形式
boolean acquire() throws InterruptedException{
while(当前状态不允许获取操作){
if(需要阻塞获取请求){
如果当前线程不在队列中,则将其插入队列
阻塞当前线程
}
else
返回失败
}
可能更新同步器的状态
如果线程位于队列中,则将其移出队列
返回成功
}
void release(){
更新同步器的状态
if(新的状态允许某个被阻塞的线程获取成功)
解除队列中一个或多个线程的阻塞状态
}
第15章 原子变量与非阻塞同步机制
15.1 锁的劣势
15.2 硬件对并发的支持
15.1 CAS
CAS的主要缺点是, 它将使调用者处理竞争问题,而锁能自动处理竞争问题
15.2.3 JVM对CAS的支持
java.util.concurrent中的大多数类在实现时直接或间接的使用了这些原子变量类
15.3 原子变量类
- AtomicInteger
- AtomicLong
- AtomicBoolean
15.4 非阻塞算法
基于锁的算法可能会发生各种活跃性故障,如果线程在持有锁时由于阻塞I/O,内存也确实或其他延迟导致推迟执行,那么很有可能其他线程都不能继续执行。
如果在某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。
非阻塞算法通常不会出现死锁和优先级反转的问题,但是可能会出现饥饿和活锁问题。
15.4.1 非阻塞栈
@ThreadSafe
public class ConcurrentStack {
AtomicReference> top = new AtomicReference>();
public void push(E item){
Node new Head = new Node(item);
Node oldHead;
do{
oldHead = top.get();
newHead.next = oleHead;
} while (!top.compareAndSet(old,newHead));
}
public E pop(){
Node oldHead;
Node newHead;
do{
oldHead = top.get();
if(oldHead == null){
return null;
}
newHead = oldHead.next;
} while(!top.compareAndSet(oldHead, newHead));
}
private static Node{
public final E item;
public Node next;
public Node(E item){
this.item = item;
}
}
}
第十六章 Java内存模型
Java语言规范要求JVM在线程中维护一种类似串行的语义:只要程序的最终结果与在严格串行的环境中执行结果相同,那么上述所有操作都是允许的。
JMM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作再合适将对于其他线程可见。
16.1.1 平台的内存模型
16.1.2 重排序
16.1.3 Java内存模型简介
java内存模型时通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放惭怍,以及线程的启动和合并操作,JMM位程序中所有的操作定义了一个偏序关系
称之为Happens-before
当一个变量被多线程读取,并且至少被一个线程写入时,如果在取操作和写操作之间没有依照Happens-before来排序,那么就会产生数据竞争问题。
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 线程启动规则
- 线程结束规则
- 终端规则
- 终结器规则
- 传递性
见jvm笔记
16.2 发布
除了不可变对象意外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行。
16.2.2 安全发布
- 延迟初始化
- 提前初始化
- 双重检查加锁(Double Lock Check)
16.3 初始化过程中的安全性
初始化安全性将确保对于被正确构造的对象,所有线程都能看到有构造函数位对象给各个final域设置的正确值,而不管采用何种方式来发布对象。而且,对于可以通过被正确构造对象中某个final域到达的任意变量,将同样对于其他线程可见。
对于含有final域的对象,初始化安全性可以防止对象的初始引用被重排序到构造过程之前。当构造函数完成时,构造函数对final域的所有写入操作,以及对通过这些域可以到达的任何变量的写入操作,都将被冻结。并且任何获得该对象引用的线程至少能确保看到被冻结的值。
初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。