Java并发编程
标签(空格分隔): 并发 多线程
基础
线程
在执行过程中,能够执行程序代码的一个执行单元,在Java语言中,线程有四种状态:运行,就绪,挂起,结束。
并发特性
-
原子性
一个操作不会被线程调度机制打断,要么操作中的指令全部执行完毕,要么全部不执行,中间不会有任何的线程切换.
-
可见性
一个线程对变量的值进行了修改,其他线程能够立即得知这个修改.
-
有序性
有序性就是指程序按照代码的先后顺序执行.编译器为了优化性能,有时候会改变程序中语句的先后顺序.Java提供了volatile和synchronized两个关键字来保证线程之间的操作的有序性.
并发核心问题
-
分工
所谓分工,类似于现实中一个组织完成一个项目,项目经理要拆分任...
-
同步
指的就是线程间的协作,本质上和现实生活中的协作没区别,一个线程执行完一个任务后,如何通知执行后续任务的线程开工.
-
互斥
指同一时刻,只允许一个线程访问共享变量
线程安全
线程安全类
指在单个线程或多个线程访问这个类时, 这个类始终都表现出正确的行为.
竞态条件
在并发编程中,由于不恰当的执行时序而出现不正确的结果,这种情况称为**竞态条件**.
数据竞争
在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现**数据竞争**.当一个线程写入一个变量,而另一个线程接下来读取这个变量,或者读取一个由另一个线程写入的变量时,并且在这两个线程之间没有使用同步,那么就可能出现**数据竞争**.
volatile关键字
使用volatile关键字修饰的变量会强制将修改的值立即写入到主存,主存中值的更新会使缓存(线程栈中的变量副本)中的值失效(读取时会直接从内存中读取新值),volatile会禁止指令重排.具有可见性,有序性不具备原子性.
synchronized关键字
synchronized关键字用在非静态方法上或代码块,则称为方法锁/对象锁.它们锁的是this,即当前对象.而用在静态方式中,则为类锁,锁的是当前类的Class.
java的内置锁
每个java对象都可以用做一个实现同步的锁,这些锁称为内置锁.线程进入同步代码块或方法时会自动获取该锁,在退出同步代码块或方法时会释放该锁.获得该内置锁的唯一途径就是进入这个锁的保护同步代码块或方法.
内置锁是一个互斥锁,意味着最多只有一个线程能够获得该锁,当线程A尝试去获取线程B持有的内置锁时,线程A必须等待或堵塞,直到线程B释放这个锁,若线程B不释放锁,线程A就会一直等下去,这种情况称为死锁.
线程栈
每一个线程运行时都有一个线程栈,线程栈保存了线程运行时的变量值信息.**当线程访问某一个对象的值时,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值加在到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值写回到对象在中变量**.
最低限的安全性
当一个线程在没有同步的情况下读取变量,它可能会得到一个过期值.但是至少它可以看到某个线程在那里设定的一个真实数值,而不是一个凭空而来的值.这样的安全保证被称为是最低限的安全性.
volatile的使用标准
- 写入变量时并不依赖变量的当前值;或能够确保只有单一的线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
- 访问变量时,没有其他的原因需要加锁
共享对象
发布
发布一个对象的意思是**使它能够被当前范围之外的代码所使用.可以通过公共静态变量,非私有方法,构造方法内隐含引用三种方式**.
// 当一个对象能够给其他代码引用。即为发布
public class Publish {
private HashMap<String, Object> map = null;
// 可通过get方法能得到它的引用
public HashMap<String, Object> getMap() {
return map;
}
public void setMap(HashMap<String, Object> map) {
this.map = map;
}
public Publish() {
map = new HashMap<String, Object>();
}
}
逸出
**若对象构造完成之前就发布该对象,就会破坏线程安全性.当某个不该发布的对象被发布时,这种情况被称为逸出**.
// 逸出
public class Escape {
private int id = 0;
private String name = null;
public Escape() {
// 其中 name属性还未被赋值, 就进行发布操作
new Thread(new MyClass()).start();
new Thread(new MyClass()).start();
name = "ray";
}
private class MyClass implements Runnable {
@Override
public void run() {
System.out.println(Escape.this.name);
System.out.println(Escape.this.id);
}
}
public static void main(String[] args) {
new Escape();
}
}
// 输出
//null 0 null 0
线程封闭
多线程访问共享数据为了安全性通常需要同步,若仅在单线程内访问数据就不需要同步,这种避免共享数据的技术称为线程封闭.
使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
Ad-hoc线程限制
指维护线程限制性的任务全部落在实现上.这是靠实现者控制的线程封闭.它的线程封闭完全靠实现者实现,Ad-hoc线程封闭非常脆弱,没有任何一种语言特性能将对象封闭在目标线程上.
栈限制
栈限制是线程限制的一种特性,是我们编程当中遇到最多的线程封闭.即局部变量,当多个线程访问一个方法,此方法中的局部变量都会被拷贝一份放到线程栈中,所以局部变量是不被多个线程共享的.
不可变性
-
它的状态不能在创建后再被修改
-
所有域都是final类型
-
它被正确创建(创建期间没有发生this引用的逸出)
即对象的状态不会发生变化,无法被修改.**不可变对象永远是线程安全的**.
安全发布
- 通过静态初始化器初始化对象的引用
- 将它的引用存储到volatile域或AtomicReference
- 将它引用存储到正确创建的对象的final域中
- 将它的引用存储到由锁正确保护的域中
高效不可变对象
一个对象的状态不会在发布后被修改,这种对象被称为高效不可变对象.
发布对象的必要条件依赖于对象的可变性
- 不可变对象可以通过任意机制发布
- 高效不可变对象必须要安全发布
- 可变对象必须要安全发布,同时必须要线程安全或是被锁保护
组合对象
类的不变式
指类中各数据间的关系,表示一个类存在的正当性.不变式是创建类的必要条件,因为类的责任就是维护不变式.
例如vector知道自己有n个元素,vector也知道自己有一个指针指向这些元素,以上两点就是不变式.若vector实际上竟然有n+1个元素,就出问题了.如果vector包含的指针为0,也表示有bug,因为该指针并未指向任何东西,这就表示了它违背了一个不变式。
不变约束
对象的任何一个方法都是完成(不管是自己的还是别的对象的)属性的“读”和“写”。只有“写”性质的方法才可能引起对象状态的变化。所以可以在具有“写”性质的方法中**检查属性的合法性。**
你可能听到过操作的前置条件和后置条件:就是说欲执行此操作必须满足一个条件,操作执行完后还须检查另一个条件,以便确保不会把对象置于不合法的状态,用后置条件加上回滚机制可以实现rollback语义。
通过在类的每个公共方法的入口和出口处同时对不变约束进行检查可以动态地检查程序是否遵从了计算的不变约束。(仅供参考)
设计线程安全类包含基本要素
- 确定对象状态是由哪些变量构成的
- 确定限制状态变量的不变约束
- 制定一个管理并发访问对象状态的策略
阻塞
指一个线程在执行过程中暂停,以等待某个条件的触发
实例限制
一个对象封装了另一个对象,所有访问被封装对象的代码路径就是全部可知的.将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁.
public class PersonSet{
private final Set<Person> mySet = new HashSet<Person>();
public synchronized void addPerson(Person p){
mySet.add(p);
}
pulbic synchronized boolean containsPerson(Person p){
return mySet.contains(p);
}
}
线程监视器(monitor)
监视器用来监视线程进入特别房间,他确保同一时间只能有一个线程可以访问特殊房间中的数据和代码。
java监视器模式
线程限制原则的直接推论之一是Java监视器模式.遵循Java监视器模式的对象封装了所有的可变状态, 并由对象自己的内部锁保护.
Java监视器模式仅仅是一种习惯约定;任意锁对象,只要始终如一地使用,都可以用来保护对象的状态.
public final class Counter{
private long value = 0;
public synchronized long getValue(){
return value;
}
public synchronized long increment(){
if(value == Long.MAX_VALUE)
throw new IllegalStateException("counter overflow");
return ++value;
}
}
同步策略的文档化
为类的用户编写类线程安全性担保的文档;为类的同步策略文档.
CAS
Compare-and-Swap,即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都是用了CAS技术.CAS也是现在面试机场问到的问题.
例如: 订单修改功能.有个订单两个人同时进行了修改可能就会出现bug,为了解决这种问题,在订单表里存一个字段version,表示订单的版本,查询订单时把版本也查出来,更新时的更新条件带入version,若数据库中的版本和当初取出来的版本一致才会更新,否则就什么都不做,整个比较并替换的操作是一个原子操作.
委托线程安全
可以理解为把类的线程安全性交给了类里的变量,当类里的变量是线程安全的,那么该类也是线程安全的.
称之为把类的线程安全性委托给了变量
构建块
委托是创建线程安全类最有效的策略之一:只需要用已有的线程安全类来管理所有状态(变量)即可.
同步容器
同步容器包括两部分,一个是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);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedList<>(list.subList(fromIndex, toIndex),
mutex);
}
}
同步容器中出现的问题
同步容器都是线程安全的.但是对于复合操作,可能还是需要额外加锁进行保护.通常对容器的符合操作包括:迭代,导航以及条件运算.在一个同步的容器中,这些复合操作即使没有额外加锁,技术上也是线程安全的,但当其他线程能够并发修改容器时,它们就可能不会按照期望的方式工作了.
并发容器
Java5.0通过提供几种并发的容器类来改进同步容器.同步容器通过对容器的所有状态进行串行访问,从而实现了它们的线程安全,这样做的代价是削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低.
另一方面,并发容器是为多线程并发访问而设计的,Java 5.0添加了ConcurrentHashMap, 来替代同步的哈希Map实现;当多数操作为读取操作时,CopyOnWriteArrayList是List相应的同步实现.新的ConcurrentMap接口加入了对常见符合操作的支持,比如"缺少即加入",替换和条件删除.
用并发容器替换同步容器,这种做法以有很小风险带来了可扩展性显著的提高.
Queue
用来临时保存正在等待被进一步处理的一系列元素.Queue的操作并不会阻塞;若队列是空的,那么从队列中获取元素的操作会返回空值(null).
BlockingQueue
BlockingQueue扩展了Queue,增加了可阻塞的插入和获取操作.如果队列是空的,一个获取操作会一直阻塞,直到队列中存在可用元素;若队列是满的(对于有界队列),插入操作会一直阻塞直到队列中存在可用空间.阻塞队列在**生产者-消费者**设计中非常有用.
ConcurrentHashMap
ConcurrentHashMap和HashMap一样是一个哈希表,但是它使用完全不同的锁策略,可以提供更好的并发性和可伸缩性.在ConcurrentHashMap以前,程序使用一个公共锁同步每一个方法,并严格地限制只有一个线程可以同时访问容器.而ConcurrentHashMap使用一个更加细化的锁机制,名叫**分离锁**.
分离锁
这个机制允许更深层次的共享访问.任意数量的读线程可以并发访问Map,读者和写者也可以并发访问Map,并且有限数量的写线程还可以并发修改Map.结果是,为并发访问带来更高的吞吐量,同时几乎没有损失单个线程访问的性能.
CopyOnWriteArrayList和CopyOnWriteArraySet
CopyOnWriteArrayList是同步List的一个并发替代品,CopyOnWriteArraySet是同步Set的并发替代品,通常情况下它们提供了更好的并发性,并避免了迭代期间对容器加锁和复制."写入时复制(copy-on-write)"容器的线程安全性来源于这样一个事实,只要有效的不可变对象被正确发布,那么访问它将不再需要更多的同步.在每次需要修改时,它们会创建并重新发布一个新的容器的拷贝,以此来实现可变性.
这类容器的迭代器保留了一个底层基础数组的引用.这个数组作为迭代器的起点,永远不会被修改.因此对它的同步只不过是为了确保数组内容的可见性.因此,多个线程可以对这个容器进行迭代,并且不会受到另一个或多个想要修改容器的线程带来的干涉.
阻塞队列(Blocking Queue)
阻塞队列提供了可阻塞的put和take方法,它们与可定时的offer和poll是等价的.如果Queue已经满了,put方法会被阻塞直到有空间可用;如果Queue是空的,那么take方法会被阻塞,直到有元素可用.Queue的长度可以有限,也可以无限;无限的Queue永远不会填满,所以它的put方法永远不会阻塞.
有界队列
即有限队列.有界队列是强大的资源管理工具,用来建立可靠的应用程序:它们遏制那些可以产生过多工作量,具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮.
双端队列
Deque和BlockingDeque,它们分别扩展了Queue和BlockingQueue.Deque是一个双端队列,允许高效地在头和尾分别进行插入和移除.实现它们的是ArrayDeque和LinkedBlockingDeque.
窃取工作模式
一个消费者生产者设计中,所有的消费者只共享一个工作队列;在窃取工作的设计中,每个消费者都有一个自己的双端队列.若一个消费者完成了自己双端队列中的全部工作,它可以窃取其他消费者的双端队列中的末尾的任务.因为工作者线程并不会竞争一个共享的任务队列,所以窃取工作模式比传统的生产者-消费者设计有更佳的可伸缩性.
线程阻塞或暂停的原因:
-
等待I/O操作结束
-
等待获得一个锁
-
等待从Thread.sleep中唤醒
-
等待另一个线程的计算结果
当一个线程阻塞时,它通常被挂起,并被设置成线程阻塞的某个状态(BLOCKED,WAITING,TIMED_WAITING). 一个阻塞的操作和普通的操作之间的差别仅在于:被阻塞的线程必须等待一个事件的发生才能继续进行,并且这个事件是超越它自己控制的,因而需要花费更长的时间(例如:等待I/O操作完成,等待外部计算结束).当外部事件发生后,线程被置回RUNNABLE状态,重新获得调度机会.
中断
当一个方法能够抛出**InterruptedException**的时候,是在告诉你这个方法是一个可阻塞方法,如果它被**中断**,将可以提前结束阻塞状态.
Thread提供了interrupt方法,用来中断一个线程,或者查询某线程是否中断.每个线程都有一个布尔类型的属性,这个属性代表了线程的中断状态;中断线程时需要设置这个值.
**中断是一种协作机制**.一个线程不能够迫使其他线程停止正在做的事或去做其他事情;当**线程A** 中断 **线程B**,A仅仅是要求B在达到某一个方便停止的关键点时,停止正在做的事情.
当你的代码中调用了一个会抛出**InterruptedException**的方法,你自己的方法也就成为了一个阻塞方法,要为响应中断做好准备.有两种选择:
**传递InterruptedException**,若你能侥幸避开异常的话,这通常是最明智的策略,只需要把异常传递给你的调用者,这可能包括不捕获异常,也可能是先捕获,进行特定的清理,再抛出.
**修复中断**.当你不能抛出InterruptedException时,你必须捕获它,并且在当前线程中调用interrput从中断中恢复,这样调用栈中更高层的代码可以发现中断已经发生.
Synchronizer
Synchronizer是一个对象,它根据本身的状态调节线程的控制流.阻塞队列可以扮演一个Synchronizer的角色;其他类型Synchronizer包括信号量,关卡以及闭锁.
所有的Synchronizer都有类似的结构特性:它们封装状态,而这些状态决定着线程执行到在某一点时是通过还是被迫等待;它们提供操作状态的方法,以及高效地等待Synchronizer进入到期望状态的方法.
闭锁
闭锁是一种Synchronizer,它可以延迟线程的进度直到线程到达终点状态.一个闭锁工作就像是一道门,例如 一群人逛超市,超市门开之前只能在门口等着,当**超市管理员(闭锁)**到达超市开门,才能进去逛.不过一旦**超市管理员(闭锁)**到达了终点状态(到达超市并开门),它就不能再改变状态了(超市管理员不能关门),所以它会永远保持敞开状态.
闭锁可以用来确保特定活动直到其他活动完成后才发生,比如:确保一个服务不会开始,直到它依赖的其他服务都已经开始.
CountDownLatch
一个灵活的闭锁实现.允许一个或多个线程等待一个事件集的发生.闭锁的状态包括一个计数器做减操作,表示一个事件已经发生了,而await方法等待计数器达到零,此时所有需要等待的时间都已发生.若计数器入口时值为非零,await会一直阻塞直到计数器为零或等待线程中断以及超时.
FutureTask
FutureTask同样是一个闭锁.(FutureTask的实现描述了一个抽象的可携带结果的计算).FutureTask的计算是通过Callable实现的,它等价于一个可携带结果的Runnable,并且有3个状态:等待,运行和完成.完成包括所有计算以任意的方式结束.一旦进入完成状态,它会永远停在这个状态.
Future.get的行为依赖于任务的状态,若它已经完成,get可以立刻获得返回结果,否则会被阻塞直到任务转入完成状态,然后会返回结果或抛出异常.
信号量(Semaphore)
**计数信号量(Counting semaphore)**用来控制能够同时访问某特定资源的活动的数量,或者同时执行某一给定操作的数量.计数信号量可以用来实现资源池或者给一个容器限定边界.
一个信号量管理一个有效的许可(permit)集;许可的初始量通过构造函数传递给信号量.活动能够获得许可(只要还有剩余许可),并在使用之后释放许可.若已经没有可用许可,那么acquire会被阻塞,直到有可用的为止(或被中断或超时).release方法向信号量返回一个许可(acquire是消费一个许可,release是创建一个许可,许可数量不受semaphore限制).
关卡(Barrier)
关卡类似于闭锁,它们都能够阻塞一组线程,直到某些事件发生.其中关卡与闭锁关键的不同在于,所有线程必须**同时**到达关卡点,才能继续处理.闭锁等待的是**事件**;关卡等待的是**其他线程**.
并发诀窍
- 所有并发问题都归结为如何协调访问并发状态.可变状态越少,保证线程安全就越容易
- 尽量将域声明为final类型,除非它们的需要是可变的
- 不可变对象天生是线程安全的
- 封装使管理复杂度变得更可行
- 用锁来守护每一个可变变量
- 对同一不变约束中的所有变量都使用相同的锁
- 在运行复合操作期间持有锁
- 在非同步的多线程情况下,访问可变变量的程序是存在隐患的
- 不要依赖于可以需要同步的小聪明.
- 在设计过程就考虑线程安全,或在文档中明确说明它不是线程安全的.
- 文档化你的同步策略.
构建并发应用程序
大多数并发应用程序是围绕执行任务(task)进行管理的.所谓任务就是抽象,离散的工作单元.把一个应用程序的工作分离到任务中,可以简化程序的管理;这种分离还在不同事务间划分了自然地分界线,可以方便程序在出现错误时进行恢复;同时这种分离还可以为并发工作提供一个自然的结构,有利于提高程序的并发性.
Executor框架
Executor只是个简单的接口,但它却为一个灵活而且强大的框架创造了基础,这个框架可以用于异步任务执行,而且支持很多不同类型的任务执行策略.它还为**任务提交**和**任务执行**之间的解耦提供了标准的方法,为使用Runnable描述任务提供了通用的方式.Executor的实现还提供了对生命周期的支持以及钩子函数,可以添加诸如统计收集,应用程序管理机制和监视器等扩展.
Executor基于生产者-消费者模式.提交任务的执行者是生产者,执行任务的线程是消费者.**如果要在你的程序中实现了生产者-消费者的设计,使用Executor通常是最简单的方式**.
public class Main {
private static final int THREADS = 100;
/*** 创建一个有界线程池 **/
private static final Executor exec = Executors.newFixedThreadPool(100);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true){
Socket accept = socket.accept();
Runnable runnable = () -> {
handleRequest(accept);
};
// 执行任务
exec.execute(runnable);
}
}
public static void handleRequest(Socket socket){
System.out.println("----->socket");
}
}
线程执行策略
-
所有任务在单一的线程中顺序执行
-
每个任务在自己的线程中执行
-
将任务的提交与任务的执行体进行解耦(Executor)
**执行策略是资源管理工具**. 最佳策略取决于可用的计算资源和你对服务质量的需求.通过限制并发任务的数量,你能够确保应用程序不会由于资源耗尽而失败,大量任务也不会在争夺稀缺资源时出现性能问题.将任务的提交与任务的执行策略规则进行分离,有助于部署阶段选择一个与当前硬件最匹配的执行策略.
线程池
线程池管理一个工作者线程的同构池,线程池与工作队列紧密绑定的.所有工作队列,其作用是持有所有等待执行的任务.工作者线程从工作队列中获取下一个任务,执行它,然后回来继续等待另一个线程.
Executor的生命周期
运行 关闭 终止
最初创建的时候初始状态是运行状态.shutdown方法会启动一个平缓的关闭过程:停止接收新的任务,同时等待已经提交的任务完成--包括尚未开始执行的任务.shutdownNow方法会启动一个强制的关闭过程:尝试取消所有运行中的任务和排在队列中尚未开始的任务.
Executor实现通常只是为了执行任务而创建线程,但**JVM会在所有线程全部终止后才推出,因此无法正确关闭Executor,将会阻止JVM的结束**.
延迟的,并具周期性的任务
Timer工具管理任务的延迟执行("100ms后执行该任务")以及周期执行("每10ms执行一次该任务").
但Timer存在一些缺陷:
Timer只创建一个线程来执行所有timer任务,顺序执行任务,若一个任务很耗时,会导致其他任务的时效准确性出问题.调度线程池(Schedule thread pool)解决了这个缺陷,它让你可以提供多个线程来执行延迟,并具周期性的任务.
若TimerTask抛出未检查的异常,Timer将会产生无法预料的行为.Timer线程并不捕获异常,所以TimerTask抛出的未检查的异常会终止timer线程.这种情况下,Timer也不会再重新恢复线程的执行了;它会认为整个Timer都被取消了.此时,已经被安排但尚未执行的TimerTask永远不会再执行,新的任务也不会被调用,称之为"线程泄漏".所以可以考虑用ScheduledThreadPoolExecutor作为替代品.
可携带结果的任务: Callable 和 Future
Callable
Callable和Runnable一样描述的是抽象的计算型任务,这些任务通常是有限的:它们有一个明确的开始点,而且最终会结束.
Future
一个Executor执行的任务的生命周期有四个阶段:**创建,提交,开始和完成**.
Future描述了任务的生命周期,并提供方法来获得任务的结果,取消任务以及检查任务是否已完成还是被取消.
Future的任务是单向的,一旦任务完成,它就永远停留在完成状态上.
可以通过get方法来获取任务的执行结果,但**任务的状态决定了get方法的行为**.若任务执行完成,get会立刻返回一个Exception.若任务没有完成,get会阻塞到它完成.若任务抛出异常,get会将该异常封装为ExecutionException,然后重新抛出;若任务取消,get会抛出CancellationException.当抛出ExecutionException时,可调用getCause重新获得被封装的原始异常.
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没有提供能安全地强迫线程停止工作的机制.它提供了**中断**--->一个协作机制,让一个线程通知另一个线程停止当前工作.
当通知接收到后,它们首先会清除当前进程的工作,然后再终止.
任务取消
当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动被称之为**可取消的**.例如:用户请求的取消,限时活动,应用程序事件,错误,关闭.
ExecutorService的关闭
ExecutorService提供了关闭的两种方式:使用shutdown优雅的关闭和利用shutdownNow强行关闭.在强行关闭中,shutdownNow首先尝试关闭当前正在执行的任务,然后返回待完成任务的清单.
正常关闭
import net.jcip.annotations.GuardedBy;
import javax.annotation.Generated;
import java.io.PrintWriter;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Logger;
/**
* @author: Keben
* @date: 2019/5/28 18:14
*/
public class LogWriter {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
@GuardedBy("this")
private boolean isShutdown;
@GuardedBy("this")
private int reservations;
public LogWriter(BlockingQueue<String> queue, LoggerThread loggerThread, PrintWriter writer) {
this.queue = queue;
this.loggerThread = loggerThread;
this.writer = writer;
}
public void start() {
loggerThread.start();
}
public void stop() {
synchronized (this) {
isShutdown = true;
}
loggerThread.interrupt();
}
public void log(String msg) throws InterruptedException {
synchronized (this) {
if (isShutdown)
throw new IllegalStateException();
++reservations;
}
queue.put(msg);
}
class LoggerThread extends Thread {
@Override
public void run() {
while (true) {
try {
synchronized (LogWriter.this) {
if (isShutdown && reservations == 0)
break;
}
String msg = queue.take();
synchronized (LogWriter.this) {
--reservations;
}
writer.println(msg);
} catch (InterruptedException e) {
} finally {
writer.close();
}
}
}
}
}
致命药丸
另一种保证生产者和消费者服务关闭的方式是使用致命药丸:一个可识别的对象,置于队列中,意味着"当你得到它时,停止一切工作".在先进先出队列中,致命药丸保证了消费者完成队列中关闭之前的所有任务,因为所有早于致命药丸提交的工作都会在处理它之前就完成了,生产者不应该在提交了致命药丸后,再提交任务工作.
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
/**
* @author: Keben
* @date: 2019/5/28 18:43
*/
public class IndexingService {
private static final File POISON = new File("");
private final IndexerThread indexerThread = new IndexerThread();
private final CrawlerThread crawlerThread = new CrawlerThread();
private final BlockingQueue<File> queue;
private final FileFilter fileFilter;
private final File root;
public void start(){
indexerThread.start();
crawlerThread.start();
}
public void stop(){
crawlerThread.interrupt();
}
class CrawlerThread extends Thread {
@Override
public void run() {
try {
crawl(root);
} catch (InterruptedException e) {
} finally {
while (true) {
try {
queue.put(POISON);
break;
} catch (InterruptedException e1) {
}
}
}
}
private void crawl(File root) throws InterruptedException {
System.out.println("处理");
}
public void awaitTermination() throws InterruptedException{
indexerThread.join();
}
}
class IndexerThread extends Thread {
@Override
public void run() {
while (true) {
try {
File file = queue.take();
if (file == POISON)
break;
else
indexFile(file);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
shutdownNow的局限性
当通过shutdownNow强行关闭一个ExecutorService时,它试图取消正在进行的任务,并返回那些已经提交,但并没有开始的任务的清单,这样,这些任务可以被日志记录,或存起来等待进一步处理.
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Runnable> runnables = executorService.shutdownNow();
处理异常的反常终止
有没有发生过这样的情况,你写的工作线程莫名其妙的挂了,如果不是被你刚好看到,拿只能抓瞎了,不知道啥原因了,因为异常的时候只会把stack trace打在控制台上,不会记在你想记得错误日志里,头皮都抓破了也没能找到确切的地方,最后只能在能加try catch 的地方都给加上。
import java.util.logging.Level;
import java.util.logging.Logger;
public class UEHLogger implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
}
}
public class UEHLoggerTest {
public static void main(String[] args) {
Thread thread = new Thread(new WorkTask());
thread.setUncaughtExceptionHandler(new UEHLogger());
// 若workTask出现未捕获异常,则会执行UEHLogger中的uncaughtException方法
thread.start();
}
}
class WorkTask implements Runnable{
@Override
public void run() {
System.out.println("test");
}
}
JVM关闭
JVM即可以通过正常手段关闭,也可以强行关闭.当最后一个"正常"线程终结时,或当有人调用System.exit时,以及通过使用其他与平台相关手段时(如:发送了SIGINT,或键入Ctrl-C),都可以开始一个正常关闭.
尽管JVM可以通过这些标准方式关闭,它仍然能通过调用Runtime.halt或"kill"JVM的操作系统进程被强行关闭.
线程钩子
钩子的本质是一段用于处理系统消息的程序,通过系统调用把它挂入系统.钩子种类很多,每种钩子可以截获并处理相应的消息.每当特定消息发出,在到达目的窗口之前,钩子程序先行截获该消息,得到对此消息的控制权.此时钩函数可以对截获消息进行.
关闭钩子
当jvm关闭的时候,会执行系统中已经设置的所有调用addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作.
public void start(){
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
try{ UEHLogger.this.stop*(; }
catch (InterruptedException ignored) {}
}
})
}
精灵线程
当你需要创建一个线程,执行一些辅助工作,但你又不希望这个线程的存阻碍JVM的关闭,这时你就需要用到精灵线程.
线程被分为两种:普通线程和精灵线程.JVM启动时会创建所有的线程,除了主线程以外,其他的都是精灵线程(例如垃圾回收器和其他类似线程).当一个新线程创建时,新线程继承了创建它的线程的后台状态,所以默认情况下,所有主线程创建的线程都是普通线程.
普通线程和精灵线程的区别在于:
当一个线程退出时,JVM会检查一个运行中的线程的详细清单,若仅剩下精灵线程,它会发起正常退出.当JVM停止时,所有仍存在的精灵线程都会被抛弃,不会执行finally块,也不会释放栈-->JVM直接退出.
Finalizer
当我们不需要资源后,垃圾回收器重新获得内存资源是非常有益的,但一些资源,当我们不需要时,必须显式地归还给操作系统(如文件或Socket).为了达到目的,垃圾回收器对这些具有特殊finalize的方法的对象会进行特殊对待:在回收器获得它们后,finalize被调用,这样就能保证持久化的资源可以被释放.
因为finalizer可以运行在一个JVM管理的线程中,任何finalizer访问的状态都会被多个线程访问,因此必须被同步.finalizer运行时不提供任何保证,并且复杂的finalizer会带来巨大的性能开销.
**避免使用finalizer**.
死锁
当一个线程永远占有一个锁,而其他线程去尝试去获得这个锁,那么它们将永远被阻塞.
锁顺序死锁
当两个线程试图通过**不同的顺序**获得多个相同的锁.若请求锁的顺序相同,就不会出现循环的锁依赖线程,也就不会产生死锁.
public class LeftRightDeadLock{
private final Object left = new Objcet();
private final Object right = new Object;
public void leftRight(){
synchronized(left){
synchronized(right){
// 执行任务
}
}
}
public void rightLeft(){
synchronized(right){
synchronized(left){
// 执行任务
}
}
}
}
动态的锁顺序死锁
两个线程同时调用一个方法(传递相反顺序的相同参数),同样会出现相互等待获取对方的锁的情况,从而导致死锁.
public void transferMoney(Account from,Account to){
synchronized(from){
synchronized(to){
// 执行转账
}
}
}
这个时候如果两个线程同时调用transferMoney,一个从X向Y转账,一个从Y向X转账,就会发生死锁.: A: transferMoney(X,Y);
B: transferMoney(Y,X);
private static final Object tieLock = new Object();
public void transferMoney(final Account from, final Account to) {
class Helper {
public void transfer() throws InsufficientResourcesException {
// 执行转账
}
}
int fromHash = System.identityHashCode(form);
int toHash = System.identityHashCode(to);
if (fromHash < toHash){
synchronized (from){
synchronized (to){
new Helper().transfer();
}
}
}else if(toHash<fromHash){
synchronized (to){
synchronized (from){
new Helper().transfer();
}
}
}else{
synchronized (tieLock){
synchronized (from){
synchronized (to){
new Helper().transfer();
}
}
}
}
}
线程饥饿死锁
在一个线程池中,若所有线程执行的任务都阻塞在线程池中,等待着仍然处于同一工作队列的其他任务.这被称作**线程饥饿死锁**.
耗时操作
若任务由于过长的时间周期而阻塞,那么即使不可能出现死锁,线程池的响应性也会变差.耗时任务会造成线程池阻塞,还会拖长时间.为此我们可以**限定任务等待资源的时间**来缓解耗时操作带来的影响.
定制线程池的大小
定制线程池的长度仅需要避免"过大"和"过小"这两种极端情况.若一个线程池过大,那么线程对稀缺的CPU和内存资源的竞争,会导致内存的高使用量,还会耗尽资源,若过小,由于存在很多可用的处理器资源却未在工作,会对吞吐量造成损失.
ThreadPoolExecutor
ThreadPoolExecutor为一些Executor(newCachedThreadPool,newFixedThreadPool 和newScheduledThreadExecutor)提供了一些基本的实现.ThreadPoolExecutor是一个灵活的,健壮的池实现,允许各种各样的用户定制.
饱和策略
当一个有限队列充满后,饱和策略就会触发.
**中止策略**,会引起execute抛出未检查的RejectedExecutionException;调用者可以捕获这个异常,然后编写能满足自己需求的处理代码.当最新提交的任务不能进入队列等待执行时,**遗弃策略**会默认放弃这个任务;**遗弃最旧的策略**选择丢弃的任务,是本应该接下来就执行的任务,该策略还会重新提交新任务.(如果工作队列是优先级队列,那么"遗弃最旧的"策略选择丢弃的刚好是优先级最高的元素,所以混合使用"遗弃最旧的"饱和策略和优先级队列是不可行的).
**调用者运行策略**,既不会丢弃哪个任务,也不会抛出任何异常.它会把一些任务推回调用者那里,以此减缓新任务流.它不会在池线程中执行最新提交的任务,但是它会在一个调用了execute的线程中执行.
当CPU切换时间片时,之前运行的线程会进入到**可运行状态(就绪状态)**,等待下一次的调度重新进入**运行状态**.
GUI
几乎所有的GUI工具集都实现为**单线程化子系统**,意味着所有GUI的活动都被限制在一个单独的线程中,这其中就包括了Swing和SWT.
所有的Swing组件和模型都被限制于事件线程中,所有任务访问它们的代码必须在事件线程中运行.Swing的单线程原则:Swing的组件和模型只能在事件分派线程中被创建,修改和请求.
在GUI程序中,只要任务是短期的,而且只访问GUI对象(或被其他线程限制以及与线程安全的应用程序对象),那么你几乎可以完全忽略线程的问题,在事件线程中做任何事,一定不会出问题的.
有时GUI程序会运行一些耗时任务,这时我们不能直接让它运行在事务线程中,以免失去响应.这时我们可以创建自己的Executor来执行耗时的任务,而且使用Future表现一个耗时任务,可以极大地简化耗时任务的取消.
如果一个数据模型必须被多个线程共享,而且实现一个线程安全模型的尝试却由于阻塞,一致性或复杂度等原因而失败,这时可以考虑运用分拆模型设计.
线程限制不仅仅限制在GUI系统;无论何时,它都可以用作实现单线程化子系统的便利工具.
GUI框架几乎都是作为单线程化子系统实现的,所有与表现相关的代码都作为任务在一个事件线程中运行.因为只有唯一一个线程,耗时任务会损害响应性,所以它们应该在后台线程中运行.
后台线程(守护线程/精灵线程)
后台线程区别于普通线程,普通线程又称为用户线程,只完成用户自己想要完成的任务,不提供公共服务.而有时,我们希望编写一段程序,能够提供公共服务,保证所有用户针对该线程的请求都能有相应.**在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分**.所有的"非后台线程"结束时,程序就终止了,同时会kill所有的后台线程.
开放调用
在持有锁的时候调用一个外部方法很难进行分析,因此是危险的.**当调用的方法不需要持有锁时,这被称为开放调用**.依赖于开放调用的类通常能表现出更好的行为,并且在与那些在调用方法时需要持有锁的类相比,也更易于编写。通过尽可能地使用开放调用,将更易于找出那些需要获取多个锁的代码路径,因此也就更容易确保采用一致的顺序来获得锁。
**开放调用需要使代码块仅被用于保护那些涉及共享状态的操作,如下程序所示,如果只是为了语法紧凑或简单性(而不是因为整个方法必须通过一个锁来保护)而使用同步方法(而不是同步代码块**
class Taxi {
@GuardedBy("this")
private Point location;
@GuardedBy("this")
private Point destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination) {
dispatcher.notifyAvailable(this);
}
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this")
private final Set<Taxi> taxis;
@GuardedBy("this")
private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public Image getImage() {
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t : copy)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
避免和诊断死锁
若一个程序一次至多获得一个锁,那么就不会产生锁顺序死锁.若必须获得多个锁,那么锁的顺序必须是设计工作的一部分:尽量减少潜在锁之间的交互数量,遵守并文档化该锁顺序协议.
监测代码中死锁自由度的策略分为两个部分:首先识别什么地方会获取多个锁(使这个集合尽量小),对这些示例进行分析,确保它们锁的顺序在程序中保持一致,尽可能使用开放调用,这样可以简化分析的难度.
尝试定时的锁
可以使用每个显式Lock类中的定时tryLock特性,来替代使用内部锁机制.在内部锁机制中,只要没有获得锁,就会永远保持等待,而显式的锁使你可以定义超时(timeout)的时间,在规定时间之后没有获得锁就会返回失败.
JVM使用线程转储来帮助我们识别锁的发生.线程转储包含每个运行中线程的栈追踪信息,以及与之相似并随之发生的异常,也包括锁的信息.
其它的活跃度危险
尽管死锁是我们遇见的主要的活跃度危险,并发程序中仍然可能遇见一些其他的活跃度危险,包括:饥饿,丢失信号和活锁.
饥饿
当线程访问它所需要的资源时却被永久拒绝,以至于不能再继续运行,这样就发生了**饥饿**;在Java应用程序中,使用线程的优先级不当可能引起饥饿.在锁中执行无终止的构建也可能引起饥饿,因为其他需要这个锁的线程永远不可能得到它.
Thread.yield()
一个调用yield()方法的线程告诉虚拟机,它乐意让其他线程占用自己的位置,这表明该线程没有再做一些紧急的事情.不过,这仅仅是一个暗示,并不能保证不会产生任何影响.
- yield是一个静态的原生的方法
- yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程
- yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
- 它仅能使一个线程从运行状态转为可运行状态,而不是等待或阻塞状态
弱响应性
不良的锁管理也可能引起弱响应性.若一个线程长时间占用一个锁(可能正在对一个大容器进行迭代,并对每个元素进行耗时的工作),其他想要访问该容器的线程就必须等待很长时间.
活锁
活锁是线程中活跃度失败的另一种形式,尽管没有被阻塞,线程却仍然不能继续,因为它不断重试相同的操作,却总是失败.在并发程序中,通过随机等待和撤回来进行重试能够相当有效地避免活锁的发生.
可伸缩性
当增加计算资源的时候(比如增加额外CPU数量,内存,存储器,I/O带宽),吞吐量和生产相应地得以改进.
** 对性能的追求很可能是并发bug唯一最大的来源.**
当线程因为竞争一个锁而阻塞时,JVM通常会将整个线程挂起,允许它被换出.
线程的挂起
线程进入"非可执行"状态下,在这个状态下CPU不会分给线程时间片,进入这个状态可以用来暂停一个线程的运行。
synchronized和volatile提供的可见性保证要求使用一个特殊的,名为存储关卡的指令,来刷新缓存,使缓存无效,刷新硬件的写缓冲,并延迟执行的传递.存储关卡可能同样会对性能产生影响,因为它们抑制了其他编译器的优化;在存储关卡中,大多数操作是不能被重排序的
锁的粗化
把邻近的synchronized块用相同的锁合并起来.
减少锁的竞争
- 减少持有锁的时间
- 减少请求锁的频率
- 用协调机制取代独占锁,从而允许更强的并发性.
- 缩小锁的范围
显式锁
Lock
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构造函数提供了两种公平性的选择:可以创建**非公平锁(默认)**或者创建**公平锁**.公平锁,在并发环境中,每个线程在获取锁之前都会先查看此锁的等待队列,若为空,则占有锁,否则就会加入到等待队列,而非公平锁则直接会去尝试占有锁,若尝试失败,就再采用公平锁那种方式.
读-写锁
ReentrantLock实现了标准的互斥锁.但互斥锁通常为了保护数据一致性的很强的加锁约束.互斥避免了"写/写"和"写/读"的重叠,但是同样避开了"读/读"的重叠.读-写锁允许:一个资源能够被多个读者访问,或被一个写者访问,两者不能同时进行.
public interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}
读-写锁实现的加锁策略允许多个同时存在的读者,或只存在一个写者.
ReentrantReadWriteLock
为两个锁提供了可重入的加锁语义.和ReentrantLock相同,也能创建非公平锁和公平锁.
在公平锁中,把选择权交给等待时间最长的线程;若锁由读者获得,而一个线程请求写入锁,那么不再允许读者获得读取锁,知道写者释放写入锁.
忙等待或自旋等待
调用者可以不休眠而直接重试take操作
条件队列
条件队列可以让一组线程--称作**等待集**,以某种方式等待相关条件变成真.不同于传统的队列,它们的元素是数据项;条件队列的元素是等待相关条件的线程.
public class BoundedBuffer<V> extends BaseBoundedBuffer<V>{
public BoundedBuffer(int size){ super(size);}
public synchronized void put(V v) throws InterruptedException{
// 阻塞 直到 not-full
while(isFull())
wait();
doPut(v);
notifyAll();
}
public synchronized V take() throws InterruptedException{
while(isEmpty())
wait();
V v = doTake();
notifyAll();
return v;
}
}
条件谓词
条件谓词是先验条件的第一站,它在一个操作与状态之间建立起依赖关系.
// 条件谓词必须被锁守护
void stateDependentMethod() throws InterrputedException{
synchronized(lock){
// 自旋 判断 条件谓词
while(!conditionPredicate())
lock.wait();
// 执行期望任务
}
}
当使用条件等待时(Object.wait 或 Condition.await)
- 永远设置一个条件谓词----一些对象状态的测试,线程执行前必须满足它;
- 永远在调用wait前测试条件谓词,并从wait中返回后再次测试
- 永远在循环中调用wait
- 确保构成条件谓词的状态变量被锁保护,而这锁正是与条件队列相关联的
- 当调用wait,notify或notifyAll时,要持有与条件队列相关的锁,
- 在检查条件谓词之后,开始执行被保护代码的逻辑之前,不要释放锁.
Condition
一个Condition和一个单独的Lock相关联,调用与Condition相关联的Lock的Lock.newCondition方法,就可以创建一个Condition,Condition提供了比内部条件队列要丰富的特征集:每个锁可以有多个等待集,可中断/不可中断的条件等待,基于时限的等待以及公平/非公平队列之间的选择.
public interface Condition{
void await() throws InterruptedException;
boolean await(long time,TimeUnit unit) throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
原子变量类
原子变量类,提供了volatile变量,以支持原子的,条件的读写改操作.
缓存一致性协议
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行操作的时候,会重新从系统内存中将数据读到处理器缓存.
使用volatile变量时,为什么追加64字节能够提高并发编程的效率?
因为对于英特尔酷睿I7,酷睿,Atom和NetBurst,以及Core Solo 和Pentium M处理器的L1,L2或L3缓存的高速缓存行是64个字节宽的,不支持部分填充缓存行,所以如果队列的头结点和尾节点都不足64字节的话,处理器就会把它们都读到同一个高速缓存行中,在多个处理器下每个处理器都会缓存同样的头节点,尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制下,会导致其他处理器也不能访问自己告诉缓存区中的尾节点,而队列的入队和出队操作则需要不停修改头结点,尾节点,所以会严重影响到队列的入队和出队效率.Doug Lea使用追加到64字节的方式来填满告诉缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头,尾节点在修改时不会相互锁定.
// 在JDK1.7的并发包
private transient final PaddedAtomicReference<QNode> head;
private transient final PaddedAtomicReference<QNode> tail;
static final class PaddedAtomicReference <T> extends AtomicReference<T>{
// 使用很多4个字节的引用追加到64字节,一个缓存行的字节宽64字节
Object p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,pa,pb,pc,pd,pe;
PaddedAtomicReference(T r){
supper(r);
}
}
public class AtomicReference <V> implements java.io.Serializable{
// 引用类型 4个字节
private volatile V value;
}
transient关键字
transient关键字修饰的变量不会被序列化.
例如:HashMap中重要的变量
transient Node<K,V>[] table;
查看后续源码发现HashMap实现了自定义的序列化
// 序列化
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
// 反序列化
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
但是HashMap明明实现了Serializable接口,实现了自动序列化的功能,为何还要自定义序列化.
原因:
HashMap实现自定义,有一个重要因素是因为hashCode方法是用native修饰符修饰的,也就是用它跟JVM的运行环境有关,Object的hashCode源码:
public native int hashCode();
也就是说不通的JVM对于同一个key所生成的hashCode可能就不一样,所以数据的内存分布可能不相等了.若我在A虚拟机上通过key的hashCode计算出值在内存1上,在B虚拟机上通过key的hashCode计算出值在内存2上.这样的话在A虚拟机上的序列化,在B虚拟机上进行反序列化,就会读取不到数据.
**当一个对象的物理表示方法与它的逻辑数据内容有实质性差别时,使用默认序列化形式有N种缺陷,所以应该尽可能的根据实际情况重写序列化方法。**
两种不易使用追加到64字节来优化volatile的场景
- 缓存行非64字节宽的处理器
- 共享变量不会被频繁地写
** 这种追加字节的方式在Java7下可能不生效,因为Java7变得更加智慧,它会淘汰货重新排列无用字段,需要使用其他追加字节的方式.
Synchonized实现原理
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样.代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另一种方式实现的,细节JVM规范里并没有详细说明.
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对.任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态.线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁.
Java对象头
synchronized用的锁是存在Java对象头里的.若对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,若对手是非数组类型,则用2字宽存储对象头.在32位虚拟机中,1字宽等于4字节,即32bit.
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组的长度(如果当前对象是数组) |
Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位.下表为32位JVM的Mark Word的默认存储结构.
锁状态 | 25bit | 4bit | 1bit 是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化.Mark Word可能变化为存储以下四种数据.
锁状态 | 25/23/2bit | 4bit | 1bit 是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||
重量级锁 | 指向互斥锁(重量级锁)的指针 | 10 | ||
GC标记 | 空 | 11 | ||
偏向锁 | 线程id | Epoch | 对象分代年龄 | 1 |
在64位虚拟机下,Mark Word是64bit大小的,
|锁状态|25bit|31bit|1bit cms_free|4bit|1bit 是否是偏向锁|2bit 锁标志位|
|-|-|-|-|-|
|无锁|unused|hashCode|||0|01|
|偏向锁|ThreadID(54bit)|Epoch(2bit)|||1|01|
锁的升级.
偏向锁
大多数情况下,锁不紧不存在多线程竞争,而且总是由同一个线程多次获取,为了让线程获取锁的代价更低而引入了偏向锁.
当一个线程访问同步块儿获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步代码块不需要进行CAS操作来加锁和解锁,只需简单测试下对象头里的MarkWord里是否存储指向当前线程的偏向锁,若成果,表示线程已经获得锁.若失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,若设置了,则尝试使用CAS将对象头里的偏向锁指向当前线程.
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁.偏向锁的撤销,需要等待全局安全点(这个时间点上没有正在执行的字节码).它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,若线程不处于活动状态,则把对象头设置为无锁状态,若线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向锁对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁状态或标记对象不适合作为偏向锁,然后唤醒暂停的线程.
关闭偏向锁
偏向锁在Java6和Java7里默认启动的,但它在应用程序启动几秒后蔡激活,若有必要可使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0.若你确定程序里所有的锁通常处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认进入轻量级锁状态.
线程,虚拟机栈,栈帧,方法的对应关系
一个线程拥有一个私有虚拟机栈(堆栈),一个虚拟机栈可以有多个栈帧,一个栈帧对应一个方法的调用.JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作.
线程可以调用多个方法,每调用一个方法,就将方法信息以栈帧方式压栈.
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 |
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 |
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 | 堆
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 | 栈
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 |
栈帧 -> | 局部变量表 | 操作栈 | 动态链接 | 方法出口 | 其他 |
栈帧
栈帧是用于支持虚拟机进入方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈的栈元素.栈帧存储了方法的局部变量表,操作数栈,动态链接,方法返回地址等信息.第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈从入栈到出站的过程.
每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈 都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现.
一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧 (Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。
1.局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。
2.操作数栈
操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的 max_stacks数据项中。操作数栈的每一个元素可以是任意Java数据类型,包括long和double。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。
3.动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的 符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化 称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。
4.方法返回地址
当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调 用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用 athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。
无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上 层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是 要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。
方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
5.附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。
轻量级锁
轻量级锁加锁
线程在执行同步代码块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为"Displaced Mark Word".然后线程尝试使用CAS将对象头里的Mark Word替换为指向锁记录的指针.若成功,当前线程获得锁,若失败,表示其他线程竞争锁,当前线程尝试使用自旋来获得锁.
轻量级锁解锁
轻量级解锁时,会使用CAS操作把Displaced Mark Word替换回到对象头,若成功,表示没有竞争.若失败,表示当前锁存在竞争,锁就会膨胀为重量级锁.
锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒的差距 | 如果线程间存在所竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 若始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行时间长 |