Java并发编程实战读书笔记

第二章 线程安全性
并发特性
原子性
一个操作不会被线程调度机制打断,要么操作中的指令全部执行完毕,要么全部不执行,中间不会有任何的线程切换.

可见性:
一个线程对变量的值进行了修改,其他线程能够立即得知这个修改.(内存屏障)

有序性
有序性就是指程序按照代码的先后顺序执行.编译器为了优化性能,有时候会改变程序中语句的先后顺序.Java提供了volatile和synchronized两个关键字来保证线程之间的操作的有序性.

同步
指的就是线程间的协作,本质上和现实生活中的协作没区别,一个线程执行完一个任务后,如何通知执行后续任务的线程开工.

互斥
指同一时刻,只允许一个线程访问共享变量

线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要额外的同步,这个类都能表现正确的行为,那么就说这个类是线程安全的。

无状态对象
无状态对象不包含域,也不包含与其他类中域的引用,计算过程中的临时状态仅存在于线程栈中的局部变量上,并且只能由正在执行的线程访问。访问无状态对象的线程不会影响另一个访问同一个对象的线程的计算结果,因为这两者之间没有共享关系。因此,无状态对象是线程安全的。

竞态条件
在并发编程中,由于不恰当的执行时序而出现不正确的结果,这种情况称为竞态条件.
当某个计算的正确性取决于多个线程的交替执行顺序时,那么就会发生竞态条件,也就是说,正确的结果取决于运气。竞态条件和原子性相关,或者说,之所以代码会发生竞态条件,就是因为代码不是以原子方式操作的,而是一种复合操作。

数据竞争
在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争.当一个线程写入一个变量,而另一个线程接下来读取这个变量,或者读取一个由另一个线程写入的变量时,并且在这两个线程之间没有使用同步,那么就可能出现数据竞争.

复合操作
当两个独立的操作是线程安全的时候, 那么这两个一起操作, 如果不进行同步的话, 也是非线程安全的, 如add和get, 即使各自都是线程安全的, 在一起操作也会有安全问题
加锁机制
内置锁
synchronized关键字
synchronized关键字用在非静态方法上或代码块,则称为方法锁/对象锁.它们锁的是this,即当前对象.而用在静态方式中,则为类锁,锁的是当前类的Class. 类锁和对象互相不影响

每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁.线程进入同步代码块或方法时会自动获取该锁,在退出同步代码块或方法时会释放该锁.获得该内置锁的唯一途径就是进入这个锁的保护同步代码块或方法.

内置锁是一个互斥锁,意味着最多只有一个线程能够获得该锁,当线程A尝试去获取线程B持有的内置锁时,线程A必须等待或堵塞,直到线程B释放这个锁,若线程B不释放锁,线程A就会一直等下去,这种情况称为死锁.

可重入是指对于同一个线程,它可以重新获得已有它占用的锁。
可重入性意味着锁的请求是基于”每线程”而不是基于”每调用”,它是通过为锁关联一个请求计数器和一个占有它的线程来实现。
可重入性方便了锁行为的封装,简化了面向对象并发代码的开发,可以防止类继承引起的死锁

注意控制synchronized锁定的范围
第三章 对象的共享
非原子的64位操作
非volatile类型的64位数值变量(double和long), jvm允许64的读操作和写操作分解为两个32位的操作, 所以很可能在多线程中发生读取到某个值的高32位和另一个值的低32位, 所以这两个数据类型要注意

volatile关键字
使用volatile关键字修饰的变量会强制将修改的值立即写入到主存,主存中值的更新会使缓存(线程栈中的变量副本)中的值失效(读取时会直接从内存中读取新值),volatile会禁止指令重排.具有可见性,有序性不具备原子性.
通常用来做某个操作的中断或者状态的标志,
发布与逸出
发布一个对象的意思是使它能够被当前范围之外的代码所使用.可以通过公共静态变量,非私有方法,构造方法内隐含引用三种方式
当一个对象能够给其他代码引用。即为发布, 例如将私有变量通过get方法发布出去

若对象构造完成之前就发布该对象,就会破坏线程安全性.当某个不该发布的对象被发布时,这种情况被称为逸出
在构造函数中发布一个对象, 则为不正确的
线程封闭
栈封闭(局部变量)和ThreadLocal
多线程访问共享数据为了安全性通常需要同步,若仅在单线程内访问数据就不需要同步,这种避免共享数据的技术称为线程封闭.
使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。

Ad-hoc线程封闭
指维护线程限制性的任务全部落在实现上.这是靠实现者控制的线程封闭.它的线程封闭完全靠实现者实现,Ad-hoc线程封闭非常脆弱,没有任何一种语言特性能将对象封闭在目标线程上.

栈封闭
就是局部变量

ThreadLocal类
ThreadLocal线程本地变量是一种规范化的维护线程限制的方式,它允许将每个线程与持有数值的对象关联在一起,为每个使用它的线程维护一份单独的拷贝。ThreadLocal提供了set和get访问器,get总是返回由当前线程通过set设置的最新值。

