第一章 并发编程挑战
线程安全:行为与规范不一致,一个类在多线程访问下运转正常,并且访问类不需要额外的同步处理或协调
并发:同时处理多个任务的能力(同时执行)
并行:可以有处理多个任务的能力,但不一定同时(交替执行)
上下文切换时长:Lmbench3
上下文切换次数:vmstat
减少上下文切换:
- 无锁并发编程
- CAS算法
- 使用最少线程
- 协程
jstack查看dump线程信息:
sudo -u admin /opt/xxx/jstack pid > 输出路径
第二章 Java并发机制底层原理
Java内存模型将内存分为了 主内存和工作内存 。类的状态,也就是类之间共享的变量,是存储在主内存中的,每个线程都有一个自己的工作内存(相当于CPU高级缓冲区,这么做的目的还是在于进一步缩小存储系统与CPU之间速度的差异,提高性能)
volatile:轻量级sychronized,线程访问的共享变量
- volatile在多个处理器中保证了共享变量的“可见性”
- 处理器使用嗅探技术保证它内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致
volatile保证了指令的可见性,禁止指令重排序
volatile++不保证原子性
原因:主内存变量修改不会影响副本内存,所以计算出来的值与预期不同
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
可以知道自增操作是三个原子操作组合而成的复合操作。在一个操作中,读取了inc变量后,是不会再读取的inc的,所以它的值还是之前读的10,它的下一步是自增操作
解决方式:
synchronized{ volatile ...... }
volatile特性:
可见性:读volatile变量时,总能从volatile变量最后的写入开始
原子性:对任意单个volatile变量的读 / 写具有原子性
深入volatile:
volatile 读 / 写操作 = 普通变量读/写 锁同步
volatile特点:(1)可见性(读能看到最后写)(2)原子性(任意单个volatile读写具有原子性)
volatile写内存语义:当写一个volatile变量时,JMM把该线程对应的本地内存中共享变量刷到主内存
volatile读内存语义:当读一个volatile变量时,JMM把该线程对应的本地内存置为无效再读
volatile重排序规则、编译器底层实现:
【写操作】 StoreStore屏障 ------------volatile写---------------StoreLoad屏障 本地内存中共享变量刷新到主存
【读操作】 volatile读---------------------LoadLoad屏障--------LoadStore屏障 把本地内存置为无效,再读
volatile与锁的区别:
- volatile仅仅保证对单个volatile变量的读 / 写具有原子性
- 锁的互斥执行可保证临界区代码具有原子性
为了提高速度,处理器不直接和内存进行通信,而是先读入到缓存,写操作JVM会发送一条#Lock前缀命令,将变量所在缓存行数据写回到系统内存
- Lock前缀指令会引起处理器缓存写回到内存(相当于就是内存屏障)
- 一个处理器的缓存回写到内存会导致其他处理器缓存无效
Lock前缀说明:
- 确保对内存的读 - 改 - 写操作原子执行
- 禁止该指令与之前和之后的读和写指令重排序
- 刷新到内存
原子性保证:
1. 总线加锁:
处理器提供一个LOCK # 信号每当一个处理器在总线上输出此信号时,其他处理器的请求被阻塞住,那么该处理器可以独占共享内存
2. 缓存加锁:
总线锁定开销很大
“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK # 信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性
两种情况不会使用缓存锁定:
1. 操作数据不能被缓存在处理器内部,或操作的数据跨多个缓存行,将调用总线锁定
2. 处理器不支持缓存锁定
synchronized:在MarkWord,jvm基于进入和退出Monitor对象来实现synchronized方法
- monitorenter:在编译后插入到代码块开始位置,监视器进入,获取锁
- monitorexit:插入到方法结束和异常处,监视器退出,释放锁
-----> monitorenter---------> 监视器-------> 对象----------->monitorexit
| |
sychronizedQueue
静态方法和普通方法同时加synchronized有什么区别?
类上锁 、对象上锁
synchronized为何是不够公平的?
- 公平锁:获取不到锁时,加入队列,等释放之后,第一个线程获取锁
- 非公平锁:获取不到锁时,加入队列,等释放之后,所有等待线程同时竞争
原子操作:Java通过锁 & CAS 方式实现原子操作
锁的级别:
- 无锁状态
- 偏向锁状态:
- 当一个线程访问同步块获取锁,会在对象头和线栈??中存储锁偏向的线程ID,线程再进入不需要加锁,解锁。测试对象头的Mark Word
- 偏向锁的撤销:等待全局安全点,检查持有偏向锁线程是否活着
- 轻量级锁:
- JVM创建存储锁记录的空间,将对象头中的Mark Word复制到锁记录中,使用CAS将对象头中的Mark Word替换成指向锁记录的指针
- 解锁:CAS操作将Displaced Mark Word替换回对象头。如果失败:升级为重量级锁
- 重量级锁:依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁
偏向锁:加锁、解锁不额外消耗,如果竞争则会带来额外消耗------->只有一个线程
轻量级锁:竞争线程不会阻塞,得不到锁竞争会自旋消耗CPU------->追求相应时间、同步速度快
重量级锁:不会消耗CPU,线程阻塞,相应时间缓慢-------->追求吞吐量
自旋锁:没有获得资源的进程要么一直循环(不阻塞),要么阻塞自己
在锁的过程中,如果不成功会持续尝试。自旋锁比较适用于锁使用时间比较短的情况
开启自旋锁:-xx:UseSpining
自旋锁等待次数:-xx:PreBolockingSpin
锁粗化:虚拟机遇到一连串对同一个锁不断请求和释放的操作时,会把其整合成对锁的一次请求,减少对锁请求同步的次数
锁清除:StringBuffer、Vector等本身无对应非同步版本,JVM基于逃逸分析技术,捕获这些不可能存在竞争却申请锁的代码段,并消除这些不必要的锁
-xx:DoEscapeAnalysis -xx+Elimimate
活锁:要执行的线程总是发现其他线程正在执行,以至于长时间或将永远无法执行
CAS(Compare and Swap):同时具有volatile读和volatile写语义。
CAS长时间不成功循环时间长开销大,可以延迟流水线执行指令
当且仅当需要读写的内存值等于旧的预期值时,才会将内存值修改为新的值,否则不执行操作
CAS可能产生的问题:
1. ABA问题:加版本号可以解决
所谓的ABA 问题是指在并发编程中,如果一个变量初次读取的时候是A 值,它的值被改成了B,然后又其他线程把B 值改成了A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了,这就是ABA 问题
2. 循环时间长开销大:
pause指令:1.延迟流水线执行指令 2.避免在退出循环时因内存冲突引起cpu流水线清空
3. 只能保证一个共享变量的原子操作:锁 or 多个共享变量合并成一个共享变量
第三章 Java内存模型
Java内存模型:Java线程之间通信由Java内存模型(JMM)控制,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,
本地内存中存储了该线程以读、写共享变量的副本
Java内存模型(TSR-133):增强volatile内存语义,增强final内存语义
写缓冲区仅对自己的处理器可见
会导致处理器执行内存操作的顺序可能会与内存实际的操作顺序不一致。
由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序
重排序:优化程序性能
重排序类型:
源代码---->编译器优化重排序--->指令级并行重排序---->内存系统重排序---->最终执行指令序列
内存屏障
为了保证内存可见性,引入内存屏障来禁止处理器重排序。数据依赖关系不会改变顺序
LoadLoad Barries、StoreStore Barries、LoadStore Barries、StoreLoad Barries
- Happens-before:
如果操作执行结果需要对另一个操作可见,那么一定存在happens-before关系
规则:
- 程序顺序规则:一个线程中的每个操作,happens-before该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于随后这个锁的加锁
- volatile变量规则:对一个volatile域写,happens-before于随后对这个锁的加锁
- 传递性:A happens-before B , 且 B happens-before C , 那么 A happens-before C
- start规则:线程的start操作happens-before 线程中的任意操作
- join规则:线程中的任意操作happens-before 线程的join操作
- as-if-serial:
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果
内存模型的类型:
放松程序中写-读操作的顺序,产生了TSO模型
在上面基础上,放松写-写操作的顺序,产生了PSO模型
在前两条基础上,放松读-写 与 读-读操作的顺序,产生了PMO 与 PowerPC模型
顺序一致性模型:
程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同
顺序一致性特性:
一个线程中的所有操作必须按照程序的顺序来执行
所有程序都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有程序可见
在任意时间点只能有一个线程可以连接到内存,当多个线程并发执行时,所有线程的所有内存读 / 写操作串行话。JMM在不改变程序执行结果前提下,尽可能为编译器和处理器的优化打开方便之门
JMM下的顺序一致性:
- 顺序一致性模型保证单线程内的操作会按照程序的顺序执行,而JMM不保证单线程内的操作会按照程序的顺序执行
- 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有的线程能看到一致的操作执行顺序
- JMM不保证64位的long型和double型变量的写操作具有原子性(64位的long和double在32位机上被拆分处理)
Final域重排序:
- 在构造函数内对一个final域写入,与随后把这个final的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作间不能重排序
- 写Final域重排序:
JMM禁止编译器吧final域的写重排序构造到构造函数之外。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障,这个屏障禁止把final域的写重排序到构造函数之外(不为其他线程所见)
2. 读Final域重排序:
读final域操作前插入LoadLoad屏障
为什么final引用不能从构造函数内“溢出”
在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“溢出”
普通单例可能引入的问题:
memory = allocate() // 1
forinstance(memory) // 2 对象初始化
instance = memory // 3 分配空间
2,3步骤重排序,导致对象不为空,但是没有被初始化
解决方法:
- 禁止重排序:private volatile
- 允许重排序,但不允许其他线程看到()???
双重检查锁定方式:
public class Singleton {
private volatile static Singleton uniqueSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == uniqueSingleton) {
synchronized (Singleton.class) {
if (null == uniqueSingleton) {
uniqueSingleton = new Singleton();
}
}
}
return uniqueSingleton;
}
}
Concurrent包通用实现模式
Lock:锁
lock()、lockInterruptibly()、tryLock()、tryLock(long time,TimeUnit unit)、unlock()、 Condition.newCondition()
范式:
Lock lock = new ReentrantLock();
lock.lock();
try{
......
}finally{
lock.unlock();
}
Lock接口提供synchronized不具备的功能:
- 尝试非阻塞地获取锁
- 能被中断地获取锁
- 超时获取锁
Condition:await() / signal()
底层:ConditionObject是AQS的内部类,每个Condition对象都包含一个等待队列
AQS(AbastractQueuedSynchronizer): 队列同步器
AQS简化了锁的实现方式,屏蔽了同步状态管理、线程的排队等待与唤醒等底层操作
AQS队列同步器:
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法中免不了对同步状态的修改:
getState()、setState(int newState)、compareAndSetState(int expect,int update)
队列同步器:
- 同步队列
- 独占式同步状态获取与释放
- 共享式同步状态获取与释放
- 超时获取同步状态
同步队列基本结构:
基于AQS实现的有:
- 重入锁:ReentrantLock
- Semaphore
- 读写锁:ReentrantReadWriteLock
状态划分:32位 = 高16位读状态 + 低16位写状态
锁降级:持有写锁,申请读锁。再释放写锁的过程
- CountDownLatch
- FutureTask
AQS方法:
- getState()
- setState(int newState)
- compareAndSetState(int expect,int updating)
- acquire
- acquireInterruptibly
- tryAcqureNanos
- acquireShared
- acquireSharedInterruptibly
- tryAcquireSharedNanos
- release
- releaseShared
- getQueuedThreads
AQS提供的方法有三类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中等待线程情况
公平锁 & 非公平锁
公平锁和非公平锁释放时,最后都要写一个volatile变量state
- 公平锁获取时,首先会去读volatile变量
- 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义
---sync
- FairSync
- NoFairSync
---ReentrantLock
公平锁加锁轨迹:
ReentrantLock:lock()
FairSync:lock()
AbstractQueuedSynchronizer:acquire
ReentrantLock:tryAcquire //获取state(volatile),公平锁首先会读volatile变量
protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState(); //获取锁的开始,首先读volatile变量state
if(c == 0){
if(isFirst(current) && compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()){
int nextc = c + acquires;
if(nextc < 0){
throw new Error("Maximum lock count exceeded")
}
setState(nextc);
return true;
}
return false;
}
公平锁解锁轨迹:
ReentrantLock:unlock()
AbstractQueuedSynchronizer:release()
Sync:tryRelease() //释放state(volatile),释放时需要写一个volatile变量state
protected final boolean tryRelease(int releases){
int c = getState() - releases;
if(Thread.currentThread() != getExclusiveOwnerThread()){
throw new IllegalMonitorStateException();
}
boolean free = false;
if(c == 0){
free = true;
setExcelusiveOwnerThread(null);
}
setState(c); //释放锁之后,写volatile变为state
return free;
}
非公平锁加锁轨迹:
ReentrantLock:lock()
NonFairSync:lock()
AbstractQueuedSynchronizer:acompareAndSetState,非公平锁会用CAS更新volatile变量
独占式同步状态获取与释放:
首先调用自定义同步器实现tryAcquire(int args)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点,并通过addWaiter方法将该节点加入到同步队列的尾部,
最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
private Node addWaiter(Node mode){
Node node = new Node(Thread.currentThread(),mode);
//快速添加到尾部
Node pred = tail;
if(pred!=null){
node.prev = pred;
if(compareAndSetTail(pred,node)){
pred.next = node;
return node;
}
enq(node);
return node;
}
private Node enq(final Node node){
for(;;){
Node t = tail;
if(t==null){
if(compareAndSetHead(new Node()))
tail = head;
}else{
if(compareAndSetTail(t,node)){
t.next = node;
return t;
}
}
}
}
}
final boolean acqureQueued(final Node node,int arg){
boolean failed = true;
try{
boolean interrupted = false;
for(;;){//自旋过程
final Node p = node.predecessor();
if( p == head && tryAcquire(arg)){
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
}
if(shouldParkAfterFailAcquire(p,node) && parkAndCheckInterrupt()){
interrupted = true;
}
}finally{
if(failed) cancelAcquire(node);
}
}
p128图
- 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;
- 移出队列或者停止自旋的条件是前驱节点为头结点且成功获取了同步状态。
- 在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头结点的后继节点。
共享式同步状态的获取与释放:
public final void acquireShared(int arg){
if(tryAcquireShared(arg) < 0){
doAcquireShared(arg);
}
private void doAcquireShared(int arg){
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try{
for(;;){
final Node p = node.predecessor();
if(p == head){
int r = tryAcquireShared(arg);
if(r >=0){
setHeadAndPropagate(node,r);
p.next = null;
if(interrpted) selfInterrypt();
failed = false;
return;
}
}
if(shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt()) interrupted = true;
}
}finally{
if(failed) cancelAcuired(node);
}
}
}
独占式超时获取同步状态:
指定时间段内获取同步状态,响应中断获取同步状态过程的“增强版”
p133图
自定义同步组件:p134 - 135
锁和同步器:
锁是面向对象使用者的,定义了使用者与锁交互的接口。隐藏实现细节
AQS同步器面向的是锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作
独占锁的实现:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class Mutex implements Lock{
//自定义同步器
private static class Sync extends AbstractQueuedSynchronizer{
//是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
//当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
if(compareAndSetState(0,1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁
protected boolean tryRelease(int releases) {
if(getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
//返回一个Condition,每个condition都包含一个condition队列
Condition newCondition() {return new ConditionObject();}
}
private final Sync sync = new Sync();
public void lock() {sync.acquire(1);}
public boolean tryLock() {return sync.tryAcquire(1);}
public void unlock() {sync.release(1);}
public Condition newCondition() {return sync.newCondition();}
public boolean isLocked() {return sync.isHeldExclusively();}
public boolean hasQueuedThreads() {return sync.hasQueuedThreads();}
public void lockInterruptibly() throws InterruptedException{
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException{
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
第四章 Java并发编程基础
为什么使用多线程?更多的处理器核心;更快的相应时间;更好的编程模型
线程的状态:
- NEW
- RUNNING
- BLOCKED
- WAITING
- TIME_WAITING 不同与waiting,是可以在指定时间自行返回的
- TERMINATED
查看线程运行状态:jps + jstack
查看生成的class文件:javap
安全地终止线程:interrupt、cancel
通过标示符或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程终止。这样的做法显得更安全和优雅
suspend()、resume()、stop() 停用原因:
suspend():不释放资源,进入睡眠,产生死锁
stop():不保证资源正常释放,工作状态不确定
sleep与wait方法的区别:
- sleep属于Thread类方法,程序暂停指定时间,让出CPU,不会释放对象锁
- wait属于Object类方法,让出对象锁
sleep与yield方法的区别:
- sleep不考虑优先级,yield考虑
- sleep进入阻塞状态,yield指当前线程回到可执行
- sleep抛出InterruptedException,yield没有声明
- sleep比yiled更具有移植性
Daemon
Thread thread = new Thread(new DaemonRunner(),"DaemonRunner");
thread.setDaemon(true);
thread.start();
同步队列
任意线程对Object的访问,首先获得Object的监视器,如果获取失败,线程进入同步队列,线程状态变为BLOCKED,当访问Object的前驱释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取
类似于zk分布式锁的获取
LockSupport:提供了一组公共静态方法
- park()、parkNanos(long nanos)、parkUntil(long deadline)
- unpark() 、unpark(Thread thread)
通知/等待机制:wait / notify
notify() 方法将等待队列的线程移步到同步队列
notify() 释放锁,wait()获取锁后才返回
wait / notify 范式
//等待方
synchronized(对象){
while(条件不满足){
对象.wait();
}
处理逻辑
}
//通知方
synchronized(对象){
改变条件
对象.notifyAll();
}
通知和等待机制demo:
import java.util.concurrent.TimeUnit;
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Thread waitThread = new Thread(new Wait(),"WaitThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
Thread notifyThread = new Thread(new Notify(),"NotifyThread");
notifyThread.start();
}
static class Wait implements Runnable{
public void run() {
synchronized(lock) {
//不满足条件,一直wait
while(flag) {
try {
lock.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
static class Notify implements Runnable{
public void run() {
synchronized(lock) {
//唤醒
lock.notifyAll();
flag = false;
}
}
}
}
使用wait、notify、notifyAll注意的细节:
- 使用wait、notify、notifyAll时需要先对调用对象加锁
- 调用wait方法后,线程状态由RUNNING 变为 WAITING,并将当前线程放在对象的等待队列
notify或notifyAll之后,等待线程依旧不会从wait返回,需要调用notify或notifyAll的线程释放锁之后,等待线程才有机会从wait返回
- notify方法将队列中的一个等待线程从等待队列中移步到同步队列,而notifyAll将所有线程都移动到同步队列,被移动的线程状态从WAITING变为BLOCKED
- 从wait方法返回的前提是获得了调用对象的锁
为什么wait、notify、notifyAll定义在Object类中?
同步代码块或同步方法中的锁对象可以是任意对象
Thread.join
当前线程等待Thread线程终止之后从join方法返回
在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。
主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待(阻塞),等待子线程结束了(Waits for this thread to die.),才能继续执行t.join()之后的代码块
join(long millis)、join(long millis,int nanos):如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回
Join源码:
//加锁当前线程对象
public final synchronized void join() throw InterruptedException{
//条件不满足,继续等待
while(isAlive()){
wait(0);
}
//条件符合,方法返回
}
ThreadLocal:为每个使用该变量的线程提供独立的变量副本
线程变量,是一个ThreadLocal对象为键,任意对象为值的存储结构。这个结构附带在线程上
- 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
- ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
- 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案
ThreadLocal的4种方法:
- set
- get
- remove:将当前线程局部变量值删除,减少内存
- initialvalue:返回该线程变量的初始值
set方法的实现:
public void set(T value){
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//静态内部类
if(map!=null){
map.set(this,value);
}else{
createMap(t,value)
}
}
get方法实现:
public T get(){
Thread t = Thread.createThread();
ThreadLocalMap map = getMap();
if(map!=null){
ThreadLocalMap.Entry e = map.getEntry(this);
if(e!=null) return(T) e.value;
}
return setInitailValue();
}
p107例子
第五章 Java中的锁
通用范式:
Lock lock = new ReentrantLock();
lock.lock();
try{
//try中不要写获取锁逻辑,抛出异常后,锁无法释放
} finally{
lock.unlock();
}
重入锁:
支持线程对资源重复加锁(任意线程在获取锁之后能够再次获取该锁而不会被锁所阻塞),且支持公平和非公平选择
需要解决的问题:
线程再次获取锁:识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功
锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁之后,其他线程能够获取到该锁(自增/自减计数)
非公平锁:
final boolean nonfairTryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
//初次请求
if(c ==0){
//公平锁: 加一个条件:!hasQueuedPredecessors
if(compareAndSetStatus(0,acquires){
setExlusiveOwnerThread(current);
return true;
}
}else if(current == getExclusiveOwnerThread()){
//再次请求,重入
int nextc = c+acquires;
if(nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
释放锁:
protected final boolean tryRelease(int releases){
int c = getState() - releases;//释放state
if(Thread.currentThread ! = getExcelusiveOwnerThread()) throw new IllegalMonitorStateException();
boolean free = false;
if(c==0){ // 状态为0表示全部释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
公平锁保证了锁的获取按照FIFO原则,代价是进行了大量的线程切换
非公平锁虽然可能造成线程“饥饿”,但是极少的线程切换,保证了其更大的吞吐量
读写锁:
读时上读锁、写时上写锁
当写锁被获取到锁时,后续的读写锁操作都会被阻塞。写锁释放之后,所有操作继续执行
在读多于写的情况下,读写锁能够提供比排他锁更好的并发性和吞吐量
ReentrantReadWriteLock特性:
- 支持公平和非公平的锁获取方式
- 锁重入
- 锁降级,写锁能够降级为读锁
ReadWriteLock -- ReentrantReadWriteLock
- int getReadLockCount()
- int getReadHoldCount()
- boolean isWriteLocked()
- int getWriteHoldCount()
读写锁可以实现HashMap同步
读写状态的设计:
一个整形变量上维护多种状态,读写锁将变量切分为两个部分:高16位表示读、低16位表示写
---------32位--------
--16位读--|--16位写--
写状态:S & 0x0000FFFF , 写状态加1:S+1
读状态:S >>> 16 , 读状态加1:S+(1 << 16) 或 S+0x00010000
写锁降级为读锁:
把持写锁,再获取到读锁,随后释放写锁的过程
ReentrantReadWriteLock不支持锁升级,如果读锁已被线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
Condition接口:对lock的场景扩充(等待/通知)
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException{
lock.lock();
try {
condition.await();
}finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException{
lock.lock();
try {
condition.signal();
}finally {
lock.unlock();
}
}
Condition---->signal() / await() ----> LockSupport 的 park
private final Condition notFull;
private final Condition notEmpty;
public ArrayBlockingQueue(int capacity,boolean fair){
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException{
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try{
while(count == items.length){
notFull.await();
}
insert(e);
}finally{
lock.unlock();
}
}
public E take() throws InterruptedException{
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try{
while(count==0){
notEmpty.await();
}
return extract();
}finally{
lock.unlock();
}
}
private void insert(E x){
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
park:
public final void await() throws InterruptedException{
if(Thread.interrupted()) throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while(!isOnSyncQueue(node)){
LockSupport.park(this);
if((interruptMode = checkInterruptWhileWaiting(node))!=0) break;
}
if(acquireQueued(node,savedState) && interruptMode !=THROW_IE) interruptMode = PEINTERRUPT;
if(node.nextWaiter != null) unlinkCancelledWaiters();
if(interruptMode!=0) reportInterruptAfterWait(interruptMode);
}
park底层实现:JVM::pthread_cond_wait
示例代码:
public class BoundedQueue {
private Object[] items;
private int addIndex,removeIndex,count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
//添加一个元素,如果数组满,则进入等待状态。直到有“空位”
public <T> void add(T t) throws InterruptedException{
lock.lock();
try {
while(count==items.length) {
notFull.await();
}
items[addIndex] = t;
if(++addIndex == items.length) {
addIndex = 0;
}
++count;
notEmpty.signal();
}finally {
lock.unlock();
}
}
public <T> T remove() throws InterruptedException{
lock.lock();
try {
while(count==0) {
notEmpty.await();
}
Object x = items[removeIndex];
if(++removeIndex == items.length) {removeIndex=0;}
--count;
notFull.signal();
return (T) x;
}finally {
lock.unlock();
}
}
}
Condition接口底层实现:
- 等待队列:p151图
- 通知:p153图
- await:加入到等待队列
- signal:从等待队列转移到同步队列、signalAll方法全部唤醒到等待队列
第六章 Java并发容器和框架
ConcurentHashMap
引入ConcurentHashMap的原因:
HashMap死循环:多线程put操作回引起死循环
HashTable效率低下:使用synchronized
ConcurrentHashMap:采用分段锁技术,将数据分成一段一段地存储,每一段分配一把锁
- Segment[] 数组
- HashEntry[] 数组
ConcurrentHashMap的Entry:
// HashEntry内部基本结构
static final class HashEntry<K,V>{
final int hash;
final k key;
volatile V value;//volatile提高性能
volatile final HashEntry<K,V> next;//HashEntry不变性
}
ConcurrentHashMap的next必须为final
删除前: A->B->C->D->E
删除后(删除C):B->A->D->E (删除C,相当于重新插入B、A)
intitialCapacity、loadFactor、concurrencyLivel
-----> segment数组、segmentShift、segmentMask、HashEntry
关于segmentShift和segmentMask
segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,
int j =(hash >>> segmentShift) & segmentMask
segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性
segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。
- 初始化segments数组
- 初始化segmentShift 和 segmentMask
- 初始化每个segment
- 插入:通过散列算法定位到segments
初始化Segments数组
if(concurencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS;//concurencyLevel是2的N次方
int sshift = 0;
int ssize = 1;
while(ssize < concurrentcyLivel){
++sshift;
ssize <<= 1;
}
//初始化SegmentShift 和 segmentMask
segmentShift = 32 - sshift; //segmentShift用于定位参数散列运算的位数
segmentMask = ssize -1; //segmentMask是散列运算的掩码
this.segments = Segment.newArray(ssize);
初始化每个Segment
if(initialCapcity > MAXIMUM_CAPCITY){initialCapcity = MAXIMUM_CAPACITY;}//initialCapcity是初始化容量
int c = initialCapcity / ssize;
if(c*ssize < initialCapcity) ++c;
int cap = 1;
while(cap < c) cap <<=1;
for(int i=0;i<this.segments.length;++i){
this.segments[i] = new Segment<K,V>(cap,loadFactor);
//loadfactor 负载因子
}
Segment容量:threshold = (int)cap * loadFactor
默认情况下: initialCapcity = 16; loadFactor = 0.75; cap = 1; threshold = 0;
定位Segment
Wang / Jenkins hash算法对元素hashCode进行再一次散列(如果不进行再散列,hash冲突会非常严重)
//定位到segment
final Segment<K,V> segmentFor(int hash){
return segment[(hash >>> segmentShift) & segmentMask];}
默认情况下:segmentShift为28、segmentMask为15
get操作
不需要加锁,除非读到的值是空才加锁重读
public V get(Object key){
int hash = hash(key.hashCode());
return segmentFor(hash).get(key,hash);
}
put操作
在操作共享变量时必须加锁
插入操作经历两个步骤:
- 判断是否需要对Segment里的HashEntry数组进行扩容
- 定位添加元素的位置,然后将其放在HashEntry数组里
扩容:只针对某个segment进行扩容。扩容过程会创建一个容量是原来两倍的数组,然后将原数组中的元素进行再散列后插入到新数组
size操作
先不锁住统计2次各个Segment大小
modCount会在基本操作之后加1,统计size前后modCount发生变化,如果有变化,则Segment发生变化,将采用加锁的方式统计
ConcurrentHashMap的差别:
Java 1.7:
Java 1.8:放弃了Segment,取而代之的是Node + CAS + Synchronized
初始化:只有在执行第一次put方法时才会调用initTable,对Node数组进行初始化
put:CAS插入数据,若Node不为空,加synchronized锁
size:baseCount 记录元素个数,部分元素变化个数保存在CounterCell数组中,累加计算即可
ConcurentLinkedQueue:安全队列
private transient volatile Node<E> tail = head;
队列添加过程
p163图
1. 将入队节点设置为当前队列尾节点的下一个节点
2. 如果tail节点的next节点不为空,将入队节点设置为tail节点
3. 如果tail节点的next节点为空,则将入队节点设置成tail的next节点(tail节点不总在尾节点)
入队过程源码:
p164 - p167
public boolean offer(E e){
if(e==null) throw new NullPointerException();
Node<E> n = new Node<E>(e);
for(;;){
Node<E> t = tail;
Node<E> p = t;
for(int hops = 0;;hops++)
}
}
阻塞队列
支持两个附加操作的队列(支持阻塞的插入和移除方法)
抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 | |
插入方法 | add(e) | offer(e) | put(e) | offer |
移除方法 | remove() | poll() | take() | poll |
检查方法 | element() | peek() | 不可用 | 不可用 |
- ArrayBlockingQueue:数组有界
- LinkedBlockingQueue:链表有界
- PriorityBlockingQueue:优先级无界,默认采取自然顺序升序排序,非稳定
- DelayQueue:优先级无界,只有延时期满才提取元素(应用场景:1,缓存系统设计 2、定时任务调度)
- SynchronousQueue:不存储元素阻塞队列,将元素直接传递给消费者(吞吐量高于LinkedblockingQueue)
- LinkedTransferQueue:链表无界,多了tryTransfer 和 transfer 方法
- LinkedBlockingDeque:链表双向阻塞,设置容量防止过度膨胀,可运用在“工作窃取”模式中
LinkedTransferQueue:
- transfer:把生产者传入元素立刻给消费者,若没有,将元素放在队列tail,并等待消费返回
- tryTransfer:无论消费者是否接收,都立即返回
底层实现原理:通知模式实现
- 当生产者往满的队列里添加元素时会阻塞生产者
- 当消费者消费队列中的元素后,会通知生产者当前队列可用
Fork / Join队列:分割-合并任务框架
窃取算法,使用双端队列:线程从头部拿任务执行,窃取任务线程从尾端拿任务执行
框架设计:
1.分割任务:fork
2.执行任务并合并结果:join
需要ForkJoinPool:ForkJoinTask数组用来提交、ForkJoinWorkerThread数组用来执行
ForkJoinTask的fork方法原理:
- ForkJoinWorkerThread.pushTask()把当前任务放在ForkJoinPool数组队列里
- 再调用signalWork()唤醒执行
ForkJoinTask的join方法原理:
1. 首先调用doJoin()方法得到当前任务来判断返回结果
NORMAL、CANCELLED、SIGNAL、EXCEPTION
2. doJoin中看任务状态,若没有执行完,则从任务数组取出执行
例子:
//计算1+2+3+4的结果
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer>{
//RecursiveAction:没有返回结果的任务
//RecursiveTask:有返回结果的任务
private static final int THRESHOLD = 2;
private int start;
private int end;
public CountTask(int start,int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canCompute = (end - start) <= THRESHOLD;//如果足够小就执行计算任务
if(canCompute) {
for(int i=start;i<=end;i++) sum+=i;
}else {
int middle = (start+end)/2;
CountTask leftTask = new CountTask(start,middle);
CountTask rightTask = new CountTask(middle+1,end);
//子任务
leftTask.fork();
rightTask.fork();
//等待任务完成并获取结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
//合并任务
sum = leftResult+rightResult;
}
return sum;
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(1,4);
Future<Integer> result = forkJoinPool.submit(task);
System.out.println(result.get());
}
}
第七章 原子操作类
原子操作类
使用Unsafe实现的包装类 , 13个类
基本方法方法:addAndGet、compareAndGet、getAndIncrement、lazySet、getAndSet
unsafe提供3种CAS方法:
compareAndSwapObject、compareAndSwapInt、compareAndSwapLong
类型boolean、char、float、double需要通过转换来实现其功能
compareAndSet底层: return unsafe.compareAndSwapInt(this,valueOffset,expect,update);
1. 原子更新基本类型类
- AtomicBolean
- AtomicInteger
- AtomicLong
2. 原子更新数组:会将传入数组复制一份,因此对内部数组元素修改时,不会影响传入数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
- AtomicInteger
3. 原子更新引用类型
- AtomicReference
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
- AtomicMarkableReference:原子更新带有标记位的引用字段
4. 原子更新字段类
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型
因为原子更新字段类都是抽象类,每次使用必须使用newUpdater(),创建更新器并需要设置要更新的类和属性。
更新类的字段必须使用public volatile修饰符
Java并发工具
1. CountDownLatch
允许一个或多个线程等待其他线程完成操作
static CountDownLatch c = new CountDownLatch(2);
c.countDown();
c.await();
2. CyclicBarrier
一组线程达到一个屏障,直到最后一个线程达到屏障才会开门
CyclicBarrier(int parties) CyclicBarrier(int parties,Runable barrierAction)
CountDownLatch 与 CyclicBarrier的区别:
- CountDownLatch只能使用一次,CyclicBarrier可以用reset()方法重置
- CyclicBarrier能处理更复杂的业务场景
- CyclicBarrier提供了getNumberWaiting方法获得CyclicBarrier阻塞线程数量,isBroken了解阻塞线程是否被中断
3. Semaphore
控制同时访问特定资源的线程数量,如数据库连接(控制流量红绿灯)
Semaphore s = new Semaphore(10);
s.acquire();
s.release();
其他方法:
- intavailablePermits():返回信号量中当前可用许可证数
- intgetQueueLength():返回正在等待获取许可证的线程数
- booleanhasQueuedThreads():是否有线程正在等待许可证
- void reducePermits(int reduction):减少许可证,protected方法
- Collection getQueuedThreads():返回所有等待获取许可证的线程集合,protected方法
4. Exchanger
两个线程交换数据:
- 可用于遗传算法
- 可用于校对工作
exchange()方法会一直等待,为避免一直等待,使用exchange(xxx)设置最大等待时长
第九章 Java线程池
Java线程池的特点:
- 降低资源消耗
- 提高相应速度
- 提供线程的可管理性
线程池参数:corePoolSize、maximumPoolSize、keepAliveTime(空闲的线程多久时间后被销毁)、unit、workQueue:阻塞队列、handler:线程拒绝策略
线程池状态:Running - 0 // SHUTDOWN - 1 // STOP - 2 // TERMINATED - 3
线程池工作原理:
线程池工作时,会封装成工作线程Worker,Worker在执行完任务后,会循环获取工作队列里的任务来执行
提交任务:
- execute():无返回值
threadPool.execute(Thread)
- submit():有返回值
Future<Object> future = executor.submit(hasReturnValueTask); Object s = future.get();
关闭线程池:
- shutdownNow:将线程池状态设置为STOP
遍历线程池中的工作线程,然后逐个调用线程的interrupt方法中断线程
尝试停止所有正在执行或暂停任务的线程
- shutdown:将线程池状态设置为SHUTDOWN状态,中断所有没有正在执行任务的线程
此时调用isShutdown会返回true
所有任务关闭后,isTerminated方法会返回true
线程池使用建议:
- 性质不同的任务可以用不同规模线程池分开处理
- 优先级不同的任务可以使用PriorityBolockingQueue处理
- 执行时间不同的任务可以交给不同规模的线程池处理
- 依赖数据库连接池的任务,线程数应该设置大一些
- 建议使用有界队列,增加系统稳定性 & 预警能力
线程池的监控:
taskCount、completedTaskCount、largestPoolSize、getPoolSize、getActiveCount
拒绝策略:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:指被拒绝的任务,由 创建了线程池的线程来执行 被拒绝的任务(哪个线程创建了线程池,这个线程来执行被拒绝的任务)
- DiscardOldestPolicy:丢掉队列中最近的一个任务,并执行当前的任务
- DiscardPolicy:不处理,丢弃掉
第十章 Executor框架
Executor:用户级调度器,将这些任务映射为固定数量的线程,executor控制线程调度
1. FixedThreadPool:无界队列LinkedBlockingQueue
适用于为了满足资源管理的需求,需要限制当前线程的数量的应用场景(负载比较重的服务器)
2. SingleThreadPool:无界队列LinkedBlockingQueue
使用于需要保证顺序执行的任务
corepool = maximumPool
3. CachedThreadPool:SynchronousQueue<Runnable>
适用于很多短期异步任务的小程序,或者负载较轻的服务器
corePool为空
4. ScheduledThreadPool:DelayQueue
适用于多个后台线程执行周期任务,同时为了满足资源管理器的需求而需要限制后台线程的应用场景
FutureTask
FutureTask<String> future = new FutureTask<String>(new RealData("5"));
execurtor.submit(future);
future.get();
协程:
一种用户态轻量级线程(非抢占式)
拥有自己的寄存器上下文和栈,切换时,将寄存器上下文和栈保存在其他地方,在切回来的时候,恢复先前保存。解决了线程抢占式无法确定的问题。
好处:跨平台、跨体系架构、无需上下文切换开销、无需原子操作锁定以及同步开锁、高并发+高扩展性+低成本
缺点:无法利用多核资源(本质为单线程)
生产者与消费者实现:
Storage中封装producer、consumer
1. 阻塞队列方式:
private final BlockingQueue sharedQueue;
//消费者
while(true){ sharedQueue.take();}
//生产者
while(true){sharedQueue.put(...);}
2. Object的 wait 和 notify
//消费者
while(true){
synchronized(queue){
while(queue.size==xx){
try{
queue.wait();
}catch (Exception e){
queue.notify();
}
}
queue.offer();
queue.notify();
}
}
//生产者
while(true){
synchronized(queue){
while(queue.size==0){
try{
queue.wait();
}catch (Exception e){
queue.notify();
}
}
queue.poll();
queue.notify();
}
}
3. 使用Condition实现
PriorityQueue queue;
Lock lock;
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
//消费者
while(true){
lock.lock();
while(queue.size==xx){
try{
notFull.await();
}catch (Exception e){
queue.notify();
}
}
queue.offer(1);
notEmpty.notifyall();
}
//生产者
while(true){
lock.lock();
while(queue.size==0){
try{
notEmpty.await();
}catch (Exception e){
queue.notify();
}
}
queue.poll();
notFull.notifyall();
}