超出字数限制了 , 所以分成了三篇 , 点击下方链接直达 , 记得三连哦 !!
十 . 原子类
10 . 1 原子类的简述
> 原子类的应用场景
> 基本类型
使用原子的方式更新基本类型
• AtomicInteger:整形原子类
• AtomicLong:长整型原子类
• AtomicBoolean :布尔型原子类
> 数组类型
使用原子的方式更新数组里的某个元素
• AtomicIntegerArray:整形数组原子类
• AtomicLongArray:长整形数组原子类
• AtomicReferenceArray :引用类型数组原子类
> 引用类型
• AtomicReference:引用类型原子类
• AtomicStampedRerence:原子更新引用类型里的字段原子类
• AtomicMarkableReference :原子更新带有标记位的引用类型
> 对象的属性修改类型
• AtomicIntegerFieldUpdater:原子更新整形字段的更新器
• AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
10 . 2 AtomicInteger 类常用方法
public final int get()
public final int getAndSet(int newValue)
public final int getAndIncrement()
public final int getAndDecrement()
public final int getAndAdd(int delta)
boolean compareAndSet(int expect, int update)
public final void lazySet(int newValue)
public final int incrementAndGet()
public final int decrementAndGet()
public final int addAndGet(int delta)
这些方法的实现都依赖另一个public方法:
public final boolean compareAndSet(int expect, int update)
10 . 3 原子类的原理
> 之所以称为原子变量,是因为其包含一些以原子方式实现组合操作的方法
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch切换到另一个线程
> CAS (compare and swap) + volatile 和 native 方法来保证原子操作
private volatile int value;
注意,它的声明带有volatile,这是必需的,以保证内存可见性。
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
先获取当前值current,计算期望的值next,然后调用CAS方法进行更新,如果当前值没有变,则更新并返回新值,否则继续循环直到更新成功为止
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
十一 . 并发工具
11 .1 并发工具总结
> CyclicBarrier 同步辅助类
- 允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)
- 让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
----------------------------------------
> CountDownLatch
- 在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
- 用给定的计数 初始化 CountDownLatch。
- 由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。
- 之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。
- 如果需要重置计数,请考虑使用 CyclicBarrier。
: CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。
: 每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务
----------------------------------------
> Semaphore
- 信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。
-
----------------------------------------
> Exchanger
- 可以在对中对元素进行配对和交换的线程的同步点
- 每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象 , Exchanger 可能被视为 SynchronousQueue 的双向形式
11 .2 CyclicBarrier
> 它允许一组线程互相等待,直到到达某个公共屏障点 (Common Barrier Point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 Barrier 在释放等待线程后可以重用,所以称它为循环( Cyclic ) 的 屏障( Barrier ) 。
> 使用场景 : 允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)
> 让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
内部使用重入锁ReentrantLock 和 Condition
CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
CyclicBarrier(int parties, Runnable barrierAction) :创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
parties 变量,表示拦截线程的总数量。
count 变量,表示拦截线程的剩余需要数量。
barrierAction 变量,为 CyclicBarrier 接收的 Runnable 命令,用于在线程到达屏障时,优先执行 barrierAction ,用于处理更加复杂的业务场景。
generation 变量,表示 CyclicBarrier 的更新换代
M- await : 等待状态
M- await(long timeout, TimeUnit unit) : 等待超时
> dowait
: 该方法第一步会试着获取锁
: 如果分代已经损坏,抛出异常
: 如果线程中断,终止CyclicBarrier
: 进来线程 ,--count
: count == 0 表示所有线程均已到位,触发Runnable任务
: 唤醒所有等待线程,并更新generation
> 跳出等待状态的方法
最后一个线程到达,即index == 0
超出了指定时间(超时等待)
其他的某个线程中断当前线程
其他的某个线程中断另一个等待的线程
其他的某个线程在等待barrier超时
其他的某个线程在此barrier调用reset()方法。reset()方法用于将屏障重置为初始状态。
SC- Generation : 描述了 CyclicBarrier 的更新换代。在CyclicBarrier中,同一批线程属于同一代。当有 parties 个线程全部到达 barrier 时,generation 就会被更新换代。其中 broken 属性,标识该当前 CyclicBarrier 是否已经处于中断状态
M- breakBarrier : 终止所有的线程
M- nextGeneration : 更新换代操作
- 1. 唤醒所有线程。
- 2. 重置 count 。
- 3. 重置 generation 。
M- reset : 重置 barrier 到初始化状态
M- getNumberWaiting : 获得等待的线程数
M- 判断 CyclicBarrier 是否处于中断
11 .3 CountDownLatch
在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
CountDownLatch是通过一个计数器来实现的,当我们在new 一个CountDownLatch对象的时候需要带入该计数器值,该值就表示了线程的数量。每当一个线程完成自己的任务后,计数器的值就会减1。当计数器的值变为0时,就表示所有的线程均已经完成了任务
> CountDownLatch内部依赖Sync实现,而Sync继承AQS
> sync :
包含方法 : tryAcquireShared 获取同步状态
tryReleaseShared 释放同步状态
> await() :
使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断
: sync.acquireSharedInterruptibly(1);
: 内部使用AQS的acquireSharedInterruptibly(int arg)
> getState()
: 获取同步状态,其值等于计数器的值,从这里我们可以看到如果计数器值不等于0,则会调用doAcquireSharedInterruptibly(int arg)
> doAcquireSharedInterruptibly
: 自旋方法会尝试一直去获取同步状态
> countDown
: CountDownLatch提供countDown() 方法递减锁存器的计数,如果计数到达零,则释放所有等待的线程
: 内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态
: tryReleaseShared(int arg)方法被CountDownLatch的内部类Sync重写
CountDownLatch内部通过共享锁实现。在创建CountDownLatch实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。
当某个线程调用await()方法,程序首先判断count的值是否为0,如果不会0的话则会一直等待直到为0为止
当其他线程调用countDown()方法时,则执行释放共享锁状态,使count值 – 1
当在创建CountDownLatch时初始化的count参数,必须要有count线程调用countDown方法才会使计数器count等于0,锁才会释放,前面等待的线程才会继续运行。注意CountDownLatch不能回滚重置
11 .4 Semaphore
信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目
当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1
Semaphore提供了两个构造函数:
Semaphore(int permits) :创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits, boolean fair) :创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore默认选择非公平锁。
当信号量Semaphore = 1 时,它可以当作互斥锁使用。其中0、1就相当于它的状态,当=1时表示其他线程可以获取,当=0时,排他,即其他线程必须要等待。
> acquire()方法来获取一个许可
: 内部调用AQS的acquireSharedInterruptibly(int arg),该方法以共享模式获取同步状态
> 公平
: 判断该线程是否位于CLH队列的列头
: 获取当前的信号量许可
: 设置“获得acquires个信号量许可之后,剩余的信号量许可数”
: CAS设置信号量
> 非公平
: 不需要判断当前线程是否位于CLH同步队列列头
> ,Semaphore提供release()来释放许可
11 .5 Exchanger
> 可以在对中对元素进行配对和交换的线程的同步点
> 每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象 , Exchanger 可能被视为 SynchronousQueue 的双向形式
> Exchanger,它允许在并发任务之间交换数据
: 当两个线程都到达同步点时,他们交换数据结构,因此第一个线程的数据结构进入到第二个线程中,第二个线程的数据结构进入到第一个线程中
11.6 并发工具使用
@ https://github.com/black-ant/case/tree/master/case%20Module%20Thread/case%20thread_utils
十二 . 并发容器
12.1 ConcurrentHashMap
> 特点 :
- 高性能的线程安全HashMap , 同时支持读操作完全并行 , 写操作一定程度得并行
- 数组+链表+红黑树
-
-
- V putIfAbsent(K key, V value); --
- boolean remove(Object key, Object value) -- key , value 均需要对应
- boolean replace(K key, V oldValue, V newValue); -- key , oldValue 均需要对应
- V replace(K key, V value);
- 分段锁 : 每一个段相当于一个独立的哈希表,分段的依据也是哈希值 , 不同分段去竞争各自的分段锁
- 读不需要锁 : 写时实际调用 putVal , 内部使用synchronized锁 , 读时没有做相关的锁操作
1. 每个节点非红即黑
2. 根节点为黑色
3. 每个叶子节点为黑色。叶子节点为NIL节点,即空节点
4. 如果一个节点为红色,那么它的子节点一定是黑色
5. 从一个节点到该节点的子孙节点的所有路径包含相同个数的黑色节点
C- Node
- final int hash;
- final K key;
- volatile V val;
- volatile Node<K,V> next;
C- TreeNode
- 链表长度过长会转换为红黑树 , 具体方式为将链表的节点包装成TreeNode放在TreeBin对象中
C- TreeBin
12.2 ConcurrentSkipListSet
TODO
12.3 ConcurrentSkipListMap
1)基于数组的实现似乎遇到更多的复杂性和开销
2)对于遍历索引列表,我们可以使用比基础数组便宜的算法
- 没有使用锁,所有操作都是无阻塞的,所有操作都可以并行,包括写,多个线程可以同时写。
- 与ConcurrentHashMap类似,迭代器不会抛出ConcurrentModificationException,是弱一致的,迭代可能反映最新修改也可能不反映,一些方法如putAll, clear不是原子的。
- 与ConcurrentHashMap类似,同样实现了ConcurrentMap接口,直接支持一些原子复合操作。
- 与TreeMap一样,可排序,默认按键自然有序,可以传递比较器自定义排序,实现了SortedMap和NavigableMap接口。
extends AbstractMap<K,V> implements ConcurrentNavigableMap<K,V>, Cloneable, Serializable
-
Head nodes Index nodes
+-+ right +-+ +-+
|2|---------------->| |--------------------->| |->null
+-+ +-+ +-+
| down | |
v v v
+-+ +-+ +-+ +-+ +-+ +-+
|1|----------->| |->| |------>| |----------->| |------>| |->null
+-+ +-+ +-+ +-+ +-+ +-+
v | | | | |
Nodes next v v v v v
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
| |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
在删除时标记已删除节点的“下一个”指针,以避免与并发插入发生冲突,并且在遍历以跟踪三元组(前任,节点,后继)时进行标记,以便检测何时以及如何取消这些链接删除的节点
+------+ +------+ +------+
... | b |------>| n |----->| f | ...
+------+ +------+ +------+
+------+ +------+ +------+ +------+
... | b |------>| n |----->|marker|------>| f | ...
+------+ +------+ +------+ +------+
+------+ +------+
... | b |----------------------------------->| f | ...
+------+ +------+
TODO
12.4 ConcurrentLinkedQueue
基于链接节点的无边界的线程安全队列,它采用FIFO原则对元素进行排序。采用"wait-free"算法(即CAS算法)来实现的 .
队列的头部是在队列中存在时间最长的元素。队列的尾部是最短时间位于队列中的元素。新元素插入到队列的尾部,并且队列检索操作在队列的开头获取元素
ConcurrentLinkedQueue 有2个主要的概念 , 分为 header节点 和 tail节点 , 对象中每个节点对象 ( Node<E> ) , 其中包含2个成员(E item + Node<E> next)
- 在入队的最后一个元素的next为null
- 队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
- 对于要删除的节点,不是直接将其设置为null,而是先将其item域设置为null(迭代器会跳过item为null的节点)
- 允许head和tail更新滞后。这是什么意思呢?意思就说是head、tail不总是指向第一个元素和最后一个元素
- casNext : p.casNext(null, newNode) -> 如果p 的下一个节点为null , 把 p 节点的next 节点设置为 newNode
- UNSAFE.compareAndSwapObject(Object var1, long var2, Object var3, Object var4)
- var1 操作的对象
- var2 操作的对象属性
- var3 var2与var3比较,相等才更新
- var4 更新值
- casTail : 设置tail 尾节点
1 for (Node<E> t = tail, p = t; ; ) {
2 Node<E> q = p.next;
3 if (q == null) {
4 if (p.casNext(null, newNode)) {
5 if (p != t)
6 casTail(t, newNode);
7 return true;
8 }
9 } else if (p == q)
10 p = (t != (t = tail)) ? t : head;
11 else
12 p = (p != t && t != (t = tail)) ? t : q;
13 }
- UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
NULL -> A -> B -> C
| |
Head Tail
1 for (; ; ) {
2 for (Node<E> h = head, p = h, q; ; ) {
3 E item = p.item;
4
5 if (item != null && p.casItem(item, null)) {
6 if (p != h)
7 updateHead(h, ((q = p.next) != null) ? q : p);
8 return item;
9 } else if ((q = p.next) == null) {
10 updateHead(h, p);
11 return null;
12 } else if (p == q)
13 continue restartFromHead;
14 else
15 p = q;
16 }
17 }
12.5 容器的使用
使用较简单,没写太多 @ https://github.com/black-ant/case/tree/master/case%20Module%20Thread/case%20container
十三 . 多线程硬件级原理
TODO
十四 . Lock 锁机制