不可变性
它的状态不能在创建后再被修改
所有域都是final类型
它被正确创建(创建期间没有发生this引用的逸出)

不可变对象一定是线程安全的,不需要任何同步或锁的机制就可以保证安全地在多线程之间共享

final域, 使用final修饰的对象的引用不可变, 但是对象本身还是可变的, 也就是引用指针指向的堆中地址不可变了

安全发布
在某些情况下我们希望在多个线程间共享对象,此时必须确保安全地进行共享

由于没有使用同步来确保对象对其他线程可见,因此将此种称为“未被正确发布”。在未被正确发布的对象中存在两个问题。首先,除了发布对象的线程外,其他线程可以看到的对象是一个失效值,因此将看到一个空引用或者之前的旧值。然而,更糟糕的情况是,线程看到引用的值是最新的,但对象状态的值却是失效的。情况变得更加不可预测的是,某个线程在第一次读取域时得到失效值,而再次读取这个域时会得到一个更新值

如果没有足够的同步,那么当在多个线程间共享数据时将发生一些非常奇怪的事情。

3.5.2不可变对象与初始化安全性
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
3.5.3安全发布的常用模式
可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布;
·在静态初始化函数中初始化一个对象引用。
·将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
·将对象的引用保存到某个正确构造对象的final类型域中。
将对象的引用保存到一个由锁保护的域中。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化
public static Holder holder = new Holder (42);
静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布
3.5.5可变对象
如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后
续修改操作的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某个锁保护起来。

对象的发布需求取决于它的可变性:
不河变对象可以通过任意机制来发布。
事实不可变对象必须通过安全方式来发布
·可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来

3.5.6安全地共享对象
在并发程序中使用和共享对象时,可以使用一些实用的第略,包括2
线程封闭,线程封用的对象只能由一个线程拥有,对象被封闭在该线程中,并且积能由这个线程修改,
只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,.但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
爱线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不常要进一步的同步。
保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。.
第四章 对象的组合***
第五章 基础构建模块
同步容器
同步容器包括两部分,一个是Vector和Hashtable,他们是早期JDK的一部分;另一个是他们的同系容器,在JDK1.2中才被加入的同步包装(wrapper)类.
这些类是由Collections.synchronizedXXX(例Collections.synchronizedList)工厂方法创建的.
这些类通过封装它们的状态,并对每一个公共方法进行了同步而实现了线程安全**,这样一次只有一个线程能访问容器的状态.
// SynchronizedList 中的部分方法
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}

同步容器中出现的问题
同步容器都是线程安全的.但是对于复合操作,可能还是需要额外加锁进行保护.

迭代器与ConcurrentModificationException
在设计同步容器类的迭代器并没有考虑到并发修改的问题, 并且他们的表现是”及时失败”(fail-fast)的, 当容器在迭代时被修改会抛出一个异常
实现方式是将计数器变化与容器关联, 如果迭代期间改变, 那么hasNext或者next将抛出上述异常, 想要避免就必须在迭代时持有锁, 如果不想加锁, 那就克隆copyonwrite

隐藏迭代器
在一些操作中或者是编译之后的代码, 会出现迭代的情况, 这种是隐藏着的迭代器, 比如容器的toString方法, 会迭代容器
并发容器
Java5.0通过提供几种并发的容器类来改进同步容器.同步容器通过对容器的所有状态进行串行访问,从而实现了它们的线程安全,这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低.
另一方面,并发容器是为多线程并发访问而设计的,Java 5.0添加了ConcurrentHashMap, 来替代同步的哈希Map实现;当多数操作为读取操作时,CopyOnWriteArrayList是List相应的同步实现.新的ConcurrentMap接口加入了对常见符合操作的支持,比如"缺少即加入",替换和条件删除.

Queue
用来临时保存正在等待被进一步处理的一系列元素.Queue的操作并不会阻塞;若队列是空的,那么从队列中获取元素的操作会返回空值(null)., 通过LinkedList实现的

BlockingQueue
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作.如果队列是空的,一个获取操作会一直阻塞,直到队列中存在可用元素;若队列是满的(对于有界队列),插入操作会一直阻塞直到队列中存在可用空间.阻塞队列在生产者-消费者设计中非常有用.
共有四组方法用于存取元素, 其中put和take为阻塞

Java6引入了ConcurrentSkipListMap和ConcurrentSkipListSet分别作为同步的SortedMap和SortedSet的替代品

ConcurrentHashMap
ConcurrentHashMap和HashMap一样是一个哈希表,但是它使用完全不同的锁策略,可以提供更好的并发性和可伸缩性.在ConcurrentHashMap以前,程序使用一个公共锁同步每一个方法,并严格地限制只有一个线程可以同时访问容器.而ConcurrentHashMap使用一个更加细化的锁机制,名叫分段锁(Java8改为cas). 并且提供的迭代器不会抛出
ConcurrentModificationException异常, 是一种弱一致性的, 所以比如size方法返回的值可能不是特别正确, 所以在使用时可以和HashTable做取舍

