目录
第1章 并发编程的挑战
1.1 上下文切换
因为上下文的创建和切换存在系统开销,所以使用了多线程不一定就快。
多线程竞争条件时会引起上下文切换,所以可以用一些方法来变使用锁。
处理多线程数据的无锁方法有:
- 将数据ID按照Hash算法取模,不同线程处理不同段的数据。
- CAS
- 使用最少线程。避免创建不需要的线程。
- 协程。在单线程调度多任务。
1.2 死锁
避免死锁的方法:
- 避免同一个线程获得多个锁
- 尽量保证每个锁只占用一个资源
- 尝试使用定时锁
- 数据库锁,加锁和解锁必须在一个数据库链接里。
第2章 并发机制的底层实现原理
java代码编译后会变成java字节码,字节码被加载器加载到JVM里,JVM找到main入口执行代码,最终需要转换成汇编指令在cpu上执行。
java中所使用的汇编机制依赖于JVM的实现和cpu的指令。
2.1 volatile关键字
volatile是轻量级的synchronized,能保证可见性。可见性是说当一个线程修改变量后另一个线程能看到。
volatile比synchronized执行成本更低,因为他不会引起上下文切换和调度。
为了提高处理速度,处理器不直接和内存通信,而是通过缓冲区,但是操作完缓冲区不会立即写内存。但是修改使用了volatile修饰的变量,会立即将缓存回写主内存,并将其他缓存中的旧数据置为无效。
2.2 synchronized
2.2.1 synchronized实现同步的基础:java中每一个对象都可以作为锁
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是synchronized括号中配置的对象
2.2.2 java对象头信息
hotspot虚拟机的对象头主要包括两部分数据:Mark Word、Kclass Pointer。Kclass Pointer指向类元数据。Mark Word存储自身运行时数据,其存储结构如下:
2.2.3 原理
synchronized在JVM的实现原理:同一时刻只有一个线程可以获得对象监视。
monitorenter和monitorexit指令在编译后分别插入到同步代码块的开始位置和结束位置。
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:
2.3 原子操作的实现原理
2.3.1 处理器是如何实现原子操作的
处理器可以保证基本的内存操作的原子性。当一个处理器读取一个字节时,其他处理器不能访问这个内存地址。
但是复杂的内存操作处理器不能保证原子性,复杂操作比如:跨总线宽度,跨多个缓存行,跨页表的访问。
总线锁定和缓存锁定用来解决复杂操作的原子性。
- 总线锁定:处理器提供一个LOCK#信号,当一个处理器在总线上输出次信号时,其他处理器的请求将被阻塞,那么该处理器可以独占共享内存。
- 缓存锁定
应该优先使用缓存锁定,因为总线锁定会阻塞其他处理器的所有内存操作,但是有两种情况不能使用缓存锁定:操作数据不缓存在处理器内部或者操作的数据跨多个缓存行、有些处理器不支持缓存锁定。
2.3.2 java是如何实现原子操作的
有两个实现方法:cas和锁。
cas的三个问题:
- ABA。用乐观锁来解决。比如JDK的Atomic包提供了一个类AtomicStampedRefrence。
- 循环时间长开销大。
- 只能保证一个共享变量的原子操作。办法是:把多个变量合并成一个变量再cas。JDK提供了AtomicRefrence,可以把多个变量放在一个对象里。
第3章 Java内存模型
并发编程中的两个问题:线程通信和同步。通信是指线程之间交换信息。同步是指线程间发生操作的相对顺序。
java用共享内存的方式解决通信问题,在共享内存并发模型里,同步是显式进行的,程序员需要显式指定某个逻辑需要线程之间互斥执行。
3.1 java内存模型基础
3.1.1 java内存模型
JMM模型如下图。JMM决定一个共享变量的变化何时对另一个线程可见。线程共享的区域才存在内存可见性问题。
3.1.2 JMM视角:java线程通信过程
每个线程都有一个本地内存,本地内存中存储了该线程以读写共享变量的副本。线程A和B通信必须经过主存。
3.1.3 重排序
编译期和处理器会做重排序。
这些重排序可能导致内存可见性问题。解决办法:
编译期重排序:JMM编译器重排序规则会禁止特定类型的编译器重排序。
处理器重排序:JMM编译器重排序规则会要求Java编译期生成指令序列时插入特定类型的内存屏障,通过屏障禁止特定类型的处理器重排序。
内存屏障可以被分为以下几种类型
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
3.1.4 happens-before
JSR-133内存模型引入happens-before概念。
在JMM中,如果一个操作的结果需要对另一个操作可见,那么这两个操作之间存在happens-before关系。这里的两个操作不一定在一个线程。
两个操作有happens-before关系,并不是说一个操作要在另一个操作之前执行。
happens-before仅仅要求前一个操作对后一个操作可见,切前一个操作按顺序排在第二个操作之前。
happens-before与JMM关系:
程序员遵循的happens-before规则影响这JMM是否要禁止某些重排序。程序员遵循的happens-before规则有:
- 程序顺序规则:一个线程中的操作happens-before与其后续操作
- 监视器锁规则:解锁happens-before加锁
- volatile变量规则:volatile的写happens-before后续的读
- 传递性
3.2 重排序
3.2.1 数据依赖性
单个线程中的两个操作,且其中一个是写操作,则这两个操作存在数据依赖。这两个操作不会被重排序。
3.2.2 as-if-serial语义
含义:单线程环境下,重排序后,执行结果不能变。
as-if-serial语义完美诠释了happens-before的定义。
举个例子,计算圆的面积,示例代码如下:
按照程序顺序规则,A happens-before B,但是A和B可能会重排序,B先于A执行,但是计算圆面积的结果不变,遵循了as-if-serial语义。
3.2.3 重排序对多线程的影响
举个例子。
这里1和2不存在依赖关系,当这两个操作被重排序,多线程的语义被破坏。
3,4被重排序也会破坏多线程语义。
对flag加volatile修饰,或者将读写方法加synchronized可以实现正确同步。
3.3顺序一致性
3.4volatile的内存语义
3.4.1 volatile特性:
- 可见性
- 原子性
3.4.2 volatile可见性的实现:
JMM为了实现volatile可见性,通过内存屏障对重排序做了限制。
- 第二个操作是volatile是写,不能重排序
- 第一个操作是volatile读,不能重排序
- 第一个操作是volatile写,第二个是volatile读不能重排序。
3.5锁的内存语义
3.5.1 以ReentrantLock为例,加锁和解锁的实现是基于volatile state;
CAS也依赖于volatile的读写内存语义。
3.5.2 current包
current包的通用实现模式:
- 声明volatile变量
- 使用CAS的原子条件实现线程同步
- 用volatile读/写和CAS所具有的volatile读和写的内存语义实现线程通信。
3.6 final域的内存语义
对final域,编译器和处理器遵循两个重排序规则:
- 在构造函数内对final域的写入,与随后把这个构造对象赋值给另一个引用,这两个操作不能重排序。
- 初次读一个包含final域的对象,与随后初次读这个对象中的final域,不能重排序。
如果final域为引用类型增加如下约束: - 在构造函数内对final域的写入,与随后在构造函数外把这个构造对象赋值给另一个引用,这两个操作不能重排序。
3.7 happens-before
JMM对程序员承诺:
- 如果一个操作happens-before于另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
JMM对编译器和处理器重排序的约束原则: - 如果一个操作happens-before于另一个操作,不一定要第一个操作的执行顺序排在第二个操作之前。如果不影响执行结果可以重排序。
3.7 双重锁检查
3.7.1 双重锁检查的由来
懒汉模式的单例初始化为例
因为synchronized存在性能开销,因此出现了“双重锁检查”。代码如下。如果第一次检查instance不为null就不需要加锁直接返回。
上面代码的问题在于第7行在实际执行时其实是三个动作
如果2,3发生重排序,将会出问题。线程B可能读到一个还没有初始化的对象。
知道了问题的原因,就有了解决方案:
- 不允许2,3重排序:基于volatile
- 允许2,3重排序,单不允许其他线程看到:基于类初始化
JVM在类的初始化阶段(class被加载后,被线程使用前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
第4章 java并发编程基础
4.1 线程状态
4.2 几个线程函数
4.2.1 中断
4.2.2 终止
4.2.3 Thread.join()
4.2.4 ThreadLocal
4.3 线程间通信
4.3.1 volatile/synchronized
4.3.2 wait/notify
4.3.3 等待/通知经典范式
等待方:
通知方:
示例:
第5章 Java中的锁
5.1 lock
lock比synchronized优势在于:
- 尝试非阻塞获取锁
- 可超时获取锁
- 可相应中断
5.2队列同步器
读懂这段灵魂代码就够了
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5.3 读写锁
- 一个32位int表示锁状态,高16位表示读状态,低16位表示写状态
- 获取写锁的时候必须没有线程持有读写锁
- 锁降级:持有写锁,持有读锁,释放写锁
- 问题:读锁长时间占有,写锁获取不到,导致线程“饥饿”,解决办法:JDK8的stampLock
5.4 LockSupport
- 阻塞/唤醒线程。park()/unpark()
- 每个线程都有一个permit,取值0或者1,park()/unpark()通过修改permit实现阻塞/唤醒
5.5 Condition接口
配合lock使用,提供await()、signal()两个方法。功能可替代synchronized和wait()、nitify().
优势:一个锁可以有多个condition().
第6章 java并发容器和框架
6.1 ConcurrentHashMap
为什么用ConcurrentHashMap:
- HashMap在多线程环境下可能会出现死循环、丢数据。发生在扩容场景。
- HashTabe 使用synchronized低效
- ConcurrentHashMap分段锁提高并发
remove(key)
加段锁
要将删除的节点之前的节点clone一遍,因为next指针域是final;
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
HashEntry<K,V>[] tab = table; //table是volatile,读写代价大,所以这里先赋值给tab
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount;
HashEntry<K,V> newFirst = e.next;
*for (HashEntry<K,V> p = first; p != e; p = p.next) //要将删除的节点之前的节点clone一遍
*newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
get(key)
V get(Object key, int hash) {
if (count != 0) { // count是volatile 当前桶的数据个数是否为0
HashEntry<K,V> e = getFirst(hash); 得到头节点
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v; //不加锁
return readValueUnderLock(e); // 读到空值加锁在get一次
}
e = e.next;
}
}
returnnull;
}
- get不加锁,因为get里用到的变量都是volatile。
- 结构更新对应count,非结构更新对应value,这俩都是volatile。
- 根据hash和key遍历查找元素,指针的next是final,所以不加锁。但是头指针不是final,getFirst(hash)可能返回过时的头结点。可能会出现读取的过程中被其他线程修改的现象。
put
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // 扩容判断
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; //覆盖旧值
}
else { //插入头结点
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
size()
把每个segement的count相加得到size是不安全的。怎样才能安全准确高效呢?
安全的做法是在统计size时锁住put、remove、clean但是这非常低效。
所以做法是:尝试不加锁统计size,如果统计完发现在统计过程中count有修改再加锁统计。
如何能统计完发现在统计过程中count有修改?
使用modCount变量,put、remove、clean操作前都会modCount++;
6.2 ConcurrentLinkedQueue
无界非阻塞线程安全队列
6.1.1 入队列
- 入队就是将元素添加到队列尾部
- tail节点并不一定是尾节点,因为CAS设置尾节点开销大,所以不是每次入队都更新尾节点。
6.1.2 出队列 - headl节点并不一定是头节点,因为CAS设置头节点开销大,所以不是每次出队都更新头节点。
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {//队列已空,更新头结点
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
6.3 Java中的阻塞队列
6.3.1 七个阻塞队列
- ArrayBlockingQueue.基于数组的有界队列。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
- LinkedBlockingQueue。基于链表的有界队列,有两把锁分别用于入队出队。
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
- PriorityBlockingQueue。基于优先级的无界队列
public PriorityBlockingQueue(int initialCapacity,
Comparator<? super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.lock = new ReentrantLock();
this.notEmpty = lock.newCondition();
this.comparator = comparator;
this.queue = new Object[initialCapacity];
}
- DelayQueue
可用于设计缓存系统、定时任务调度。 - SynchronousQueue。不存储元素
- LinkedTransferQueue。链表无界阻塞。tryTransfer()试图将入队的元素直接给等待的消费者。
- LinkedBlockingDeque。链表双向。只有一把锁,性能低于LinkedBlockingQueue.使用场景:工作窃取模式(每个消费者消费自己的队列,消费完了可以消费其他队列,从其他队列尾开始消费)
6.3.12 阻塞队列的实现
通知模式实现。
第七章 13个原子操作类
7.1 原子类
Atomic包提供三个类:
- AtomicInteger
- AtomicBoolean
- AtomicLong
Java并发包只实现了三个基本类型的原子类,int,boolean,long,那么其他的呢,char,duble,float怎么办?——借鉴boolean的实现,先转为int在计算。
7.2 原子更新数组
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
7.3 原子引用类型
- AtomicReference
7.4 原子更新字段
- AtomicIntegerFieldUpdater
第八章 并发工具类
8.1 CountDownLatch
使用场景:等待所有子线程执行完再继续执行主线程
功能类似join()。
8.2 CyclicBarrier
使用场景:等所有子线程执行到一个节点后再开一起继续执行。
8.3 Semaphore
流量控制。与lock的区别:非owner线程可以释放Semaphore。lock只有owner才能释放锁。
8.4 Exchanger
数据交换,支持timeout
第九章 线程池
9.1 原理
9.1.1 处理流程
1>当前运行的线程少于corePoolSize,创建新线程执行任务(加全局锁)
2>任务数>=corePoolSize,则加入BlockingQueue
3>BlockingQueue满,创建新线程执行任务(加全局锁)
4>运行的线程数>maximumPoolSize,调用拒绝策略拒绝任务
线程池创建线程会被封装成工作线程,工作线程处理完现在任务还会继续在队列中取任务。
9.1.2 线程池的创建
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//线程存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//线程创建工厂
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- keepAliveTime
线程最大空闲时间。
只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用
如果调用了allowCoreThreadTimeOut(true)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用 - RejectedExecutionHandler
线程池满 并且 缓存队列满 拒绝策略会起作用
AbortPolicy 丢弃并抛异常 RejectExecutionException
DiscardPolicy 丢弃不抛异常
DiscardOldestPolicy 丢弃队列最前的任务 并执行当前任务
callerRunnsPolicy 由调用线程处理该任务
9.1.3 线程池的几个方法 - execute()
- submit()。
它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果 - shutdown()。线程处于SHUTDOWN状态,此时线程池不在接收新的任务,等待现有的线程执行完毕
- shutdownNow()。线程处于STOP状态,尝试终止正在执行的任务。
9.1.4 线程池状态
volatile int runState;
static final int RUNNING = 0;
static final int SHUTDOWN = 1;//调用shutdown()
static final int STOP = 2;//调用shutdownNow()
static final int TERMINATED = 3;//线程处于SHUTDOWN或者STOP状态,并且所有工作线程已销毁,任务队列被清空或者已经执行完。
9.1.5线程池大小
cpu密集型任务:N+1
io密集型任务:2*N