14 . 1 ReentranLock
ReentranLock 即重入锁 , 表示在单个线程内,这个锁可以反复进入,也就是说,一个线程可以连续两次获得同一把锁
- 排他锁
-
> void lock()
> Condition newCondition()
> boolean tryLock()
> void unlock()
ReentranLock 比 synchronized 提供更具拓展行的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。它的优势有:
• 可以使锁更公平。
• 可以使线程在等待锁的时候响应中断。
• 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
• 可以在不同的范围,以不同的顺序获取和释放锁。
> 响应中断
14 . 2 synchronized 和 ReentrantLock 异同
• 都实现了多线程同步和内存可见性语义。
• 都是可重入锁。
• 同步实现机制不同
○ synchronized 通过 Java 对象头锁标记和 Monitor 对象实现同步。
○ ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。
• 可见性实现机制不同
○ synchronized 依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。
○ ReentrantLock 通过 AQS 的 volatile state 保证包含共享变量的多线程内存可见性。
• 使用方式不同
○ synchronized 可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。
○ ReentrantLock 显示调用 tryLock 和 lock 方法,需要在 finally 块中释放锁。
• 功能丰富程度不同
○ synchronized 不可设置等待时间、不可被中断(interrupted)。
○ ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能
• 锁类型不同
○ synchronized 只支持非公平锁。
○ ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。
在 synchronized 优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从 synchronized 引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了 .
在两种方法都可用的情况下,官方甚至建议使用 synchronized 。
并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock
14 . 3 ReadWriteLock
ReadWriteLock ,读写锁是,用来提升并发程序性能的锁分离技术的 Lock 实现类。可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。
ReadWriteLock 对向数据结构相对不频繁地写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。ReadWriteLock 使得你可以同时有多个读取者,只要它们都不试图写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直至这个写锁被释放为止。
ReadWriteLock 对程序性能的提高主要受制于如下几个因素:
1. 数据被读取的频率与被修改的频率相比较的结果。
2. 读取和写入的时间
3. 有多少线程竞争
4. 是否在多处理机器上运行
14 . 5 Condition
在 Java SE 5 后,Java 提供了 Lock 接口,相对于 synchronized 而言,Lock 提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活
C- Condition : 条件 Condition 接口
C- ConditionObject
> AQS 等待队列与 Condition 队列是两个相互独立的队列
#await() 就是在当前线程持有锁的基础上释放锁资源,并新建 Condition 节点加入到 Condition 的队列尾部,阻塞当前线程 。
#signal() 就是将 Condition 的头节点移动到 AQS 等待节点尾部,让其等待再次获取锁。
> 流程
1. 将 head 后移
2. 释放节点 1 的锁并从 AQS 等待队列中移除
3. 将节点 1 加入到 Condition 的等待队列中
4. 更新 lastWaiter 为节点 1
5. 将 firstWaiter后移
6. 将节点 4 移出 Condition 队列
7. 将节点 4 加入到 AQS 的等待队列中去
8. 更新 AQS 的等待队列的 tail