额外的原子Map操作
由于ConcurrentHashMap不是独占锁, 所以一些如没有则添加,若相等则移出的操作没有保障, 所以jdk已经实现了一些此类原子方法供开发者使用

分段锁
这个机制允许更深层次的共享访问.任意数量的读线程可以并发访问Map,读者和写者也可以并发访问Map,并且有限数量的写线程还可以并发修改Map.结果是,为并发访问带来更高的吞吐量,同时几乎没有损失单个线程访问的性能.,默认分为16个段

CopyOnWriteArrayList和CopyOnWriteArraySet
CopyOnWriteArrayList是同步List的一个并发替代品,CopyOnWriteArraySet是同步Set的并发替代品,通常情况下它们提供了更好的并发性
阻塞队列(Blocking Queue)
阻塞队列提供了可阻塞的put和take方法,它们与可定时的offer和poll是等价的.如果Queue已经满了,put方法会被阻塞直到有空间可用;如果Queue是空的,那么take方法会被阻塞,直到有元素可用.Queue的长度可以有限,也可以无限;无限的Queue永远不会填满,所以它的put方法永远不会阻塞.
Blocking Queue存在多种实现, 如LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列还有一些其他的实现类

有界队列
即有限队列.有界队列是强大的资源管理工具,用来建立可靠的应用程序:它们遏制那些可以产生过多工作量,具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮.

双端队列
Deque和BlockingDeque,它们分别扩展了Queue和BlockingQueue.Deque是一个双端队列,允许高效地在头和尾分别进行插入和移除.实现它们的是ArrayDeque和LinkedBlockingDeque.
阻塞方法与中断方法
线程阻塞或暂停的原因:
等待I/O操作结束
等待获得一个锁
等待从Thread.sleep中唤醒
等待另一个线程的计算结果

当一个线程阻塞时,它通常被挂起,并被设置成线程阻塞的某个状态
(BLOCKED,WAITING,TIMED_WAITING).

中断
当一个方法能够抛出InterruptedException的时候,是在告诉你这个方法是一个可阻塞方法,如果它被中断,将可以提前结束阻塞状态.

Thread提供了interrupt方法,用来中断一个线程, isInterrupted()方法查询某线程是否中断.每个线程都有一个布尔类型的属性(默认false),这个属性代表了线程的中断状态;中断线程时需要设置这个值.设为true, 线程会不定期的检查该值

中断是一种协作机制.一个线程不能够迫使其他线程停止正在做的事或去做其他事情;当线程A 中断 线程B,A仅仅是要求B在达到某一个方便停止的关键点时,停止正在做的事情.

当你的代码中调用了一个会抛出InterruptedException的方法,你自己的方法也就成为了一个阻塞方法,要为响应中断做好准备.有两种选择:

传递InterruptedException,若你能侥幸避开异常的话,这通常是最明智的策略,只需要把异常传递给你的调用者,这可能包括不捕获异常,也可能是先捕获,进行特定的清理,再抛出.

修复中断.当你不能抛出InterruptedException时,你必须捕获它,并且在当前线程中调用interrput从中断中恢复(因为抛出异常后中断标示会被清除),这样调用栈中更高层的代码可以发现中断已经发生.
同步工具类
Synchronizer
Synchronizer是一个对象,它根据本身的状态调节线程的控制流.阻塞队列可以扮演一个Synchronizer的角色;其他类型Synchronizer包括信号量,关卡以及闭锁.

闭锁
闭锁是一种Synchronizer,它可以延迟线程的进度直到线程到达终点状态.
闭锁可以用来确保特定活动直到其他活动完成后才发生,

CountDownLatch
一个灵活的闭锁实现.允许一个或多个线程等待一个事件集的发生.闭锁的状态包括一个计数器做减操作,表示一个事件已经发生了,而await方法等待计数器达到零,此时所有需要等待的时间都已发生.若计数器入口时值为非零,await会一直阻塞直到计数器为零或等待线程中断以及超时. 每次Countdown方法会减1,

FutureTask
FutureTask同样是一个闭锁.(FutureTask的实现描述了一个抽象的可携带结果的计算).FutureTask的计算是通过Callable实现的,它等价于一个可携带结果的Runnable,并且有3个状态:等待,运行和完成.完成包括所有计算以任意的方式结束.一旦进入完成状态,它会永远停在这个状态.
Future.get的行为依赖于任务的状态,若它已经完成,get可以立刻获得返回结果,否则会被阻塞直到任务转入完成状态,然后会返回结果或抛出异常. Get是阻塞方法

信号量(Semaphore)
计数信号量(Counting semaphore)**用来控制能够同时访问某特定资源的活动的数量,或者同时执行某一给定操作的数量.计数信号量可以用来实现资源池或者给一个容器限定边界.

