《java并发编程的艺术》摘要
文章目录
- JMM与JVM内存模型
- happens-before
- as-if-serial
- 数据依赖性
- 总线事务
- final
- volatile
- 实现原理
- 语义
- synchronized的实现原理
- 循环cas带来的问题
- 线程状态
- suspend,resume,stop
- 队列同步器
- ReenterLock
- 公平锁与非公平锁
- 读写锁
- HashMap
- 阻塞队列
- CycleBarrier和CountDownLatch
- Semaphore
- Exchanger
- 线程池
- ScheduledThreadPoolExecutor
- FutureTask
- 知识结构图
JMM与JVM内存模型
Java内存模型(JMM):控制线程和主内存之间的关系,线程通过更改本地内存,最终刷新到主内存,其他线程读取主内存数据,来控制线程之间数据可见。在JVM内存模型中,以方法栈来对应线程本地内存,堆对应主内存。
JVM内存模型包含:方法区,堆,程序计数器,方法栈,本地方法区。前两者是线程共享,后三者是线程独占。
happens-before
它是指前一个动作要对后一个动作可见。常见的happens-before规则:一个线程内的程序顺序,锁的释放与下一个锁的获取,volatile变量的写与随后的volatile读,start先于线程的其他操作,A中B.join会导致B的所有操作先于join的返回。happens-before具有传递性。
as-if-serial
它是指无论怎么重排序,程序结果都不会发生改变。
数据依赖性
两个对同一个变量的操作,且至少有一个为写操作,则这两个操作具有数据依赖性,在重排序时,不会对这两个操作重排序。
总线事务
数据需要通过总线在处理器和内存之间传递,在该过程中的一系列步骤称为一个总线事务。从内存读数据到处理器为读事务,从处理器写数据到内存是写事务。由于在一个总线事务处理期间,会禁止其他处理器或者io设备对内存的读写。在多线程请求执行总线事务时,总线会仲裁,选择一个事务先执行,其他线程等待。
在32位处理器中,如果想对64位的数据操作,是需要拆成两个操作,就不能保证原子性(long,double).
final
1.在final变量的写之后,构造函数return之前会有一个storestore屏障。jvm也会禁止final写重排序到构造函数之外。这样会保证在读一个对象时,final域已经初始化过了。
2.因为读final域前会有loadload屏障,所以初次读一个对象和随后的初次读final域不会重排序。
volatile
实现原理
1.lock前缀指令会引起处理器缓存回写到内存。这个过程最终是通过缓存锁定,即锁住内存区域的缓存,并回写到内存实现的。
2.一个处理器的缓存回写到内存会导致其他处理器的缓存无效。因为处理器会通过嗅探技术保证它的内部缓存,系统缓存和其他处理器的缓存在总线上一致。当处理器嗅探到该缓存行处于无效状态(和其他处理器总线数据不一样),则会强行刷新缓存。
语义
1.原子性:只对volatile变量的读写操作保证原子性,对于volatile变量的多操作如++(先读后写)等,不具有原子性。
2.可见性:任意的volatile读总是能看到volatile的最后的写(最新值)。
原理:
1.每个volatile写操作前都有一个storestore屏障。禁止前面的普通写与volatile写发生重排序。
2.每个volatile写操作后都有一个storeload屏障。禁止volatile写与后续的volatile读写发生重排序。这个为什么不放在volatile读前,是因为使用volatile的场景就是多读少写的场景,所以放在写后面可以提高效率。
3.每个volatile读操作前都有一个loadload屏障。禁止下面的普通读与volatile读重排序。
4.每个volatile写操作后都有一个loadstore屏障。禁止下面的写与volatile读重排序。
有时候多个volatile的读写操作会省略不必要的屏障指令。
在AQS当中其实就是使用volatile的state来维护同步状态。
synchronized的实现原理
java对象头包含了hashcode,gc分代年龄,锁标记位等。
通过monitorenter标记同步代码块的开始位置,monitorexit标记同步块的异常和结束位置。任何一个对象都会关联一个monitor。当执行到同步块时,会去获取对象的monitor所有权,一旦持有monitor则说明该线程获取到锁了。
synchronized支持隐式重入—递归。
java通过锁和循环cas实现原子操作。cas是通过CMPXCHG指令实现的。
锁升级过程:
循环cas带来的问题
1.ABA:由A变成B,又由B变成A,检查发现先后值并没有变化。为了解决这种问题,引入了AtomicStampedRefrence,可以记录版本号,看是否发生变化了。
2.循环时间太久:循环太久会消耗cpu,而使用pause指令,可以延迟流水线执行指令,也可以防止内存冲突导致cpu流水线被清空。
3.只能保证一个共享变量的原子操作:可以通过合并共享变量(i=2,j=a,合并后就是ij=2a,cas只要操作ij就行了),或者使用atomicrefrence来保证多个变量的原子性。
线程状态
- yield是将cpu放弃掉,进入就绪状态,所以有可能又被调度。
- 阻塞状态就是等待进入synchronized同步块的状态;因为Lock中使用的是LockSupport相关方法,所以阻塞在Lock中的线程是等待状态。
当线程等待进入运行状态(eg:sleep)时,这个时长是不确定的。
suspend,resume,stop
- 因为suspend的线程并不会释放锁,且不会有时间限制,容易导致死锁,现已废弃suspend和与之对应的resume。
- stop无法保证资源的释放。
synchronized的同步队列和等待队列:当线程没有获取到锁时,成为阻塞状态,且进入同步队列,只有当其他线程(A)释放了锁,才会被A唤醒同步队列的线程。notify是让等待队列中的线程进入同步队列,线程状态由waiting变成blocked。
中断:线程的中断标识,在线程响应了中断后,会在抛出InterruptedException前先清除中断标识位,所以在捕获InterruptedException后,查看isInterrupted也是false状态。
队列同步器
AQS:使用一个volatile int成员变量(state)表示锁的占有状态,通过FIFO双向队列(同步队列)完成线程排队工作。它支持独占式获取与释放同步状态,共享式获取与释放同步状态和查询同步队列中线程的情况。当一个节点获取同步状态失败,就会封装成一个节点,加入到同步队列,一旦同步状态释放,会唤醒首个节点尝试获取同步状态。
Condition(FIFO队列)是等待队列,能够同时存在多个。
- state表示同步状态,state为0表示没有线程持有锁,大于0表示有线程占有锁,也可表示为占用锁的线程数。
- Node中waitStatus属性具有几个特殊值
INITIAL(0)初始状态;
CACELLED(1)线程等待超时或者被中断了,需要从同步队列移除;
SIGNAL(-1)后继节点处于等待状态,如果当前节点被取消或者释放了同步状态,会通知后继节点的线程;
CONDITION(-2)节点处于等待队列condition中,只能在signal()后移入同步队列;
PROPAGATE(-3)下一次共享式同步状态会我无条件的传播下去。)。 - 获取锁:如果成功,则设置为头节点。失败,则入队,阻塞住。
addWaiter: 获取锁失败后,会调用该方法,将当前线程包装成一个node,加入到队尾。exclusive是null。
shouldParkAfterFailedAcquire:当前节点是SIGNAL状态则当前节点需要阻塞;其他情况,需要保持当前节点的前节点状态是SIGNAL(移除所有为CANCELLED的前驱节点),且当前节点不用阻塞。因为在该方法执行时,资源已被占有,当前节点之前的节点都不会发生变化,所以在此方法操作前节点。
cancelAcquire:取消获取资源。在该方法中先会移除后节点指针(CANCELLED);然后设置当前节点为CANCELLED状态;再进行如下操作:
1.如果当前节点是tail,则需要设置当前节点前面最近一个不为CANCELLED的节点为tail节点。
2.如果当前节点是head,则需要唤醒后继节点。如果当前节点的后继节点为null,则从tail往前找,找到第一个不为CANCELLED的节点,且唤醒它。因为此时是先断开next,所以只能根据prev从tail往前找。
3.如果是中间节点,则将前节点与后继节点关联起来,设置前节点为SIGNAL。
释放锁:如果已经能释放锁,则将头节点(自己)状态变更为0,将唤醒后继节点。
ReenterLock
内部通过AQS来实现锁功能,支持公平与非公平两种模式,具有一个同步队列,多个等待队列(condition)。所有的等待队列中的线程可以被signal加入到同一个同步队列中。
公平锁与非公平锁
两者是指在绝对时间上,是不是等待时间越久的线程,越先获取锁。在非公平锁中,获取锁时,相较公平锁少了 判断当前同步队列是否有节点正在等待锁,非公平锁锁直接CAS同步状态。
读写锁
锁状态:使用同一个int值表示读写状态。高16位表示读状态,低16位表示写状态。
锁降级:在拥有写锁的情况下,获取到读锁,然后释放写锁的过程。
HashMap
因为通过(n - 1) & hash来定位key在table中的位置,所以n如果能是2的整数倍,n-1就是奇数,与运算就能更加均匀。
阻塞队列
方法 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll(e) | take() | poll(time,unit) |
检查(返回头元素,不删除) | element() | peek() | - | - |
适用于生产者与消费者的场景。
LinkedBlockingQueue先进先出的有界阻塞队列。
PriorityBlockingQueue支持优先级比较的无界阻塞队列。
DelayQueue是控制元素延时被获取,适用于缓存和定时任务,一旦能获取到元素,分别说明元素有效期到了或者需要开始执行任务了。(ScheduledThreadPoolExecutor)
SynchronousQueue是不存储任何元素的,一个put必须等待下一个take。
CycleBarrier和CountDownLatch
两者都可以通过计数控制多个线程并发操作。区别在于CycleBarrier能够重置多次,且CycleBarrier还提供getNumerWaiting(阻塞的线程个数),isBroken查看线程是否中断。
Semaphore
用于流量控制,某时间只允许多少流量通过,通过release方法,可以使得后续线程再次获得(acquire)许可。
Exchanger
用于数据交换,协同线程工作。可以用于校对工作。
线程池
提交任务:execute不会由返回值,submit会返回一个future。
关闭线程池:通过遍历所有线程,执行interrupt方法中断线程,所以无法响应中断的线程永远不会终止任务。shutdownNow是先设置线程池状态为stop,然后尝试停止所有正在执行或者暂停任务的线程,并返回所有等待执行任务的列表;shuntdown只是先设置线程池状态为shutdown,然后只中断没有执行任务的线程。再调用这两个关闭线程池的方法后,再调用isShutdown都会为true,但只有在所有任务都关闭,线程池才是真正关闭,调用isTerminaed才为true。通常不需要执行完任务的话,就直接适用shutdowwnNow。
ScheduledThreadPoolExecutor
内部适用DelayQueue,而这个DelayQueue封装了一个PriorityQueue,根据time来排序,当两个任务的time一样,则按照加入队列的顺序来排序。(time是下次要执行的时间,在执行完任务后会再次修改time)
FutureTask
内部使用AQS实现。
run方法过程:
1.执行指定的任务
2.CAS更新同步状态
3.唤醒其他等待结果的线程
4.执行FutureTask.done()
还没有评论,来说两句吧...