14 . 6 ReentrantReadWriteLock
> 重入锁 ReentrantLock 是排他锁,排他锁在同一时刻仅有一个线程可以进行访问
> 读写锁 : ReadWriteLock
> 特征 :
- 公平性:支持公平性和非公平性。
- 重入性:支持重入。读写锁最多支持 65535 个递归写入锁和 65535 个递归读取锁。
- 锁降级:遵循获取写锁,再获取读锁,最后释放写锁的次序,如此写锁能够降级成为读锁。
I- ReadWriteLock
M- Lock readLock();
M- Lock writeLock();
C- ReentrantReadWriteLock : 可重入的读写锁实现类
I- ReadWriteLock
?- 内部维护了一对相关的锁,一个用于只读操作,另一个用于写入操作 , 写锁是独占的,读锁是共享的
十五 . Future
15 . 1 Future 笔记
> future 可以用于异步获取多线程任务结果 , Callable 用于产生结果,Future 用于获取结果
> 当 Future 进行 submit 开始 , 业务处理已经在多线程中开始 , 而 Get 即从多线程中获取数据
> 当 Get 获取时业务还未处理完 , 当前线程会阻塞 , 直到业务处理完成 . 所以需要注意 future 的任务安排
> 1 启动多线程任务
> 2 处理其他事情
> 3 收集多线程任务结果
> cancel(boolean)
> get : 获取结果
> get(long,TimeUtil)
> isCancelled()
> isDone() :判断是否有结果
流程类似于叫好等餐 , 等餐是花费时间的过程,但是不妨碍我们叫号
Future 接口的作用就是先生成一个 Future 对象 ,将具体的运行放入future 对象中 ,最终通过future 对象的 get 方法来获取最终的结果
15 . 2 Future Task
FutureTask 表示一个可以取消的异步运算
> 它有启动和取消运算、查询运算是否完成和取回运算结果等方法。
> 只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞
15 . 3 Future 使用
public class FutureService extends AbstractService implements ApplicationRunner, Callable<String> {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private static Long startTime;
private static Long endTime;
private String salt;
private Integer sleepNum;
public FutureService() {
}
public FutureService(String salt, Integer sleepNum) {
this.salt = salt;
this.sleepNum = sleepNum;
}
@Override
public String call() throws Exception {
logger.info("------> 业务逻辑开始执行 :{} <-------", salt);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sleepNum; i++) {
sb.append(salt);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
endTime = System.currentTimeMillis();
logger.info("------> {} - 业务执行完成 :{} <-------", salt, sb.toString());
getTime(startTime, endTime);
return sb.toString();
}
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("------> 创建一个初始连接池 <-------");
ExecutorService executor = Executors.newFixedThreadPool(3);
logger.info("------> 开始业务一 future - a : <-------");
FutureTask<String> future = new FutureTask<String>(new FutureService("a", 10));
startTime = System.currentTimeMillis();
executor.submit(future);
logger.info("------> 业务一请求完毕!主线程执行 <-------");
logger.info("------> 开始业务二 future - b : <-------");
FutureTask<String> future2 = new FutureTask<String>(new FutureService("b", 5));
startTime = System.currentTimeMillis();
executor.submit(future2);
logger.info("------> 业务三请求完毕!主线程执行 <-------");
logger.info("------> 开始业务三 future - c : <-------");
FutureTask<String> future3 = new FutureTask<String>(new FutureService("c", 3));
startTime = System.currentTimeMillis();
executor.submit(future3);
logger.info("------> 业务三请求完毕!主线程执行 <-------");
logger.info("------> future2 数据处理完成:{} <-------", future2.get());
logger.info("------> 2-1 测试主线程是否阻塞 <-------");
logger.info("------> future1 数据处理完成:{} <-------", future.get());
logger.info("------> 1-3 测试主线程是否阻塞 <-------");
logger.info("------> future3 数据处理完成:{} <-------", future3.get());
}
}
19.839 [ main] this is run <-------
19.839 [ main] 开始业务一 future - a : <-------
19.839 [ main] 业务一请求完毕!主线程执行 <-------
19.839 [ main] 开始业务二 future - b : <-------
19.840 [ main] 业务三请求完毕!主线程执行 <-------
19.840 [ main] 开始业务三 future - c : <-------
19.840 [ main] 业务三请求完毕!主线程执行 <-------
19.840 [pool-4-thread-2] 业务逻辑开始执行 :b <-------
19.840 [pool-4-thread-1] 业务逻辑开始执行 :a <-------
19.840 [pool-4-thread-3] 业务逻辑开始执行 :c <-------
22.843 [pool-4-thread-3] c - 业务执行完成 :ccc <-------
22.843 [pool-4-thread-3] time is :3.0 <-------
24.844 [pool-4-thread-2] b - 业务执行完成 :bbbbb <-------
24.844 [pool-4-thread-2] time is :5.0 <-------
24.844 [ main] future2 数据处理完成:bbbbb <-------
24.844 [ main] 2-1 测试主线程是否阻塞 <-------
29.847 [pool-4-thread-1] a - 业务执行完成 :aaaaaaaaaa <-------
29.847 [pool-4-thread-1] time is :10.0 <-------
29.847 [ main] future1 数据处理完成:aaaaaaaaaa <-------
29.847 [ main] 1-3 测试主线程是否阻塞 <-------
29.847 [ main] future3 数据处理完成:ccc <-------
> future submit 多线程执行
> future get 会阻塞主线程等待 , 当 get 时 , 多线程才会把数据提供出来
15 . 4 Future 原理
按照最常见的用法 , 我们通过 executor.submit 时会返回一个 Future 对象 ,然后通过 Future 对象获取 .
我们以这个方法去推理 :
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(A Callable Object);
Step 1 : 当通过 submit 调用的时候 , 底层会调用 :
return new FutureTask<T>(runnable, value);
Step 2 : 在外层会被提升为父类 RunnableFuture , 在返回的时候又会被提成 Future
RunnableFuture<T> ftask = newTaskFor(task, result);
总结 : 所以 , 底层的实现类主要可以看成 FutureTask , 而task 实际上可以算 Runnable 的实现类
C- FutureTask : FutureTask 中主要有三个参数
F- Callable<V> callable;
F- Object outcome : 保存需要输出的对象
F- Thread runner;
F- volatile WaitNode waiters;
运行基于 run() ,
1 通过调用 callable.call() 完成
2 如果call执行成功,则通过set方法保存结果 ,将 result 保存到 outcome;
3 如果call执行有异常,则通过setException保存异常;
通过调用 report(s) 完成调用
if (s <= COMPLETING){
s = awaitDone(false, 0L);
}
awaitDone 中主要做了以下几件事 :
1、如果主线程被中断,则抛出中断异常;
2、判断FutureTask当前的state,如果大于COMPLETING,说明任务已经执行完成,则直接返回;
3、如果当前state等于COMPLETING,说明任务已经执行完,这时主线程只需通过yield方法让出cpu资源,等待state变成NORMAL;
4、通过WaitNode类封装当前线程,并通过UNSAFE添加到waiters链表;
5、最终通过LockSupport的park或parkNanos挂起线程;
cancel(boolean mayInterruptIfRunning) 中 t.interrupt() , 并且 UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); 修改状态
return state >= CANCELLED;