一个信号量管理一个有效的许可(permit)集;许可的初始量通过构造函数传递给信号量.活动能够获得许可(只要还有剩余许可),并在使用之后释放许可.若已经没有可用许可,那么acquire会被阻塞,直到有可用的为止(或被中断或超时).release方法向信号量返回一个许可(acquire是消费一个许可,release是创建一个许可,许可数量不受semaphore限制).
类似令牌桶, 当发放完毕之后就没有资源了, 线程就不能消费资源, 需要等待其他线程的释放

关卡(Barrier)/栅栏
关卡类似于闭锁,它们都能够阻塞一组线程,直到某些事件发生.其中关卡与闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件;关卡等待的是其他线程.

闭锁用在一等多的情况, 栅栏用在多个线程互相等待的一种情况,
CyclicBarrier是一个标准的实现, 通过await方法进行等待

Exchanger
另一种栅栏是Exchanger它是一种两方栅栏, 主要用于两个线程之间相互交换数据

第一部分小结
并发诀窍
所有并发问题都归结为如何协调访问并发状态.可变状态越少,保证线程安全就越容易
尽量将域声明为final类型,除非它们的需要是可变的
不可变对象天生是线程安全的
封装使管理复杂度变得更可行
用锁来守护每一个可变变量
对同一不变约束中的所有变量都使用相同的锁
在运行复合操作期间持有锁
在非同步的多线程情况下,访问可变变量的程序是存在隐患的
不要依赖于可以需要同步的小聪明.
在设计过程就考虑线程安全,或在文档中明确说明它不是线程安全的.
文档化你的同步策略.
第二部分 结构化并发应用程序
第六章 任务执行
Executor框架
Executor基于生产者-消费者模式.提交任务的执行者是生产者,执行任务的线程是消费者.如果要在你的程序中实现了生产者-消费者的设计,使用Executor通常是最简单的方式.

线程执行策略
所有任务在单一的线程中顺序执行
每个任务在自己的线程中执行
将任务的提交与任务的执行体进行解耦(Executor)

线程池
线程池管理一个工作者线程的同构池,线程池与工作队列紧密绑定的.所有工作队列,其作用是持有所有等待执行的任务.工作者线程从工作队列中获取下一个任务,执行它,然后回来继续等待另一个线程.

Executors工具类包含四个方法来创建固定的线程池

Executor的生命周期
为解决生命周期问题, Executor扩展了ExecutorService接口, 添加了一些用于生命周期管理的方法,

ExecutorService生命周期有三种状态: 运行 关闭 终止
最初创建的时候初始状态是运行状态.shutdown方法会启动一个平缓的关闭过程:停止接收新的任务,同时等待已经提交的任务完成–包括尚未开始执行的任务.shutdownNow方法会启动一个强制的关闭过程:尝试取消所有运行中的任务和排在队列中尚未开始的任务.
Executor实现通常只是为了执行任务而创建线程,但JVM会在所有线程全部终止后才推出,因此无法正确关闭Executor,将会阻止JVM的结束.

延迟任务与周期任务
Timer工具管理任务的延迟执行(“100ms后执行该任务”)以及周期执行(“每10ms执行一次该任务”).但Timer存在一些缺陷:
所以可以考虑用ScheduledThreadPoolExecutor作为替代品.

可携带结果的任务: Callable 和 Future
Callable
Callable和Runnable一样描述的是抽象的计算型任务,这些任务通常是有限的:它们有一个明确的开始点,而且最终会结束., 带有返回值

Future
一个Executor执行的任务的生命周期有四个阶段:创建,提交,开始和完成.
Future描述了任务的生命周期,并提供方法来获得任务的结果,取消任务以及检查任务是否已完成还是被取消.
Future的任务是单向的,一旦任务完成,它就永远停留在完成状态上.
可以通过get方法来获取任务的执行结果,但任务的状态决定了get方法的行为.
若任务执行完成,get会立刻 .
若任务没有完成,get会阻塞到它完成.
若任务抛出异常,get会将该异常封装为ExecutionException,然后重新抛出;
若任务取消, get会抛出CancellationException.当抛出ExecutionException时,可调用getCause重新获得被封装的原始异常.
还可以显示的为一个任务实例化一个FutureTask

CompletionService
CompletionService整合了Executor和BlockingQueue的功能.你可以将Callable任务提交给它去执行,然后使用类似于队列中take和poll方法,在结果完整可用时将被封装为Future.

ExecutorCompletionService
ExecutorCompletionService是实现CompletionService接口的一个类,并将计算任务委托给一个Executor.
ExecutorCompletionService在构造函数中创建了一个BlockingQueue,用它去保存完整的结果.计算完成时会调用FutureTask中的done方法,当提交了一个任务后,首先会把这个任务包装为一个QueueingFuture,它是FutureTask的一个子类,然后覆写done方法,将结果置入BlockingQueue中,它会在结果不可用时阻塞.

限时任务
Future.get可以设置时限, 若在时限内没有返回结果,就会抛出TimeoutException,若时限超过后应该立刻停止它们,防止资源浪费,调用Future.cancel(true)来取消任务.
第七章 取消与关闭
任务取消
当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动被称之为可取消的.例如:用户请求的取消,限时活动,应用程序事件,错误,关闭.

中断
取消机制会使得任务退出, 但是如果是一个阻塞任务那么任务可能不会检查取消标识,
Java中一些特殊的阻塞库的方法支持中断, 线程中断是一种协作机制
让一个线程通知另一个线程停止当前工作.
每个线程都有一个boolean类型的中断状态, 当中断线程时候, 被设为true, 在Thread中包含中断线程和查询中断状态的方法,
Interrupt方法能中断目标线程, 而isInterrupt方法能返回线程的中断状态, 静态的Interrupted方法将清楚当前线程中断状态, 并返回之前的值, 这也是清除中断状态的唯一方法,

阻塞库方法, 例如Thread.sleep和Object.wait, 都会检查线程何时中断, 并且在发现中断时提前返回, 它们在响应中断时执行的操作包括, 清除中断状态, 抛出InterruptException
JVM并不保证阻塞方法检查中断状态的速度, 但是实际上还是挺快的,

调用Interrupt方法并不意味着立即停止目标线程, 而是传达了请求中断的消息, 然后由线程在一个合适的时刻中断自己, 这些时刻也被叫为取消点

在使用静态的Interrupted时应该小心, 它会清楚当前线程的中断状态, 如果在调用Interrupted时返回true, 那么需要对它进行处理, 可以抛出InterruptException, 或者再次调用Interrupt方法来恢复中断状态, (Interrupted会清除中断状态)

通常中断是实现取消的最合理的方式

响应中断
两种处理InterruptException
传递异常, 从而使你的方法也成为可中断的阻塞方法,
恢复中断状态, 从而使调用栈上的上层代码能对其进行处理

如果不想传递InterruptException, 那么需要另一种方式来保存中断请求, 标准方式就是再次调用Interrupt方法恢复中断状态

通过Future来实现取消
Future有一个cancel方法, 该方法带有一个Boolean类型的参数, 表示取消操作是否成功, 如果为true表示正在运行, 那么可以取消, 如果为false那意味着不要启动这个任务
当get方法抛出InterruptException或Timeout异常时, 如果不再需要结果, 那么就 可以调用cancel来取消任务

处理不可中断的阻塞
Java.io中同步SocketIO, 关闭底层的套接字可以抛出异常
Java.io中同步IO,
Selector的异步IO, 如果一个线程调用Selector.select方法阻塞, 调用close和wakeup方法会是线程抛出异常

获取某个锁, Lock类中提供了lockInterruptibly方法,该方法允许等待一个锁时仍能响应中断

ExecutorService的关闭
ExecutorService提供了关闭的两种方式:使用shutdown优雅的关闭和利用shutdownNow强行关闭.在强行关闭中,shutdownNow首先尝试关闭当前正在执行的任务,然后返回待完成任务的清单.

shutdownNow的局限性
当通过shutdownNow强行关闭一个ExecutorService时,它试图取消正在进行的任务,并返回那些已经提交,但并没有开始的任务的清单,这样,这些任务可以被日志记录,或存起来等待进一步处理.
ExecutorService executorService = Executors.newFixedThreadPool(10);
List runnables = executorService.shutdownNow();
第八章 线程池的使用
当线程池中的会因为抛出异常而终止, 这时候会在线程池中新加一个线程, 所以使用ThreadLocal时候, 需要注意抛出异常之后的处理

线程饥饿死锁
在线程池中, 如果一个任务依赖于其他的任务, 那么可能产生饥饿死锁,

针对运行时间长的任务, 可能会阻塞线程池, 导致其他任务变慢, 可以采用带有限时的阻塞方法, 超时则返回

设置线程池的大小
避免过大或过小, 过大很导致多个线程在竞争cpu和内存, 性能反而下降, 过小导致cpu空闲
在设置线程池的时候应该注意项目的情况, 比如是计算密集型还是IO密集型
计算密集型建议按照CPU的核心数来设置,
IO密集型的可以适当调大些

配置ThreadPoolExecutor
线程的创建和销毁
通过核心大小, 最大大小, 存活时间,来控制
并且一开始接受任务时候, 才开始创建线程, 核心线程也是这样, 当超过核心数量时候, 任务先进入到存储队列, 如果队列满了, 则开始创建线程到最大大小, 当线程空闲了超过存活时间, 并且线程数量超过核心大小时候, 会回收超过的部分线程

newFixedThreadPool工厂方法创建一个固定大小的线程池, 但是队列长度不限制,
newCachedThreadPool方法创建的线程不限制, 队列长度为1
newSingleThreadPool单个线程, 无限队列

尽量使用有界队列, 防止资源耗尽