15 . 5 Future 与 Callable
<T> Future<T> submit(Callable<T> task);
- RunnableFuture<T> ftask = newTaskFor(task);
- return new ForkJoinTask.AdaptedCallable<T>(callable);
- execute(ftask);
<T> Future<T> submit(Runnable task, T result);
- RunnableFuture<T> ftask = newTaskFor(task, result);
- execute(ftask);
15 . 7 衍生用法 ScheduledFutureTask
• time:任务执行时间;
• period:任务周期执行间隔;
• sequenceNumber:自增的任务序号。
M- getDelay(TimeUnit unit)
M- int compareTo(Delayed other)

十六 . Fork / join
16 . 1 简述
: 该框架是一个工具 , 通过分而治之的方式尝试将所有可用的处理器内核使用起来帮助加速并行处理
- fork : 递归地将任务分解为较小的独立子任务 , 直到它们足够简单以便异步执行
- join : 将所有子任务的结果递归的连接成单个结果
> Fork / Join 的执行是先把一个大任务分解(fork)成许多个独立的小任务,然后起多线程并行去处理这些小任务。处理完得到结果后再进行合并(join)就得到我们的最终结果。
> Fork / Join 使用的算法为 work-stealing(工作窃取) , 该算法会把分解的小任务放在多个双端队列中,而线程在队列的头和尾部都可获取任务。当有线程把当前负责队列的任务处理完之后,它还可以从那些还没有处理完的队列的尾部窃取任务来处理
> 专属线程池 :
- ForkJoinPool : 用于管理 ForkJoinWorkerThread 类型的工作线程
> 实现了 ExecutorService接口 的多线程处理器
> 把一个大的任务划分为若干个小的任务并发执行,充分利用可用的资源,进而提高应用的执行效率
> ForkJoinPool:充当fork/join框架里面的管理者,最原始的任务都要交给它才能处理。它负责控制整个fork/join有多少个workerThread,workerThread的创建,激活都是由它来掌控。它还负责workQueue队列的创建和分配,每当创建一个workerThread,它负责分配相应的workQueue。然后它把接到的活都交给workerThread去处理,它可以说是整个frok/join的容器。
> ForkJoinWorkerThread:fork/join里面真正干活的"工人",本质是一个线程。里面有一个ForkJoinPool.WorkQueue的队列存放着它要干的活,接活之前它要向ForkJoinPool注册(registerWorker),拿到相应的workQueue。然后就从workQueue里面拿任务出来处理。它是依附于ForkJoinPool而存活,如果ForkJoinPool的销毁了,它也会跟着结束。
> ForkJoinPool.WorkQueue: 双端队列就是它,它负责存储接收的任务。
> ForkJoinTask:代表fork/join里面任务类型,我们一般用它的两个子类RecursiveTask、RecursiveAction。这两个区别在于RecursiveTask任务是有返回值,RecursiveAction没有返回值。任务的处理逻辑包括任务的切分都集中在compute()方法里面。
16 . 2 Fork Join 用法
public class ForkJoinPoolReferenceService extends RecursiveTask<Integer> {
private File file;
private Integer salt;
public ForkJoinPoolReferenceService(File file, Integer salt) {
this.file = file;
this.salt = salt;
}
@Override
protected Integer compute() {
return ForkFileUtils.read(file, salt);
}
}
ForkJoinPoolReferenceService rt = new ForkJoinPoolReferenceService(files.get(0), i);
rt.fork();
result = result + rt.join();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(rt);
result = result + forkJoinTask.get();
16 .3 Fork/Join 源码梳理
public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
if (task == null)
throw new NullPointerException();
externalPush(task);
return task;
}
M- externalPush
TODO
16 . 4 衍生类 ForkJoinTask
十七 . 多线程算法
15 . 1 算法简述
> 线程调度算法
- 分时调度模型
: 让所有的线程轮流获得CPU的使用权 ,平均分配占用的时间
- 抢占式算法 (Java 虚拟机)
: 优先让运行池中优先级高的线程占用 CPU
: 优先级相同时随机选择
> 线程优先级
: 线程优先级是 int 类型的 1-10
: 线程优先级虽然定义后但是不一定优先级高的先执行
: 线程的优先级调度通常会被委托给操作系统
15 . 2 线程池的实现算法
十八 . 设计模式
> 模式 : 针对某个语境下反复出现的问题得解决方案
> 语境 : 问题所处得状况和背景或者说上下文
> 模式语言 : 模式得集合