饱和策略
当线程池无法在处理更多请求之后开始触发饱和策略, 通过构造函数或者调用setRejected…来设置, 可以继承接口后自定义自己的策略并设置到线程池中
jdk提供了几种策略
AbortPolicy: 中止, 默认的饱和策略, 会抛出RejectedExecutionException
CallsRunsPolicy 调用者运行, 将任务回退给调用者, 在调用者的主线程上执行
DiscardPolicy: 会抛弃新任务, 不会抛异常
DiscardOldestPolicy: 抛弃最旧的任务, 及下一个将要执行的任务, 优先级队列则会抛弃优先级最高的任务

线程工厂
每当线程池创建一个线程时都会调用线程工厂的方法来创建, 在ThreadFactory中只定义了一个方法newThread, 这个线程工厂通过构造函数传给ThreadPoolExecutor
如果你想自定义一些创建的线程的信息可以覆盖这个线程工厂, 比如给新建的线程起一个自定义的名字, 注意覆盖newThread方法

可以通过set方法设置ThreadPoolExecutor的信息

扩展ThreadPoolExecutor
它提供了几个可以在子类中改写的方法, beforeExecute, afterExecute, terminated
如果beforeExecute抛出异常了那么任务不会执行afterExecute也不会执行, afterExecute会在run正常返回, 或者抛异常之后都会调用, Error则不会调用, terminated在线程池完成关闭操作是调用
第九章 图形用户界面应用程序
第三部分 活跃性,性能与测试
第十章 避免活跃性危险
死锁
锁顺序死锁
当两个线尝试采用不同的顺序获取同一个锁时会发生顺序死锁, 如果都按照相同顺序那么久不会发生死锁,

动态的锁顺序死锁
由于传参导致的, 一个方法接受的参数, 如果倒过来那么可能会发生死锁,比如银行转账时候要获取转账账户和被转账账户

协作对象之间发生死锁
如果在持有锁的情况下调用某个外部方法时候, 需要注意外部方法是否要获取锁, 如果要获取锁那么可能存在死锁

开放调用
如果在调用某个方法时候不需要持有锁, 这种调用被称为开放调用
也就是缩小锁定的范围, 同步代码块代替同步方法, 然而需要注意开放调用中是否需要同步

资源死锁
当多个线程在相同的资源上等待时, 也会发生死锁

死锁的避免与诊断
尝试使用带有定时的锁
可以通过线程转储信息来分析死锁
其他活跃性危险
饥饿
当线程无法访问它所需要的资源而无法继续执行时, 就发生饥饿
通常因为cpu的时间周期, 或者线程优先级导致, 编程应避免使用线程优先级

活锁
单线程时候, 处理器会反复调用, 并返回相同的结果, 虽然处理消息的线程没有阻塞, 但也无法继续执行,
这种通常是因为过度的错误恢复代码造成的
当两个相互协作的线程对彼此响应从而修改各自状态时, 并使得任何一个线程都无法继续执行时候就发生了活锁, 为了避免需要在重试时候加入随机性

最常见的活跃性故障是锁顺序死锁, 请确保线程在获取多个锁时候顺序一致

第十一章 性能与可伸缩性
Amdahl定律
线程引入的开销
上下文切换
切换时候需要访问操作系统和jvm的共享数据, 造成开销, 导致应用的cpu使用时钟周期变少, 新线程需要的数据可能不再当前处理器的本地缓存中, 因为可能会导致一些缓存缺失, 因而线程首次运行会更加缓慢

内存同步
同步操作的性能开销包括多个方面, 在synchronized和volatile提供的可见性中会使用一些特殊指令, 即内存栅栏, 它会刷新缓存, 使缓存无效, 还会抑制编译器优化, 禁止重排序等

阻塞
当发生阻塞时候, jvm使用两种方式处理, 自旋等待和线程挂起, 自旋等待引入了自适应的自旋等待, 根据当前线程以往获取某个锁的成功率计算得出自旋时间, 线程挂起将导致上下文的切换

减少锁的竞争
减少锁的持有时间
减少锁的请求频率
使用带有协调机制的锁, 这些锁有更高的并发性

缩小锁的范围
将不需要锁的代码提出同步代码块之外

减小锁的粒度
主要涉及锁分段和锁分解,

避免热点域
比如全局的计数器, 总被访问, 这种应该避免

替换掉独占锁
比如使用读写锁, 使用并发容器替换同步容器, 使用volatile变量 原子变量等

第十二章 并发程序的测试
第四部分 高级主题
第十三章 显示锁
jdk5之后开始提供显示锁

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();
}

ReentrantLock
ReentrantLock实现了Lock接口,提供了与synchronized相同的互斥和内存可见性的保证.获得ReetrantLock的锁与进入synchronized块有相同的内存语义,释放ReentrantLock锁与退出synchronized块有相同的内存语义.ReetrantLock提供了synchronized一样的可重入加锁的语义.同时支持Lock接口定义的所有获取锁的模式.与synchronized相比,ReentrantLock为处理不可用的锁提供了更多的灵活性.

可轮询的和定时锁
可轮询的和可定时的锁获取模式,是由tryLock方法实现,与无条件的锁获取相比,它具有更完善的错误恢复机制. 从而更容易的防止死锁的产生

可中断的锁获取操作
可中断的锁获取操作允许在可取消的活动中使用,比如请求不响应中断的内部锁.这些不可中断的阻塞机制使得实现可取消的任务变得复杂.当你正在响应中断的时候,lockInterruptibly方法能够使你获得锁,并且由于它是内置于Lock的,你因此不必再创建其他种类不可中断的阻塞机制.

非块结构的锁
在内部锁中,获取和释放这样成对的行为是块结构的—总是在其获得的相同的基本程序块中释放锁,而不考虑控制权是如何退出阻塞块的.自动释放锁简化了程序的分析,并避免了潜在的代码错误造成的麻烦,但有时需要更灵活的加锁规则.将加锁和解锁放在不同的代码中不同的类中

公平性
ReentrantLock构造函数提供了两种公平性的选择:可以创建非公平锁(默认)或者创建公平锁.公平锁,在并发环境中,每个线程在获取锁之前都会先查看此锁的等待队列,若为空,则占有锁,否则就会加入到等待队列,而非公平锁则直接会去尝试占有锁,若尝试失败,新发出请求的线程才会被放入队列中,
但是公平锁的性能比较差, 因为挂起和唤醒线程存在延迟, 如果在这段时间中有线程插队那么将会做完事情后释放掉锁之后, 之前那个线程可能才刚醒过来, 并且使用锁, 所以这种比不可插队的公平锁会性能上有所提高, 但是如果锁定时间长或者获取锁的周期时间长的可以使用公平锁
内置锁和Lock都不会提供确定的公平性保证

使用锁时候首选synchronized, 如果需要更高级功能则选择ReentrantLock

读-写锁
ReentrantLock实现了标准的互斥锁.但互斥锁通常为了保护数据一致性的很强的加锁约束.互斥避免了"写/写"和"写/读"的重叠,但是同样避开了"读/读"的重叠.读-写锁允许:一个资源能够被多个读者访问,或被一个写者访问,两者不能同时进行.
public interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}
读-写锁实现的加锁策略允许多个同时存在的读者,或只存在一个写者.
ReentrantReadWriteLock
为两个锁提供了可重入的加锁语义.和ReentrantLock相同,也能创建非公平锁和公平锁.
在公平锁中,把选择权交给等待时间最长的线程;若锁由读者获得,而一个线程请求写入锁,那么不再允许读者获得读取锁,知道写者释放写入锁.
在非公平锁中, 锁的顺序是不确定的, 写锁可以降级为读锁, 但是读锁不可以升级为写锁, 这样会造成死锁
当读取操作多的时候, 可以明显提升性能

第十四章 构建自定义的同步工具
条件队列
每个java对象都可以作为一个锁,同样,每个对象也可以作为一个条件队列,使用wait notify notifyAll这些API进行操作, 其操作框架有如下:
• 检测前置条件之前,要先获得锁,保证前置条件的一致性。(不被别的线程并发修改)
• 循环检测前置条件,因为可能被唤醒的时,前置条件仍失效(条件队列可能管理了多个条件谓词),所以需要循环检测。
• Object.wait操作会自动释放锁,并请求操作系统挂起当前线程。当醒来时,重新获取之前的锁。
• Object.notifyAll用来唤醒在当前条件上等待的所有线程。

条件谓词
条件谓词是先验条件的第一站,它在一个操作与状态之间建立起依赖关系.
当线程到达某个状态时候开始执行

当使用条件等待时(Object.wait 或 Condition.await)
永远设置一个条件谓词----一些对象状态的测试,线程执行前必须满足它;
永远在调用wait前测试条件谓词,并从wait中返回后再次测试
永远在循环中调用wait
确保构成条件谓词的状态变量被锁保护,而这锁正是与条件队列相关联的
当调用wait,notify或notifyAll时,要持有与条件队列相关的锁,
在检查条件谓词之后,开始执行被保护代码的逻辑之前,不要释放锁.

丢失的信号
线程必须等待一个已经为真的条件, 但在开始之前没有检查条件谓词, 现在线程一直等待一个已经发生过的事件

通知
当在等待一个条件时, 一定要确保在条件谓词变为真时发出通知
显示的Condition对象
一个Condition和一个单独的Lock相关联,调用与Condition相关联的Lock的Lock.newCondition方法,就可以创建一个Condition,Condition提供了比内部条件队列要丰富的特征集:每个锁可以有多个等待集,可中断/不可中断的条件等待,基于时限的等待以及公平/非公平队列之间的选择.
如果想编写一个带有多个条件谓词的并发对象,