Single Threaded Execution : 能同时通过一座桥得只有一个人
> 语境 : 多个线程共享实例时
> 问题 : 实例状态不可随意改变 ,影响实例安全性
> 解决 : 严格规定实例得不稳定状态得范围 , 施加保护 , 确保临界区只能被一个线程执行 ,保证线程安全
> 实现 : 通过 synchronized 实现临界区
Immutable : 想破坏也破坏不了
> 语境 : 虽然多个线程共享了实例 ,但是实例得状态不会改变
> 问题 : 使用 Single Threaded Excution 模式下 ,吞吐量会下降
> 解决方案 : 对于创建后不会变动得对象 ,使用该模式保证类得不可变性可以提高吞吐量 ,
> 实现 : 通过 private 隐藏字段或者 final 保证其不可改变
Guarded Suspension : 等我准备好
> 语境 : 多个环境共享实例
> 问题 : 多个线程不可以随意得访问实例 , 如果实例没有准备好 , 会影响安全性
> 解决 : 为实例准备守护状态 , 在执行危险处理前 ,检查实例得安全状态 ,不安全情况下使线程等待
> 实现 : while + wait + notify 实现
Balking : 不需要就算了
> 语境 : 多个线程共享实例时
> 问题 : 均可访问影响安全性 , 守护等待影响性能
> 解决 : 不通过while处理 ,中断线程立即返回
> 实现 : if + return/throw
>???? 这逻辑有毛用啊
Producer - Consumer : 我来做 ,你来用
> producer : 生产者 , 生产数据的线程
> consumer : 消费者 , 使用数据的线程
> 要求 : 生产者安全地将数据交给消费者 ,
> 问题 : 业务的处理效率会导致2者之间的处理速度存在差异 ,
- 添加新角色 ,桥梁角色 , 用于消除多线间处理速度的差异
> Pipe : 当生产者和消费者都只有一个的时候
> 模式解释 :
1 . 一个地方只能容纳指定数量的物品的桌子,当物品满了的时候 ,只能等移除其他物品才能加入新的物品 ,
> 解决 : 准备中间角色 Channel , Channel 中持有多个数据 , 在Channel 中进行线程互斥
Read - Write - Lock : 读写锁 ,可以一起读 ,但是不能写
> 语境 : 多个线程共享实例 ,存在可以读取和改变线程状态的情况
> 问题 : 需要兼顾安全性和吞吐量
> 解决 : 将读取锁和写入锁进行区分 , Reader - Reader 没有影响 ,Reader-Writer , Writer - writer 互斥
> 方案 :
Thread-Per-Message : 这项工作就交给你了
> 语境 : 当线程调用实例的方法
> 问题 : 实例的处理时间较长 , 线程无法获取程序的控制权
> 解决 :
> 方案 : 通过匿名内部类启动新线程
Worker Thread : 持续工作
> 问题 : 虽然可以通过启动新线程释放空间 ,但是会造成负荷 ,所以采用工人线程进行循环使用
> 方案 : 线程池
Future 模式 : 先给提货单
> 与其一直等待 , 不如先拿一个提货单
- data.getContent 获取运行结果
1 如果其他线程完成了运算 ,会立即返回结果
2 如果其他线程还没有处理请求 ,会继续等待运行结果
> future 模式中 ,核心的对象就是 FutureData 和 RealData
- FutureData 中持有 realData 对象和 isReady 对象
- 当getContent 时 ,判断 isReady 并且进入等待 , 获取 realData 对象
: 所以避免因为未 Ready 导致的阻塞过程
Two-Phase Termination 模式 : 先收拾房间再睡觉
> 问题 : 紧急停止的线程会失去安全性
> 解决 :
Thread-Specific Storage 模式 : 一个
为每个线程准备存储空间 ,就算只有一个入口 ,也会在内部为每个线程分配特有的存储空间
> ThreadLocal 类 : 该类即为存储间 , 或者可以以某个角度将其看成集合,ThreadLocal 可以管理多个对象 , 提供了getter 和 setter 方法
- set : 通过currentThread 获取线程的值作为key,将线程的参数和调用该方法的线程进行对应
- get : 以 当前线程的值!去获取set 方法关联的对象
> ThreadLocal 对象是一个泛型类 , 可以自定义存储的格式
十九 . 零散知识点
# 并发和并行
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件;并发是在同一实体上的多个事件。
解释三:在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如 Hadoop 分布式集群。
# 线程的基本概念
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。
进程中的一个执行流程,一个进程中可以运行多个线程
1、java.lang.Thread类的一个实例;
2、线程的执行。
• 方式一,继承 Thread 类创建线程类。
- 编写简单 ,可以直接通过this访问当前线程
- 多继承限制了能力的发展
• 方式二,通过 Runnable 接口创建线程类。
• 方式三,通过 Callable 和 Future 创建线程。
- 可以使用线程池 , 设计清楚
: 当 2 个人去约会的情况下 ,当不互相沟通的情况下 ,我们是不能了解到对方的情况 ,对方的情况就是状态 ,当我们打一次电话就是同步了一次状态 ,当挂掉后 , 双方继续同时做事 ,此时状态又不可见
: 检查在运行(例如惰性初始化)
: 读 - 改 -写 : 自增
# 线程的创建
方式一,继承 Thread 类创建线程类。
方式二,通过 Runnable 接口创建线程类。
方式三,通过 Callable 和 Future 创建线程。
# 线程的基本使用
> 扩展java.lang.Thread类 , 直接 new
> 实现java.lang.Runnable接口 , 调用 Thread 的 构造方法
> 启动教程 : 调用 start()
在调用start()方法之前:线程处于新状态中,新状态指有一个Thread对象,但还没有一个真正的线程。
在调用start()方法之后:发生了一系列复杂的事情
- 启动新的执行线程(具有新的调用栈)
- 该线程从新状态转移到可运行状态
- 当该线程获得机会执行时,其目标run()方法将运行。
> run 和 start 的区别
- 调用start 的时候 , 会创建新的线程 , 并且执行run 方法
- 调用run 方法仅仅是简单的调用
# 实现Runnable接口和Callable接口的区别**
Runnable接口或Callable接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。
工具类Executors可以实现Runnable对象和Callable对象之间的相互转换。
# 常见的线程安全和线程不安全
- 线程安全
struts2
饿汉式单例模式
双检锁单例模式
- 线程不安全
servlet
springMVC
懒汉式单例模式 (不加同步锁时)
# 写时复制技术
写时复制就是将共享访问的对象变为只读的,写的时候,再使用锁,保证只有一个线程写,写的线程不是直接修改原对象,而是新创建一个对象,对该对象修改完毕后,再原子性地修改共享访问的变量,让它指向新的对象。
这个可以通过容器类 : CopyOnWriteArrayList和CopyOnWriteArraySet 来体现
# 线程的 CPU 时间片
- 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
- 当前运行线程结束,即运行完run()方法里面的任务。
Java的线程的调度机制都由JVM实现,假如有若干条线程,你想让某些线程拥有更长的执行时间,或某些线程分配少点执行时间,这时就涉及“线程优先级”,Java把线程优先级分成10个级别,线程被创建时如果没有明确声明则使用默认优先级,JVM将根据每个线程的优先级分配执行时间的概率。
有三个常量Thread.MIN_PRIORITY、Thread.NORM_PRIORITY、Thread.MAX_PRIORITY分别表示最小优先级值(1)、默认优先级值(5)、最大优先级值(10)。
由于JVM的实现以宿主操作系统为基础,所以Java优先级值与各种不同操作系统的原生线程优先级必然存在某种映射关系,这样才足以封装所有操作系统的优先级提供统一优先级语义。例如1-10优先级值在linux可能要与0-99优先级值进行映射,而windows系统则有7个优先级要映射。
线程的调度策略决定上层多线程运行机制,JVM的线程调度器实现了抢占式调度,每条线程执行的时间由它分配管理,它将按照线程优先级的建议对线程执行的时间进行分配,优先级越高,可能得到CPU的时间则越长。
# 执行execute()方法和submit()方法的区别
1)execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
# 定时组件
Future + Callable
?- 可以实现有效的回调
java.util包中的Timer和TimerTask
?- 一个Timer对象背后只有一个Timer线程
Java并发包中的ScheduledExecutorService
# 线程的名称
- 线程总是有名称 ,如果没有定义 , 则线程名称由 Java 虚拟机指定
- 任何线程的名称都可以设置和获取
# 守护线程
- 线程分为守护线程和用户线程
- 任何线程都可以设置为守护线程(setDeamor(Boolean on))和用户线程
- 守护线程的设置必须再 start 方法之前
- 守护线程是 JVM 自动创建的线程 ,用户线程是程序创建的线程
- 普通线程和守护线程的区别是普通线程全部关闭后 , JVM 才会关闭 ,从而关闭守护线程
# 线程饥饿
- 一个或者多个线程因为种种原因无法获取所需要的资源,导致一直无法执行的状态
- 高优先级线程占用了所有的 CPU 时间
- 其他线程持续的占用同步块,导致某个线程一直等待同步块
# 线程停和线程启的几种方法和区别
- wait : 使一个线程处于等待的状态 ,并且释放锁
- sleep : 使一个正在运行的线程处于睡眠状态 , 此方法为一个静态方法
- notify : 唤醒一个等待的线程 , 调用此方法时并不能确定会唤醒哪一个线程, 且与优先级无关 这个唤醒操作会由 JVM 决定
- notifyAll : 唤醒所有等待的线程 , 由所有线程去竞争锁 , 获取了锁的线程会进入就绪状态
# 为什么 wait / notify / notifyall 方法不在Thread 类里面 ?
Java 提供的锁是对象级而不是线程级的 , 每一个对象都有一个锁
# sleep、join、yield
- sleep : sleep 是系统级 , 精准的让出 CPU 资源 , 让其他的线程有机会执行(使低优先级的线程有机会执行) , 但是这个状态不会释放锁
- yield : 该方法是使线程重新回到可执行的状态 ,但是因为是放入线程池里面 , 所以随时都可能进入到运行状态
sleep(0) : 主要占用是让出资源 ,使其他的对象有机会获取线程锁
十九 . 优化和实践
19 . 1 常见的优化方式
• 1、给线程命名。
这样可以方便找 bug 或追踪。OrderProcessor、QuoteProcessor、TradeProcessor 这种名字比 Thread-1、Thread-2、Thread-3 好多了,给线程起一个和它要完成的任务相关的名字,所有的主要框架甚至JDK都遵循这个最佳实践。
• 2、最小化同步范围。
锁花费的代价高昂且上下文切换更耗费时间空间,试试最低限度的使用同步和锁,缩小临界区。因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。
• 3、优先使用 volatile ,而不是 synchronized 。
• 4、尽可能使用更高层次的并发工具而非 wait 和 notify 方法来实现线程通信。
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。
其次,这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。
• 5、优先使用并发容器,而非同步容器。
这是另外一个容易遵循且受益巨大的最佳实践,并发容器比同步容器的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到 Map ,我们应该首先想到用 ConcurrentHashMap 类。
6、考虑使用线程池。
19 . 2 多线程情况单例模式安全版
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private volatile static Singleton singletonOne;
public static Singleton getInstanceOne() {
if (singletonOne == null) {
synchronized (Singleton.class) {
if (singletonOne == null) {
singletonOne = new Singleton();
}
}
}
return singletonOne;
}
private static class SingletonHolder {
public static Singleton singleton = new Singleton();
}
public static Singleton getInstanceTwo() {
return SingletonHolder.singleton;
}
写在最后
作为一篇总结性质的文章 , 其中还是有很多不足 , 整个系列期望是能作为一个手册 , 供大家开始查找 , 同时也是对自己的一种提升 , 整个文档的完成 , 能明显感觉对多线程有了进一步的理解 .
后面还会陆续推出 Spring 的系列 , 毕竟Spring 是那么诱人 , 大不了 , 不要这肝了!!!!
致谢
完成这一篇笔记 , 拜读了很多文档 , 每一篇都能给我带来很多的新知识 , 有些甚至原样引用自部分博客 , 有些是JDK直译 , 毕竟别人写的那么好了 , 又何必去动他们呢
有些知识点是很早之前记录的 , 很难追溯 , 在这里对这些博主以及未录入的作者表示感谢
芋道源码 : http://www.iocoder.cn/JUC/sike/aqs-3/
https://mp.weixin.qq.com/s?__biz=MzIxOTI1NTk5Nw==&mid=2650047475&idx=1&sn=4349ee6ac5e882c536238ed1237e5ba2&chksm=8fde2621b8a9af37df7d0c0a7ef3178d0253abf1e76a682134fce2f2218c93337c7de57835b7&scene=21
https://blog.csdn.net/javazejian/article/details/70768369
死磕系列 , 我的多线程导师
http://cmsblogs.com/?cat=151
https://blog.csdn.net/iter_zc/article/details/41943387