内置条件队列存在一些缺陷:每个内置锁只能有一个相关联的条件队列,而没法支持在不同条件谓词下分别等待的逻辑。灵活的方式是使用Condition,一个Condition和一个Lock相关联,但可以根据需要生成多个不同的Condition来分别管理。
在Condition对象中,与wait notify notifyAll对应的接口是await signal signalAll,名字是不一样的。其实Condition也有wait接口(继承于Object),但该接口提供的条件队列是关联于Condition本身的锁,而不是生成Condition的锁,使用接口时要特别留意。

Condition的公平性依赖于锁的公平性:Condition是否阻塞取决于是否可获得背后管理的锁,所以获取锁的公平性也决定了Condition的公平性(Condition调用signal时,只是将Condition管理的条件队列放入到Lock执行调度的队列,而具体是否能公平执行依赖于Lock的公平性)
如果需要使用一些高级功能,比如公平队列(构造公平的锁,然后根据公平的锁创建Condition),或者在每个锁上对应多个等待线程集,那就需要使用Condition而不是内置条件队列

AbstractQueuedSynchonizer
AQS是大部分同步器类的基类 基于AQS构建的同步器最基本的操作包括各种形式的获取和释放操作, AQS负责管理同步容器类中的状态, 管理了一个整数的状态, 可以通过getState…等方法控制, 这个整数表示任意状态, 例如ReentrantLock中使用它表示获取锁的次数, Semaphore中用它来表示许可的数量
AQS中的accuire等方法都会调用子类的带有try前缀的方法, 来判定某个操作是否能执行,
通过继承AQS来实现自定义的同步器类

系统大多数同步器都基于AbstructQueuedSynchronizer抽象类提供的框架代码进行实现。AQS的实现有如下几个要点:
volatile 变量 state 记录状态。通过CAS的原子非阻塞接口对状态进行高效同步。
基于CLH算法的并发 Sync Queue 管理线程的阻塞和调度。
模板模式。AQS 内部定义获取锁(acquire),释放锁(release)的主逻辑,子类实现相应模版方法。
支持共享和独占两种操作语义。两者的区别在于释放线程时,是否只有一个线程可以被唤醒。独占只有一个线程可以被唤醒,而共享有多个。

子类需要实现的是根据当前的状态来判断是否可以执行获取和释放逻辑。如果是独占操作,需要实现tryAcquire tryRelease isHeldExclusively回调接口,如果是共享操作,需要实现tryAcquireShared tryReleaseShard接口。

java.util.concurrent同步器类中的AQS
在java.util.concurrent中许多的可阻塞的类例如: ReentrantLock, Semaphore,
ReentrantReadWriteLock, CountDownLatch, FutureTask等, 都是基于AQS构建的, 他们都实现了AQS中的各种方法,来定义自己内部的逻辑, 比如ReentrantLock因为只支持独占的方式, 因此实现了tryAcquire tryRelease isHeldExclusively

Synchonized实现原理
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样.代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另一种方式实现的,细节JVM规范里并没有详细说明.
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对.任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态.线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁.

第十五章 原子变量与非阻塞同步机制
AtomicInteger等
CAS实现, ABA问题解决

第十六章 Java内存模型
重排序问题与Happens-Before有关系

Happens-Before(先行发生原则)
Java内存模型为程序中一系列操作定义了一个偏序关系, 称为Happens-Before, 要想保证操作B的线程看到操作A的线程的结果, 那么A和B直接必须满足Happens-Before, 如果两个操作之间缺乏Happens-Before关系, 那么JVM可以对它们任意重排序

Happens-Before包括八个
1.程序次序规则:在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生于编写在前面的操作之后。
这句话看起来是程序按照编写的顺序来执行,但是虚拟机还是可能对程序代码的指令重排序,只要保证一个线程内最终的结果和代码顺序执行的结果一致即可。

2.锁定原则:一个unlock操作要先行发生于对同一个锁的lock操作。
无论是单线程还是多线程的环境下,如果同一个锁是锁定状态,那必须先对其执行释放操作之后才能继续执行lock操作。

3.volatile变量规则:对一个变量的写操作要早于对这个变量的读操作。
如果一个变量使用volatile关键词修饰,一个线程怼他进行读操作,一个线程对他进行写操作,那么写操作肯定要先行发生于读操作。

4.传递规则:如果操作A先于操作B,而操作B又先于操作C,则可以得出操作A肯定先于操作C。

5.线程启动原则:Thread对象的start()方法先行发生于对该线程的任何动作,只有start之后线程才能真正运行,否则Thread也只是一个对象而已。

6.线程中断规则:对线程执行interrupt()方法肯定要优先于捕获到中断信号。如果线程收到了中断信号,那么在此之前势必要有interrupt()。

7.线程终结原则:线程中所有的操作都要先行发生于线程的终止检测,通俗的讲,线程的任务执行、逻辑单元执行肯定要发生于线程死亡之前。

8.对象的终结规则:一个对象的初始化完成先行于finalize()方法之前。

发布
当缺少Happens-Before关系时候, 就可能出现重排序问题, 导致不安全的发布, 这就使一个线程看到一个只被部分构造的对象,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值