1 什么是 JUC
java.util.concurrent 并发工具包 用来优雅的解决多线程下的高并发问题,JUC 由Doug Lea 设计开发,此乃神人也!
让 Java 程序员膜拜的大神,这个憨态可掬的老者让人又爱又恨!
JUC 的基本框架
文章目录
2 线程与进程
概念
进程:一个程序,或者程序的集合
一个进程可以包含多个线程,至少包含一个java默认有2个线程 mian GC
线程:进程下的不同操作有线程操作负责的 Thread Runnable Callable
java 无法直接操作硬件。本身无法开启线程 使用本地方法native 调用底层的c++方法来开启线程
并发、并行
并发:多个线程同时操作同一资源
- CPU 一核 模拟多个线程,快速交替执行
并行:多个线程同时执行
- CPU 多核多个线程可以同时执行
并发编程本质:充分利用cpu的资源
查看进程和线程的方法
线程运行原理
两种线程模型
用户级线程(ULT)—用户程序实现不依赖于操作系统。应用提供创建,同步,调度管理线程的函数来控制。不需要用户态/内核态来切换,速度快。内核对ULT无感知,线程阻塞则进程就会阻塞
内核级线程(KLT)—系统内核管理线程(KLT),内核保存线程的状态和上下文信息,线程阻塞不会引进进程阻塞,可以在多处理器上并行运行,线程的创建与调度由内核完成,速度比UTL要慢,但比进程操作要快。java JVM就是使用KLT模型
JVM通过调用内核系统开放的API来创建线程,最后映射到底层的cpu
栈与栈帧
Java Virtual Machine Stacks(Java虚拟机栈)
JvM中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存
-
每个栈由多个栈帧( Frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
栈帧图解
线程上下文切換( Thread Context Switch)
因为以下ー些原因导致cpu不再执行当前的线程,转而执行另一个线程的代码
- 线程的cpu时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、 synchronized、lock等方法
当 Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器( Program Counter Register),它的作用是记住下ー条jvm指令的执行地址,是线程私有的
常用方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新线程中运行 run 方法中的代码 | start 方法只是让线程进入就绪状态,里面代码不一定立刻运行,只有当 CPU 将时间片分给线程时,才能进入运行状态,执行代码。每个线程的 start 方法只能调用一次,调用多次就会出现 IllegalThreadStateException | |
run() | 新线程启动会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n 毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记,park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
sleep 与yield
sleep
调用slep会让当前线程从 Running进入Timed Waiting状态
其它线程可以使用 Interrupt方法打断正在睡眠的线程,这时sleep方法会抛出 nterruptedexception
睡眠结東后的线程未必会立刻得到执行
建议用 Timeunit 的 sleep代替 Thread的 sleep来获得更好的可读性
sleep实现
在没有利用cpu来计算时,不要让 while(true)空转浪费cpu,这时可以使用 yield或slep来让出cpu的使用
权给其他程序
while(true) {
try{
Thread.sleep(50);
} catch (Interruptedexception e) {
e.printstacktrace();
}
}
- 可以用wait或条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep适用于无需锁同步的场景
yield
- 调用 yield会让当前线程从 Running进入Rble状态,然后调度执行其它同优先级的线程。如果这时没
有同优先级的线程,那么不能保证让当前线程暂停的效果 - 具体的实现依赖于操作系统的任务调度器
join
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
interrupt
interrupt 打断线程有两种情况:
如果一个线程在在运行中被打断,打断标记会被置为 true 。
如果是打断因sleep wait join 方法而被阻塞的线程,并抛出中断异常,会将打断标记置为 false 。
isInterrupted() 与 interrupted() 比较 :
首先,isInterrupted 是实例方法,interrupted 是Thread的静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted 方法查看线程的时候,不会将打断标记清空,也就是置为 false,interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记,简单来说,interrupt() 方法类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。
public class InterruptedTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
//try {
System.out.println("正常情况下");
//Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
},"t");
t.start();
t.interrupt();
Thread.sleep(2);
System.out.println("中断标记:"+t.isInterrupted());//打断阻塞状态的线程会抛出中断异常, 会清除中断标志位为false,正常模式下不会清除中断标志位
System.out.println("中断标记:"+Thread.interrupted());//会清除中断标志位为false
}
}
两阶段终止模式
Two Phase Termination
在一个线程T1中如何“优雅”终止线程T2?
使用线程对象的 stop方法停止线程
stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就
再也没有机会释放锁,其它线程将永远无法获取锁
使用 System. exit(int)方法停止线程
目的仅是停止一个线程,但这种做法会让整个程序都停止
public class TwoPhaseTerminationTest {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination toPhaseTermination = new TwoPhaseTermination();
toPhaseTermination.start();
System.out.println(111);
TimeUnit.SECONDS.sleep(5);
System.out.println(2);
toPhaseTermination.stop();
}
}
class TwoPhaseTermination{
private Thread moThread;
void start(){
moThread = new Thread(()->{
while (true) {
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()) {
System.out.println("料理后事!");
break;
}
else {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("继续监控");
} catch (InterruptedException e) {
currentThread.interrupt();
e.printStackTrace();
}
}
}
});
moThread.start();
}
void stop() {
moThread.interrupt();
}
}
线程的状态
操作系统层面
- 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
- 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换 - 阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
- 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
JVM进程和内核系统关系
Java API层面
- NEW 跟五种状态里的初始状态是一个意思
- RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
blocked 与 WAITING 区别
BLOCKED是指线程正在等待获取锁; WAITING是指线程正在等待其他线程发来的通知
( notify),收到通知后,可能会顺序向后执行( RUNNABLE),也可能会再次获取锁,进而被阻
塞住( BLOCKED)
线程的状态切换
线程活跃性
线程因为某些原因,导致代码一直无法执行完毕,这种的现象叫做活跃性。
死锁
不同的线程分别占用对方需要同步的资源不放弃,都在等待对方放弃自己需要同步的资源,就形成了线程的死锁。
一个线程需要同时获取多把锁,这时就容易发生死锁
public class ThreadDeadLockTest {
public static void main(String[] args) {
StringBuffer s1=new StringBuffer();
StringBuffer s2=new StringBuffer();
//匿名类的匿名对象
new Thread(() -> {
synchronized(s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized(s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized(s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized(s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}).start();
}
}
定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
jps 查看线程id
jstack 该线程id
死锁代码位置
使用jconsole
活锁
活锁出现在两个线程互相改变对方的结束条件,谁也无法结束。
避免活锁的方法
在线程执行时,中途给予不同的间隔时间即可。
死锁与活锁的区别
- 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞,停止运行的现象。
- 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。
public class LiveLockTest {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
try {
TimeUnit.SECONDS.sleep( 1);
//TimeUnit.SECONDS.sleep( 2);更改睡眠时间间隔解决活锁
count--;
System.out.println(count);;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
try {
TimeUnit.SECONDS.sleep( 1);
count++;
System.out.println(count);;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t2").start();
}
}
线程饥饿
某些线程因为优先级太低,导致一直无法获得资源的现象。
在使用顺序加锁时,可能会出现饥饿现象
3 线程共享与安全
线程共享
线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了。
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 1;i < 5000; i++){
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 1;i < 5000; i++){
count--;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.print("count")
}
当执行 count++ 或者 count-- 操作的时候,从字节码分析,实际上是 4 步操作。
count++; // 操作字节码如下:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
count--; // 操作字节码如下:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
线程内存与共享内存
出现负数
出现正数
阻塞式解决线程安全
Synchronized
俗称的【对象锁】,它采用互斥的方式让
同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能
保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
虽然java中互斥和同步都可以采用 synchronized关鍵字来完成,但它们还是有区别的
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
语法
synchronized(对象)//线程1,线程2(b1 ocked)
{
临界区 // synchronized 包裹的临界区代码越小越好,提升同步性能
}
线程八锁
Sychronized 作用于对象的八种情况
Sychronized 作用于非静态方法:
Sychronized 锁的对象是非静态方法的调用者,相同对象的同一把锁哪个线程先抢到锁就先执行
普通方法不受锁的影响,若延迟小会先执行
不同的对象使用锁则是不同的锁,即A线程上使用A锁。B线程使用B锁,A和B哪个延迟(sleep)小则先执行
public class SynchronizedTest {
public static void main(String[] args) {
Phone phone = new Phone();
//作用于同一个对象phone 哪个线程先抢到锁就先执行 此时发短信先执行-因为发短信的线程先开启先抢到锁。
new Thread(phone::sendMes,"A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// new Thread(phone::call,"B").start();--普通非同步方法,不受锁的影响,此线程没有延迟且A线程睡眠2秒主线程只睡眠1秒所以hello先执行
new Thread(phone::say,"B").start();
}
}
class Phone{
public synchronized void sendMes(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信!");
}
public synchronized void call(){
System.out.println("打电话");
}
public void say(){
System.out.println("hello");
}
}
Sychronized 都作用于静态方法
锁的是Class 是唯一的,类一加载就有了,会按类的加载对象的顺序执行
public static void main(String[] args) {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
//锁的存在 ,锁的是Class 示例对象,发短信先开启,先抢到锁,发短信先执行
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
class Phone{
// static 静态方法
// 类一加载就有了!锁的是Class
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public static synchronized void call(){
System.out.println("打电话");
}
}
Sychronized 一个作用静态方法,一个普通同步方法,同一个对象
是不同的锁,两者不受影响, 两者并行执行,谁延迟小则先执行
Sychronized 一个作用静态方法,一个普通同步方法,不同的对象
是不同的锁,两者不受影响, 两者并行执行,谁延迟小则先执行
public static void main(String[] args) {
// 两个对象的Class类模板只有一个,static,锁的是Class
Phone phone1 = new Phone();
Phone phone2 = new Phone();
//锁的存在
new Thread(()->{
phone1.sendSms();
},"A").start();
// 捕获
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
},"B").start();
}
}
class Phone{
// 静态的同步方法 锁的是Class类模板
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
// 二者锁的对象不同,所以不需要等待
public synchronized void call(){
System.out.println("打电话");
}
}
总结:
当 synchronized作用于非静态方法时锁住的是当前对象的实例,作用于静态方法时锁住的是class的实例,class的数据存储于永久带所以静态方法相当于类的全局锁
Sychronized 作用于无论是静态方法和非静态方法,肯定是不同的锁
Sychronized 作用于普通方法,同一个对象谁先抢到锁谁就先执行
Sychronized 作用于不同的对象的方法,是不同的锁两者不受影响,两者并行执行,谁延迟小则先执行
变量的线程安全
成员变量和静态变量是否线程安全?
-
如果它们没有共享,则线程安全
-
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
如果只有读操作,则线程安全
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程私有的是安全的
但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
/*
可以看到重写的方法中又使用到了线程,子类重写方法将局部变量的引用暴露出去。当主线程和重写的 method3 方法的线程同时存在,此时 list 就是这两个线程的共享资源了,就会出现线程安全问题
解决:method2 method3 权限改成私有private,对于不想被子类覆盖的方法可以使用final阻止子类修改
*/
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector (List的线程安全实现类)
- Hashtable (Hash的线程安全实现类)
- java.util.concurrent 包下的类
多个线程调用它们同一个实例的某个方法时,是线程安全的
多个方法的组合操作同一共享数据,虽然这些方法都是安全的但对一个共享数据进行组合读写,会受到线程切换的影响,造成线程不安全
但操作不同的共享数据,是线程安全的
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
String、 Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
String 里的replace subString 方法均返回新建一个对象 ,没有对原对象修改等操作,是线程安全的。
分析线程是否安全,先对类的成员变量,类变量,局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的
Synchronized 底层原理
ReentrantLock
共享模型之内存
线程安全之无锁
自旋锁
MCS锁
MCS是一种基于单向链表的高性能、公平的自旋锁。申请加锁的线程通过当前节点的变量进行自旋(locked == true)。在前置节点解锁后,会修改当前节点的锁值(locked ==false),这一刻当前节点会结束自旋,并进行加锁。
申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减小了没必要要的处理器缓存同步的次数,下降了总线和内存的开销。
public class MCSLock {
public static class MCSNode {
volatile MCSNode next;
volatile boolean blocked = true;//等待锁
}
private final ThreadLocal<MCSNode> currentThreadNode = new ThreadLocal<>();
volatile MCSNode tail;
private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "tail");
public void lock() {
MCSNode cNode = currentThreadNode.get();
if (cNode == null) {
cNode = new MCSNode();
currentThreadNode.set(cNode);
}
MCSNode predecessor = UPDATER.getAndSet(this, cNode); // step 1
if (predecessor != null) {
// 形成单链表
predecessor.next = cNode; // step 2
// 当前线程等待等待前驱节点主动通知,即将blocked设置为false,表示当前线程可以获取到锁
while (cNode.blocked) {
Thread.onSpinWait();
}
} else {
// 加锁成功
cNode.blocked = false;
}
}
public void unlock() {
// 获取当前线程对应的节点
MCSNode cNode = currentThreadNode.get();
if (cNode == null || cNode.blocked) {
//当前线程处于等待不是锁拥有者
return;
}
// 没有后继节点的情况
if (cNode.next == null ) {
//tail 设置为null 成功直接return
if (UPDATER.compareAndSet(this, cNode, null)){
return;
}
// 如果CAS操作失败了等待其他线程操作结束
// 因为上述的lock操作中step 1执行完后,step 2可能还没执行完
while (cNode.next == null) {
Thread.onSpinWait();
}
}
// 当前节点通知后继节点可以获取锁
cNode.next.blocked = false;
cNode.next = null;
currentThreadNode.remove();//使用完删除线程内部变量避免内存泄漏
}
}
- MCS锁的节点对象需要有两个状态,next用来维护单向链表的结构,blocked用来表示节点的状态,true表示处于自旋中;false表示加锁成功
- MCS锁的节点状态blocked的改变是由其前驱节点触发改变的
- 加锁时会更新链表的末节点并完成链表结构的维护
- 释放锁的时候由于链表结构建立的时滞(getAndSet原子方法和链表建立整体而言并非原子性),可能存在多线程的干扰,需要使用忙等待保证链表结构就绪
CLH锁
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋
public class CLHLock{
private static class CLHNode {
volatile boolean lock = true;//等待锁
}
private volatile CLHNode tail = null;
private final ThreadLocal<CLHNode> currentThreadNode = new ThreadLocal<>();
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class,"tail");
public void lock() {
CLHNode cNode = currentThreadNode.get();
if (cNode == null) {
cNode = new CLHNode();
currentThreadNode.set(cNode);
}
// 轮询前驱节点的locked状态
CLHNode predecessor = UPDATER.getAndSet(this, cNode);
if (predecessor != null) {
while (predecessor.lock) {
Thread.onSpinWait();
}
}
//已经成功获取到了锁
cNode.lock = true;
}
public void unlock() {
CLHNode cNode = currentThreadNode.get();
// 只有持有锁的线程才能够释放
if (cNode == null || !cNode.lock) {
return;
}
currentThreadNode.remove();
// 尝试将tail从currentThread变更为null,失败表示还有线程在等待加锁,则自己需要释放锁
if (!UPDATER.compareAndSet(this, cNode, null)) {
cNode.lock = false;
}
}
}
-
CLH锁的节点对象只有一个lock属性
-
CLH锁的节点属性lock的改变是由其自身触发的
-
CLH锁是在前驱节点的lock属性上进行自旋
CLH锁是在前驱节点的lock属性上自旋。
因此当前驱节点释放了锁之后,其对应的active属性就会被设置为false,此时它的后继节点就能够退出自旋并成功地获取到锁。从字面意思上来说,当lock被设置为false之后,即表示该节点已经完成:等待加锁 - 加锁成功 - 执行业务 - 释放锁成功 这一标准的流程,节点可以被释放了,因此也就变成了不活跃状态,等待垃圾回收。
CLH节点中并没有指向其后继节点的next属性。但是这并不代表CLH锁不依赖链表这种数据结构,毕竟作为一种公平的自旋锁,CLH还是需要仰仗链表的。只不过这个链表是隐式维护的,通过原子更新器的getAndSet方法在更新tail时,可以在Set的同时获取到原来的tail节点。这也从侧面反映了,为什么CLH锁是在前驱节点的lock属性上自旋。每个节点只了解它直接前驱节点的状态,不需要显式地去维护一个完整的链表结构。
MCS锁 与CLH锁区别:
- 从代码实现来看,CLH比MCS要简单得多。
- 从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
- 从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。
- CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。
CAS
compareAndSet(比较并设置值),它的简称就是 CAS (也有 Compare And Swap 的说法),映射到操做系统就是一条CPU的原子指令。CAS需要有3个操作数: CAS(V,E,N),V表示要更新变量的主内存实际值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,若是V值和E值不一样,则说明已经有其余线程完成更新,则当前线程则什么都不作,最后CAS 返回当前V的真实值。
CAS 本质是CLH 自旋锁
CAS : 比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就一直循环!
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的 。
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。 线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
volatile 仅仅保证了共享变量的可见性,让其它线程能够看到新值,但不能解决指令交错问题(不能保证原子性)
CAS 是原子性操作借助 volatile 读取到共享变量的新值来实现【比较并交换】的效果
CAS效率
synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞耗费性能。无锁情况下,即使重试失败,虽然开始自旋但线程始终在高速运行,没有停歇 但需要额外 CPU 的支持,没有额外的cpu支持,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。所以CAS需要多核cpu才有意义并且线程数不能超过cpu的核心数
使用CAS无锁的方式彻底没有锁竞争带来的线程间频繁调度的开销和阻塞,它对死锁问题天生免疫,所以他要比基于锁的方式拥有更优越的性能。
CAS 特点
-
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
-
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量
-
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量
-
CAS 体现的是无锁并发、无阻塞并发
-
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响优点
-
由于CAS是非阻塞的,可避免死锁,线程间的互相影响非常小。
-
、没有锁竞争带来的系统开销,也没有线程间频繁调度的开销。
缺点:
1、可能自旋循环时间过长。
如果某个线程通过CAS方式操作某个变量不成功,长时间自旋,则会对CPU带来较大开销。
怎么解决:限制自旋次数。
2、ABA问题。
3、只可用来对单个变量进行同步
CAS使用时机
- 线程之间抢占资源不是特别激烈使用CAS机制,这保证了大部分线程不会是在干等资源的释放
- 等待资源释放时的CPU占用反而小于上下文切换所消耗的资源,使用CAS机制
- 线程可能出现不安全情况的条件下才使用CAS机制
- CAS 适合短时间运行的代码片段,可以利用cpu来进行更改;长时间运行的代码片段不适合。通常在长时间连接的操作如数据库连接池如果cas一定次数失败或者超时可以让该线程放弃cpu资源使其陷入等待
- CAS只能针对单个变量;如果是多个变量那么就要使用锁了;AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作
原子整数
原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
在单线程环境下整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的,
是无法保证复合操作的原子性,即使使用volatile也不行.
java.util.concurrent.atomic并发包提供了一些并发工具类,这里把它分成五类:
使用原子的方式更新基本类型来保证原子性
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
原子引用
原子引用保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人)。
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起。
CAS ABA 问题
public class CASABATest {
public static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
System.out.println(("main start..."));
String preVal = ref.get();
other();
TimeUnit.SECONDS.sleep(1);
System.out.println(ref.compareAndSet(preVal, "C"));
}
private static void other() throws InterruptedException {
new Thread(() -> {
System.out.println( ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
System.out.println( ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
CAS只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况,如果主线程希望:只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。使用AtomicStampedReference来解决。
AtomicStampedReference
哪个线程对内存的当前值进行更改了,那么该线程就需要对版本号进行+1
时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号,可以知道被修改了多少次
public class AtomicStampedReferenceTest {
public static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
System.out.println(("main start..."));
//获取当前线程读取的最新值
String preVal = ref.getReference();
other();
TimeUnit.SECONDS.sleep(2);
//版本号
int stamp = ref.getStamp();
System.out.println(ref.compareAndSet(preVal, "C",stamp,stamp+1));
}
private static void other() throws InterruptedException {
new Thread(() -> {
int stamp = ref.getStamp();
System.out.println( ref.compareAndSet(ref.getReference(),"B",stamp,stamp+1));
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
int stamp = ref.getStamp();
System.out.println( ref.compareAndSet(ref.getReference(), "A",stamp,stamp+1));
}, "t2").start();
}
}
AtomicMarkableReference
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程, 可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference 。
public class AtomicMarkableReferenceTest {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
System.out.println(("start..."));
GarbageBag prev = ref.getReference();
System.out.println(prev.toString());
new Thread(() -> {
System.out.println("start...");
bag.setDesc("空垃圾袋");
ref.compareAndSet(bag, bag, true, false);
System.out.println(bag.toString());
},"保洁阿姨").start();
Thread.sleep(1);
System.out.println("想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
System.out.println("换了么?" + success);
System.out.println(ref.getReference().toString());
}
}
class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return super.toString() + " " + desc;
}
}
原子数组
使用原子的方式更新数组里的某个元素,原子数组保证数组里的元素线程安全
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :引用类型数组原子类
public class AtomicIntArrayTest {
public static void main(String[] args) {
AtomicIntegerArray array = new AtomicIntegerArray(new int[]{45,23,13,47,12,42});
for (int i = 0; i <array.length() ; i++) {
final int j=i;
new Thread(()->{
array.compareAndSet(j, 13, 31);
array.getAndAdd(j, 2);
array.decrementAndGet(j);
array.getAndSet(j, array.get(j) - 1);
}).start();
}
System.out.println(array);
}
}
字段更新器
保护对象里的属性线程安全
- AtomicReferenceFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域( Field)进行原子操作,只能配合 volatile修饰的字段使用
public class AtomicReferenceFieldUpdaterTest {
public static void main(String[] args) {
Student student = new Student();
AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
new Thread(() -> {
System.out.println(updater.compareAndSet(student, null, "张三"));
}).start();
new Thread(() -> {
updater.set(student,"李四");
}).start();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(student);
}
}
class Student{
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
原子累加器
UnSafe
Unsafe对象提供了非常底层的,操作内存、线程的方法, Unsafe对象不能直接调用,只能通过反射获得
JDK9 及以后:
JDK内部将不会继续使用 sun.misc.Unsafe 类,而是会克隆出一个新的 jdk.internal.misc.Unsafe 类来替代前者的功能。新的Unsafe类将完全不暴露给应用,并且会随时根据JDK的需要而演化其API。
内存操作
//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);
Java中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM所管控的 Java进程内存,并且它们遵循 JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于 JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于 Unsafe提供的操作堆外内存的 native方法。
使用堆外内存的原因
【1】对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC时减少回收停顿对于应用的影响。
【2】提升程序I/O操作的性能。通常在 I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
CAS相关
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
Unsafe提供的 CAS方法(如compareAndSwapXXX)底层实现即为CPU指令 cmpxchg。
public class UnsafeTest {
public static void main(String[] args) {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe o = (Unsafe)theUnsafe.get(null);
System.out.println(o);
//id 的域相对对象的偏移量
long id = o.objectFieldOffset(Teacher.class.getDeclaredField("id"));
//获取name的偏移量
long name = o.objectFieldOffset(Teacher.class.getDeclaredField("name"));
Teacher teacher = new Teacher();
o.compareAndSwapInt(teacher,id,0,1);
o.compareAndSwapObject(teacher,name,null,"张三");
System.out.println(teacher);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
class Teacher {
volatile int id;
volatile String name;
@Override
public String toString() {
return "Teacher{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
线程调度
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过 park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。
内存屏障
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence()
用于定义内存屏障(是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。volatile 关键字的底层实现原理
Class相关
//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
//获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
public native boolean shouldBeInitialized(Class<?> c);
//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
对象操作
//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
public native long objectFieldOffset(Field f);
//获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
public native Object getObjectVolatile(Object o, long offset);
//存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
public native void putObjectVolatile(Object o, long offset, Object x);
//有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
public native void putOrderedObject(Object o, long offset, Object x);
//绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
数组相关
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass)
系统相关
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize()
MethodHandle
方法句柄、对可直接执行的方法的类型化引用,能够安全调用方法的对象。
MethodHandle
是抽象类,无法直接实例化,需通过MethodHandles.Lookup
的工厂方法(findxxx方法)来创类似于反射
方法句柄是对底层方法、构造函数、字段或类似低级操作的类型化、直接可执行的引用,具有参数或返回值的可选转换。这些转换非常普遍,包括转换、插入、删除和替换等模式;
MethodHandle 要比反射快很多因为访问检查在创建的时候就已经完成了,而不是像反射一样等到运行时候才检查.
MethodHandles可以操作方法,更改方法参数的类型和他们的顺序。而反射则没有这些功能。
反射更通用,但是安全性更差,因为可以在不授权的情况下使用反射对象。而method Handles遵从了分享者的能力。所以method handle是一种更低级的发现,适配和调用方法的方式,唯一的优点就是更快。所以反射更适合主流Java开发者,而method handle更适用于对编译和运行性能有要求的人,适合做底层开发
MethodHandle进行方法调用一般需要以下几步:
- 创建MethodType对象,指定方法的签名;
- 创建 MethodHandles.Lookup MethodHandle工厂类
- 在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;
- 传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。
MethodType
每个方法句柄都有一个MethodType
实例,用来指明方法的返回类型和参数类型。
static MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes)
第一个参数是返回类型,后面的剩余参数是方法的参数类型
可以通过调用MethodType的静态方法创建MethodType实例,有三种创建方式:
- methodType及其重载方法:需要指定返回值类型以及0到多个参数;
- genericMethodType:需要指定参数的个数,类型都为Object;
- fromMethodDescriptorString:通过方法描述来创建。
MethodHandles.Lookup
MethodHandle.Lookup相当于MethodHandle工厂类,用于创建方法和变量句柄的工厂,通过findxxx方法可以得到相应的MethodHandle,还可以配合反射API创建MethodHandle,对应的方法有unreflect、unreflectSpecial等。
MethodHandles.Lookup lookup = MethodHandles.lookup();
创建MethodHandle
只想的工厂方法查找类
MethodHandle findVirtual(Class<?> refc, String name, MethodType type)
查找方法名为name的方法MethodHandle findStatic(Class<?> refc, String name, MethodType type)
查找静态方法MethodHandle findSetter(Class<?> refc, String name, Class<?> type)
查找setter方法,name是该属性的名称,不是方法名称
对变量访问也添加了相应的工厂方法
- findVarHandle:用于创建对象中非静态字段的VarHandle。接收参数有三个,第一个为接收者的class对象,第二个是字段名称,第三个是字段类型。
- findStaticVarHandle:用于创建对象中静态字段的VarHandle。接收参数与findVarHandle一致。
- unreflectVarHandle:通过反射字段Field创建VarHandle。
invoke
得到MethodHandle后就可以进行方法调用了,有三种调用形式
- invokeExact:调用此方法与直接调用底层方法一样,需要做到参数类型精确匹配;
- invoke:参数类型松散匹配,通过asType自动适配;
- invokeWithArguments:直接通过方法参数来调用。其实现是先通过genericMethodType方法得到MethodType,再通过MethodHandle的asType转换后得到一个新的MethodHandle,最后通过新MethodHandle的invokeExact方法来完成调用。
invokeExact方法在调用时要求严格的类型匹配,方法的返回值类型也在考虑范围之内,要求更严格
获取String类的replace方法,并传参调用
//1 创建MethodType对象,指定方法的签名;
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
//2 创建 MethodHandles.Lookup
MethodHandles.Lookup lookup = MethodHandles.lookup();
//3 .Lookup中查找类型为MethodType的MethodHandle
MethodHandle mh = lookup.findVirtual(String.class, "replace", mt);
//4 传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。
String s = (String) mh.invokeExact("daddy",'d','n');
VarHandle
Varhandle与Unsafe 区别
Unsafe 所操作的并不属于Java标准,会容易带来一些安全性的问题。JDK9 之后,官方推荐使用 java.lang.invoke.Varhandle 来替代 Unsafe 大部分功能,对比 Unsafe ,Varhandle 有着相似的功能,但会更加安全,并且,在并发方面也提高了不少性能。
Varhandle
是对变量或参数定义的变量系列的动态强类型引用,包括静态字段,非静态字段,数组元素或堆外数据结构的组件。 在各种访问模式下都支持访问这些变量,包括简单的读/写访问,volatile 的读/写访问以及 CAS (compare-and-set)访问。简单来说 Variable
就是对这些变量进行绑定,通过 Varhandle
直接对这些变量进行操作。
VarHandle 的出现替代了 java.util.concurrent.atomic 和 sun.misc.Unsafe 的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的API。VarHandle 可以与任何字段、数组元素或静态变量关联,支持在不同访问模型下对这些类型变量的访问,包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap)等。
如果要原子性地增加某个字段的值, 我们可以使用下面三种方式:
- 使用AtomicInteger来达到这种效果,这种间接管理方式增加了空间开销,还会导致额外的并发问题;
- 使用原子性的FieldUpdaters,利用了反射机制,操作开销也会更大;
- 使用sun.misc.Unsafe提供的JVM内置函数API,虽然这种方式比较快,但它会损害安全性和可移植性。
访问私有属性
private static void privateDemo() throws NoSuchFieldException, IllegalAccessException {
Demo instance = new Demo();
VarHandle varHandle = MethodHandles.privateLookupIn(Demo.class, MethodHandles.lookup())
.findVarHandle(Demo.class, "privateVar", int.class);
varHandle.set(instance, 33);
System.out.println(instance);
}
访问public 成员
private static void publicDemo() throws NoSuchFieldException, IllegalAccessException {
Demo instance = new Demo();
VarHandle varHandle = MethodHandles.lookup()
.in(Demo.class)
.findVarHandle(Demo.class, "publicVar", int.class);
varHandle.set(instance, 11);
System.out.println(instance);
}
访问数组
private static void arrayDemo() throws NoSuchFieldException, IllegalAccessException {
Demo instance = new Demo();
VarHandle arrayVarHandle = MethodHandles.arrayElementVarHandle(int[].class);
arrayVarHandle.compareAndSet(instance.arrayData, 0, 1, 11);
arrayVarHandle.compareAndSet(instance.arrayData, 1, 2, 22);
arrayVarHandle.compareAndSet(instance.arrayData, 2, 3, 33);
System.out.println(instance);
}
VarHandle
中的每个方法都被称为access mode method,方法所接收的参数都是一个协调表达式,首个参数用来指示被访问变量的对象,后续参数表示当前访问模式的操作所需要的值。access mode将覆盖在变量声明时指定的任何内存排序效果
Varhandle 使用方式
VarHandle来使用plain、opaque、release/acquire和volatile四种共享内存的访问模式,根据这四种共享内存的访问模式又分为写入访问模式、读取访问模式、原子更新访问模式、数值更新访问模式、按位原子更新访问模式。
Varhandle 中对 getOpaque 、 setOpaque 线程环境下是可以保证内存可见性的,但不确保其他线程可见顺序。
Varhandle 中对 release/acquire 的多线程环境下是可以保证内存可见性的,是可以按顺序执行的。
不可变类
不可变对象
如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改!这样的对象在 Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类 DateTimeFormatter,String ,BigDecimal,BigInteger
这些类的对象的单个方法运行时是线程安全的,但无法保证多个方法的组合是线程安全的,所以设计多个方法的组合时仍然需要考虑线程安全问题
String类中不可变
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
// ...
}
final 的使用
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
- final 修饰引用类型只能保证引用不可变但里面的属性仍然可以修改
final 变量的赋值操作都必须在定义时或者构造器中进行初始化赋值,并发现 final 变量的赋值也会通过 putfield 指令来完成,同样在这条指令之后也会加入写屏障,保证在其它线程读到它的值时不会出现为 0 的情况
如果我们设置了final变量会将final变量修饰的值复制到栈内存中(修饰的值较大会放进当前类的常量池),没有共享;如果不加final修饰,会从另一个类中获取变量(从堆内存中获取),存在共享,性能较低
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 写屏障
10: return
使用final关键字的好处:
- final方法比非final快一些
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
- 使用final关键字,JVM会对方法、变量及类进行优化
保护性拷贝
使用拷贝出来的新的数组来保证被修改;
通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】
但频繁的拷贝创建对象,为了解决频繁创建,使用享元模式解决
享元模式
Flyweight pattern. 当需要重用数量有限的同一类对象时,对相同值的对象进行共享
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包装类提供了 valueOf 方法。
例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象,在这个范围之间会重用对象,大于这个范围,才会新建 Long 对象:
Byte, Short, Long 缓存的范围都是 -128~127
Character 缓存的范围是 0~127
Integer 的默认范围是 -128~127,最小值不能变,但最大值可以通过调整虚拟机参数 "-Djava.lang.Integer.IntegerCache.high "来改变
Boolean 缓存了 TRUE 和 FALSE
参考:设计模式总结中的享元模式
4 并发工具类
JDK9 之后,官方推荐使用 java.lang.invoke.Varhandle 来替代 Unsafe 大部分功能,对比 Unsafe ,Varhandle 有着相似的功能,但会更加安全规范,并且,在并发方面也提高了不少性能 jdk9 以后的并发工具类实现方法cas 的底层使用的都是varHandle 和MethodHandles 实现
以下章节没有特殊说明均基于JDK17
线程池
Fork/Join
ThreadLocal
AQS
常用辅助类
countDownLatch 倒计时计数器
倒计时锁:用来进行线程同步协作,等待所有线程完成倒计时
其中构造参数用来初始化等待计数值, await用来等待计数归零, countDown用来让计数减 一
countDownLatch.countDown(); // 数量-1 注意: 计数器一旦变为0,就不能再重置了。
countDownLatch.await(); // 等待计数器归零,然后再向下执行,调用aqs 的acquireSharedInterruptibly说明可以被中断
每次有线程调用 countDown() 数量-1,假设计数器变为0,countDownLatch.await() 就会被唤醒,继续执行!
原理
CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown方法时,其实使用了 tryReleaseShared 方法以CAS 的操作来减少 state ,直至 state 为 0 就代表所有的线程都调用了countDown方法。当调用 await 方法的时候,如果 state 不为0, 那么就把已经调用过 countDown 的线程都放入阻塞队列 Park ,并自旋 CAS 判断 state == 0,直至最后一个线程调用了 countDown ,使得 state == 0,释放完锁唤醒阻塞的await线程
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
//必须要执行任务的时候再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "Go out!");
countDownLatch.countDown();//减一
},String.valueOf(i)).start();
}
countDownLatch.await();//等待计数器归0,再向下执行--让多个线程执行完在执行下面代码
System.out.println("close door");
}
}
倒计时计数器可以配合线程池一起使用
public class CountDownLatchByPool {
public static void main(String[] args) {
downLatch();
}
private static void downLatch(){
AtomicInteger num = new AtomicInteger(0);
ExecutorService service = Executors.newFixedThreadPool(10, (r) -> new Thread(r, "t" + num.getAndIncrement()));
CountDownLatch latch = new CountDownLatch(10);
String[] all = new String[10];
Random r = new Random();
for (int j = 0; j < 10; j++) {
int x = j;
service.submit(() -> {
for (int i = 0; i <= 100; i++) {
try {
Thread.sleep(r.nextInt(100));
} catch (InterruptedException ignored) {
}
all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";
System.out.print("\r" + Arrays.toString(all));
}
latch.countDown();
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\n游戏开始...");
service.shutdown();
}
}
CyclicBarrier 加法计数器
Semaphore 信号量
Exchanger 线程数据交换器
Phaser 分层栅栏处理器
5 线程安全集合类
线程安全集合类可以分为三大类
- 遗留的线程安全集合如 Hashtable, Vector
- 使用 Collections装饰的线程安全集合,如:
- Collections. synchronizedCollection
- Collections. synchronizedList
- Collections. synchronizedMap
- Collections. synchronizedSet
- Collections. synchronizedNavigableMap
- Collections. synchronizedNavigableSet
- Collections synchronizedsortedMap
- Collections. synchronizedsortedset
- java.util. concurrent.*
Collections装饰的线程安全集合因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改操作时,锁住了整个集合,从而使得其表现的效率低下
java.uti1. concurrent,*下的线程安全集合类,可以发现它们有规律,里面包含三类关键词
Blocking、 CopyOnWrite、 Concurrent
- Blacking大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite之类容器修改时拷贝的方式避免并发安全,开销相对较重 ,适合读多写少的场景
- Concurrent类型的容器
- 内部很多操作使用cas优化,一般可以提供较高吞吐量
- 弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
历,这时内容是旧的 - 求大小弱一致性,size操作未必是100%准确
- 读取弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
遍历时如果发生了修改,对于非安全容器来讲,使用fail-fast机制也就是让遍历立刻失败,抛出
ConcurrentModification Exception,不再继续遍历 ,使用fail-safe机制的安全集合则不会出现此异常
CopyOnWriteArraylist
底层实现采用了写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。只有写写才会互斥
public static void main(String[] args) {
//1 使用vector
// List<String> list = new ArrayList<>();
//2 Collections.synchronizedList(new ArrayList())
//CopyOnWriteArrayList
//List<String> list = new Vector<>();
List<String> list = new CopyOnWriteArrayList<>();
//CopyOnWriteArrayList写入时复制,多个线程调用的时候,list 读取的时候固定的,写入的时候避免覆盖,复制一份副本,写入完插入进去
for (int i = 1; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
public boolean add(E e) {
synchronized (lock) {
// 获取旧的数组
Object[] es = getArray();
int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
es = Arrays.copyOf(es, len + 1);
// 添加新元素
es[len] = e;
// 替换旧的数组
setArray(es);
return true;
}
}
读操作并未加锁 例如:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}
适合『读多写少』的应用场景
get 弱一致性
读取的线程拿到的仍然是旧数组的引用,获取到是旧数组的值,实际上该值可能会被其他线程给删除了
迭代器弱一致性
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
Thread.sleep(1);
while (iter.hasNext()) {
System.out.println(iter.next());
}
线程是在新的数组上进行删除,删除完还没来得及替换原数组,迭代器线程拿到的是旧数组引用进行遍历
数据库的 MVCC 都是弱一致性的表现,比如可重复读。并发高和一致性是矛盾的,需要权衡
CopyOnWriteSet
CopyOnWriteArraySet 是CopyOnWriteArraylist的马甲 ,内部使用CopyOnWriteArraylist 的引用al 使用al.addIfAbsent(e)方法不在集合里在添加进去来保证唯一性
public static void main(String[] args) {
//1 Collections.synchronizedSet(new hashSet())
Set<String> Set = new CopyOnWriteArraySet<>();
for (int i = 1; i < 30; i++) {
new Thread(()->{
Set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(Set);
},String.valueOf(i)).start();
}
}
ConcurrentHashMap
ConcurrentSkipListSet
ConcurrentSkipListSet就是一种跳表类型的数据结构,其平均增删改查的时间复杂度均为O(logn)
。
是ConcurrentSkipListMap的马甲;ConcurrentSkipListSet实现了NavigableSet
接口,以提供和排序相关的功能,维持元素的有序性,所以ConcurrentSkipListSet就是一种为并发环境设计的有序SET工具类。
ConcurrentSkipListSet提供了根据指定Key返回最接近项、按升序/降序返回所有键的视图等功能。唯一的区别是NavigableSet针对的仅仅是键值,NavigableMap针对键值对进行操作。
ConcurrentSkipListSet在插入元素的时候,用一个Boolean.TRUE
对象(相当于一个值为true的Boolean型对象)作为value,同时putIfAbsent
可以保证不会存在相同的Key 最终跳表中的所有Node结点的Key均不会相同,且值都是Boolean.True
ConcurrentSkipListMap
6 ReadWriteLock 读写锁
7 阻塞队列与并发队列
BlockingQueue
多线程并发处理,线程池用的较多 !
学会使用队列
添加、移除
四组API
方式 | 抛出异常 | 有返回值,不抛出异常 | 阻塞 等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer() | put() | offer(,) |
移除 | remove | poll() | take() | poll(,) |
检测队首元素 | element | peek() | - | - |
ArrayBlockIngQueue
是一个有边界的阻塞队列,它的内部实现是一个数组。它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变
- 此队列按 FIFO(先进先出)原则对元素进行排序,队列获取操作则是从队列头部开始获得元素, 内部的阻塞队列是通过可重入的互斥锁 ReentrantLock 和 Condition 条件队列实现的其锁没有实现分离不同的线程共用同一个锁,存在公平访问与非公平访问的区别,
- 其内部还保存着两个整型变量。分别标识着队列的头部和尾部,插入和删除不会产生或者销毁任何的对象实例
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();
}
ArrayBlockingQueue内部是通过一个Object数组items和一个ReentrantLock实现的。同时ReentrantLock在使用时也提供了公平和非公平两种。因为数组是有界的,所以在数组为空和数组已满两种情况下需要阻塞线程,所以使用了Condition来实现线程的阻塞。
public boolean offer(E e) {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E e) {
// assert lock.isHeldByCurrentThread();
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
}
在当前的Array的count等于数组的容量时,也就是数组满的时候返回false,如果没满,那么插入数据到Array中,最后解锁返回true。
put方法
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
在put的实现里用到了Condition—notFull,在put的时候,如果数组已经满了,那么添加元素是不成功的(offer的实现),但是此时如果希望能等待数组有空间添加元素,那么可以使用put,如果数据已满,那么在notFull上等待。如果有数组的元素移除的操作就会唤醒这个put,让元素能添加到数组中。同时在调用insert方法是会调用notEmpty.signal() 唤醒在notEmpty上等待的线程。最后解锁返回。
另外put 方法加锁是可中断的
take()方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
在take时,如果当前的数组内没有元素的话,那么线程等待在notEmpty上,线程阻塞直到有元素可取.直到insert是唤醒在notEmpty上等待的线程。
LinkedBlockingQueue
内部维持着一链表构成的缓冲队列其容量默认值为 Integer.MAX_VALUE,也可以自定义容量,建议指定容量大小,变成有界阻塞队列,只有当缓冲区队列达到默认最大缓存容量Integer.MAX_VALUE或者指定的缓存容量时才会阻塞队列是一个由链表实现的无界阻塞队列。不能添加null元素。其实现队列的锁是分离的来控制数据的同步,两把锁分别锁住队列的头和尾部,可以允许多个线程同时入队和出队。高并发可以提高整个队列的性能 。
入队列
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node(真正的后继节点)
* - this Node, meaning the successor is head.next(自己, 发生在出队的时候)
* - null, meaning there is no successor (this is the last node)(null, 表示没有后继节点, 是最后了)
*/
Node<E> next;
Node(E x) { item = x; }
}
初始化链表 last = head = new Node(null); Dummy 节点用来占位,item 为 null
当一个节点入队 last = last.next = node;
再来一个节点入队 last = last.next = node;
出对列
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
h = head;
first = h.next;
h.next = h;
head = first;
E x = first.item; //dummy只是占位节点,出队时会将其next的节点的item值即head的item值返回,然后置空此时该节点成为新的dummy节点
first.item = null;
return x;
加锁
LinkedBlockingQueue用了两把锁和 dummy 节点提高效率
-
用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
-
用两把锁,锁住队列的头部和尾部 同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
-
消费者与消费者线程仍然串行
-
生产者与生产者线程仍然串行
-
线程安全分析
- 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争
- 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
put 操作
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// count 用来维护元素计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 满了等待
while (count.get() == capacity) {
// 倒过来读就好: 等待 notFull
notFull.await();
}
// 有空位, 入队且计数加一
enqueue(node);
c = count.getAndIncrement();
// 除了自己 put 以外, 队列还有空位, 由put线程叫醒其他 put 线程,不是消费者线程来唤醒
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中有一个元素(这里主内存的count已经加1了), 叫醒 take 线程
if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
signalNotEmpty();
}
take操作
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
//取出的数量达到capacity时需要告诉生产者生产产品了
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull()
return x;
}
与ArrayBlockIngQueue比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
- 推荐使用LinkedBlockingQueue
LinkedBlockingDeque
LinkedBlockingDeque 基于双向链表的无界阻塞队列,支持在链表两头同时进行put和take操作,是一个双端队列,内部基于AQS 和ReentrantLock Condition 条件队列实现
LinkedBlockingDeque是一个带有长度的阻塞队列,初始化的时候可以指定队列长度(如果不指定就是Integer.MAX_VALUE),且指定长度之后不允许进行修改。
LinkedBlockingDeque
与LinkedBlockingQueue
的实现大体上类似,区别在于LinkedBlockingDeque
提供的操作更多。并且LinkedBlockingQueue
内置两个锁分别用于put和take操作,而LinkedBlockingDeque
只使用一个锁控制所有操作。因为队列能够同时在头尾进行put和take操作,所以使用两个锁也需要将两个锁同时加锁才能保证操作的同步性,不如只使用一个锁的性能好。
同步节点相比LinkedBlockingQueue
多了一个prev
字段。
内部结构
插入线程在执行完操作后如果队列未满会唤醒其他等待插入的线程,同时队列非空还会唤醒等待获取元素的线程take线程同理
PriorityBlockingQueue
一个支持优先级排序的基于对象数组的无界阻塞队列,其内部控制线程的同步锁采用的是公平锁
内部使用可重入的互斥锁 ReentrantLock 和 Condition 条件队列实现的其锁没有实现分离不同的线程共用同一个锁
PriorityBlockingQueue也是基于最小二叉堆实现
allocationspinLock是个自旋锁,其使用 CAS操作来保证同 时只有一个线程可以扩容队列,状态为 0或者 1,其中 0表示当前没有进行扩容, 1表示 当前正在扩容。
由于这是一个优先级队列,所以有一个比较器 comparator用来比较元素大小。 lock独 占锁对象用来控制同时只能有一个线程 可以进行 入队、出队操作。 notEmpty 条件变量用 来实现 take 方法阻塞模式。这里没有 notFull 条件变量是因为这里的 put 操作是非阻塞的,
默认队列容量为 11,默认比较器为 null,也就是使用元素的 compareTo方法进行比较来确定元素的优先级, 这意味着队列元素必须实现了 Comparable 接口。
注意:不管PriorityBlockingQueue还是PriorityQueue 均是以顺序存储二叉树的特点存储在数组中,意味着对数组进行打印不是完全的升序,但总体上小元素在前大元素在后
heapify 堆化
//堆化
private void heapify() {
final Object[] es = queue;
int n = size, i = (n >>> 1) - 1;
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
for (; i >= 0; i--)
siftDownComparable(i, (E) es[i], es, n);
else
for (; i >= 0; i--)
siftDownUsingComparator(i, (E) es[i], es, n, cmp);
}
//调整小顶堆:满足当前节点值小于或等于其左右孩子的值,这个是从上向下维护
//这里的k就是当前空缺的位置,x就是覆盖元素一般用队尾元素覆盖
private static <T> void siftDownComparable(int k, T x, Object[] es, int n) {
// assert n > 0;
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // 最后一层叶子最多 占 1 / 2
while (k < half) {//顺序存储二叉树处理一半即可
int child = (k << 1) + 1; // 根据顺序存储二叉树特点,左孩子在数组中的索引为(2*n+1),右孩子为(2*n+2)
//父节点为 (n-1)/2
Object c = es[child];
int right = child + 1;//右孩子位置
if (right < n &&
((Comparable<? super T>) c).compareTo((T) es[right]) > 0)//左孩子大
c = es[child = right];//如果右孩子小,更新child = right
if (key.compareTo((T) c) <= 0)//当前节点和左右孩子小的那个比较 若小则满足了小顶堆退出
break;
es[k] = c;//设置k位置(空缺的位置)的值为c也就是将小的数向上移,k向下更新
k = child;//更新i为交换后的孩子的小的索引,因为后序可能会破坏结构还需要处理k的子节点
}
es[k] = key;//退出循环时,一定找到了x覆盖的位置,覆盖即可
}
siftDownComparable
我们现在要取出 第一个元素9,然后我们取出最后一个数23,拿他和11和65中较小的数11比较,发现23比11大,将11放置在数组的第一个位置,即树的根节点,然后拿23跟17,45中较小的数字17比较,23>17,17 上移到11位置,然后拿23和53比较,选择小的23,key=23=23 直接break循环,所以23占据原来17的位置。
offer()方法
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
//如果当前元素个数〉=队列容量,则扩容
while ((n = size) >= (cap = (array = queue).length))
//扩容操作
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
//将e 插入位置 n,通过将 n 提升到树上,使其大于或等于其父节点,或是根部,从而保持堆不变
siftUpComparable(n, e, array);
else
//自定义比较器的
siftUpUsingComparator(n, e, array, cmp);
//将队列元素数增加 1, 并且激活 notEmpty的条件队列里面的 一 个阻塞线程
size = n + 1;
notEmpty.signal();
} finally {
//释放独占锁
lock.unlock();
}
return true;
}
//维护小顶堆确保父节点值比孩子节点小这个是从下向上维护
private static <T> void siftUpComparable(int k, T x, Object[] es) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;//父节点的数组索引
Object e = es[parent];//当前父节点值
if (key.compareTo((T) e) >= 0)//插入的位置值比父节点值大直接退出放入
break;
es[k] = e; // 如果小则先将父元素移下来,然后向上更新父节点值为最小的值key
k = parent;//把K的位置设置成父节点的下标值
}
es[k] = key;//定位出了合适的的位置,更新k位置的值为当前最小的值key
}
siftUpComparable
K的位置是新插入元素将要放置的位置,加入插入的是 25,那么此元素的优先级别小于它的父节点的优先级,直接把25放在K的位置。假如放置的是8,优先级别最高,所以 (k-1)>>>1 获取父节点,与父节点比较,发现优先级大于父节点,把父节点的值放入K, 把K的位置设置成父节点的下标值,递归查询父节点,定位出K的位置,把新元素放入此位置。此种二叉树算法大大降低了算法复杂度。
扩容方法
扩容 64 个元素之内, 在原来的容量上+2, 大于 64 , 每次扩容 0.5 倍.
使用 CAS控制只有一个 线程可 以进行扩容,并且在扩容前释放锁,让其他线程可以进行入队和出队操作 。
spinlock锁使用 CAS控制只有一个线程可以进行扩容, CAS失败的线程会调用 Thread.yield() 让出 CPU, 目的是让扩容线程扩容后优先调用 lock.lock 重新获取锁,但是这得不到保证。有可能 yield 的线程在扩容线程扩容完成前己经退出,
扩容线程扩容完毕后会重置自旋锁变量 allocationSpinLock 为 0,这里并没有使 用 UNSAFE 方法的 CAS 进行设置是因为同时只可能有一个线程获取到该锁 , 并且 allocationSpinLock 被修饰 为 了 volatile 的。
private void tryGrow(Object[] array, int oldCap) {
lock.unlock(); // 必须释放,然后重新获得锁
Object[] newArray = null;
if (allocationSpinLock == 0 &&
ALLOCATIONSPINLOCK.compareAndSet(this, 0, 1)) {
try {
// oldGap<64则扩容 ,执行oldcap+2,否则扩容50%
int growth = oldCap < 64 ? oldCap + 2 : oldCap >> 1;
int newCap = ArraysSupport.newLength(oldCap, 1, growth);
if (queue == array)
newArray = new Object[newCap];
} finally {
allocationSpinLock = 0;
}
}
if (newArray == null) // 如果另一个线程正在扩容则让出当前线程时间片
Thread.yield();
lock.lock();
if (newArray != null && queue == array) {
queue = newArray;
System.arraycopy(array, 0, newArray, 0, oldCap);//旧数组值拷贝到新数组中
}
}
take
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)//说明队列没有元素需要await() 等待非空
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
//队列头部元素出队列,堆顶元素出队列
private E dequeue() {
// assert lock.isHeldByCurrentThread();
final Object[] es;
final E result;
//顶堆元素出队列需要重新维护小顶堆
if ((result = (E) ((es = queue)[0])) != null) {
final int n;
final E x = (E) es[(n = --size)];
es[n] = null;
if (n > 0) {
final Comparator<? super E> cmp;
if ((cmp = comparator) == null)
//重新维护小顶堆:将队列尾部的元素填充到处队列的位置,但需要满足小顶堆所以需要调整
siftDownComparable(0, x, es, n);
else
siftDownUsingComparator(0, x, es, n, cmp);
}
}
return result;
}
小结
PriorityBlockingQueue 队列 在内部使用二叉树小顶堆维护元素优先级,使用数组作为元素存储的数据结构,这个数组是可扩容的 。当当 前元素个数>=最大容量时会通过 CAS 算法扩容,出队时始终保证出队的元素是堆树的根节点,而不是在队列里面停留时间最长的元素。使用元素的 compareTo 方法提供默认的元素优先级比较规则,用户可以自定义优先级 的比较规则。
PriorityBlockingQueue类似于 ArrayBlockingQueue,在内部使用一个独占锁来控制同时只有一个线程可以进行入队和出队操作。另外,前者只使用了一个 notEmpty 条件变量而没有使用 notFull,这是因为前者是无界队列,执行 put操作时永远不会处于 await 状态,所以也不需要被唤醒。而 take 方法是阻塞方法,并且是可被中断的 。 当需要存放有优先级的元素时该队列比较有用 。
DelayQueue
DelayQueue是一个支持延时获取元素的无界阻塞队列,队列中的元素必须实现 Delayed 接口,在创建元素时可以指定延迟时间,只有到达了延迟的时间之后,才能获取到该元素。实现了 Delayed 接口必须重写两个方法 ,getDelay(TimeUnit) 和 compareTo(Delayed)。此队列不允许使用 null 元素
其内部使用:
- 可重入锁
- 用于根据delay时间排序的优先级队列
- 用于优化阻塞通知的线程元素leader
- 用于实现阻塞和通知的Condition对象
delayQueue其实就是在每次往优先级队列中添加元素,然后以元素的delay/过期值作为排序的因素,以此来达到先过期的元素会拍在队首,每次从队列里取出来都是最先要过期的元素
offer()方法
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();//执行加锁操作
try {
q.offer(e);//元素添加到优先级队列中
if (q.peek() == e) {//元素是否为队首
leader = null;//设置leader为空,唤醒所有等待的队列
available.signal();
}
return true;
} finally {
lock.unlock();//释放锁
}
}
take()方法
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//加锁操作-可中断
try {
for (;;) {
E first = q.peek();//取出优先级队列元素q的队首
if (first == null)//如果元素q的队首为空,阻塞请求
available.await();
else {
如果元素q的队首(first)不为空,获得这个元素的delay时间值
long delay = first.getDelay(NANOSECONDS);
//如果first的延迟delay时间值为0的话,说明该元素已经到了可以使用的时间,调用poll方法弹出该元素
if (delay <= 0)
return q.poll();
/*
线程A进来获取first,然后进入 else 的else ,设置了leader为当前线程A
线程B进来获取first,进入else的阻塞操作,然后无限期等待
这时在JDK 1.7下面他是持有first引用的
如果线程A阻塞完毕,获取对象成功,出队,这个对象理应被GC回收,但是他还被线程B持有着,GC链可达,所以不能回收这个first.
假设还有线程C 、D、E.. 持有对象1引用,那么无限期的不能回收该对象1引用了,那么就会造成内存泄露.
*/
first = null; // 释放元素first的引用,避免内存泄露
if (leader != null)//判断leader元素是否为空,不为空的话阻塞当前线程
available.await();
else {
//如果leader元素为空的话,把当前线程赋值给leader元素,然后阻塞delay的时间,即等待队首到达可以出队的时间,在finally块中释放leader元素的引用
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 等待delay时间后自动醒过来
// 醒过来后把leader置空并重新进入循环判断堆顶元素是否到期
// 这里即使醒过来后也不一定能获取到元素
// 因为有可能其它线程先一步获取了锁并弹出了堆顶元素
// 条件锁的唤醒分成两步,先从Condition的队列里出队
// 再入队到AQS的队列中,当其它线程调用LockSupport.unpark(t)的时候才会真正唤醒
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//如果leader为空并且优先级队列不为空的情况下(判断还有没有其他后续节点),调用signal通知其他的线程
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
DelayQueue中的Thread类型的元素leader,leader来减少不必要的等待时间:
-
如果有多个消费者线程用take方法去取,内部先加锁,然后每个线程都去peek第一个节点.如果leader不为空说明已经有线程在取了,设置当前线程等待
-
为空说明没有其他线程去取这个节点,设置leader并等待delay延时到期,直到poll后结束循环
SynchronousQueue
同步队列
没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!
SynchronousQueue的内部实现了两个类,一个是TransferStack类,使用LIFO(后进先出)顺序存储元素,这个类用于非公平模式;还有一个类是TransferQueue,使用FIFI顺序存储元素,这个类用于公平模式
FIFO通常用于在竞争下支持更高的吞吐量,而LIFO在一般的应用中保证更高的线程局部性。
都来自父类为Transferer
。它只定义了一个通用方法。
abstract static class Transferer<E> {
/**
* 执行put或者take操作/
* 如果参数e非空,这个元素将被交给一个消费线程;如果为null,
* 则请求返回一个被生产者提交的元素。
* 如果返回的结果非空,那么元素被提交了或被接受了;如果为null,
* 这个操作可能因为超时或者中断失败了。调用者可以通过检查
* Thread.interrupted来区分到底是因为什么元素失败。
*/
abstract E transfer(E e, boolean timed, long nanos);
}
非公平模式
TransferStack类,使用LIFO(后进先出)顺序存储元素
static final class TransferStack<E> extends Transferer<E> {
/* Modes for SNodes, ORed together in node fields */
/** 表示一个未满足的消费者 */
static final int REQUEST = 0;
/** 表示一个未满足的生产者 */
static final int DATA = 1;
/** Node is fulfilling another unfulfilled DATA or REQUEST */
static final int FULFILLING = 2;
static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }
/** Node class for TransferStacks. */
static final class SNode {
volatile SNode next; // 栈中的下一个结点
volatile SNode match; // 匹配此结点的结点
volatile Thread waiter; // 控制 park/unpark
Object item; // 数据
int mode;
put 的时候,就往栈中放数据。take 的时候,就从栈中取数据,两者操作都是在栈顶上操作数据.
入栈
使用 put 等方法,将数据放到栈中
出栈
使用 take 等方法,把数据从栈中拿出来
操作的对象都是栈顶,底层实现的方法也是同一个
E transfer(E e, boolean timed, long nanos) {
SNode s = null; // constructed/reused as needed
// 根据所传元素判断为生产or消费
int mode = (e == null) ? REQUEST : DATA;
for (;;) { // 无限循环
SNode h = head; // 获取头结点
if (h == null || h.mode == mode) { // 头结点为空或者当前节点状态和头结点相同
if (timed && nanos <= 0) { // 设置有时间
// 节点不为null并且为取消状态
if (h != null && h.isCancelled())
// 弹出取消的节点
casHead(h, h.next); // pop cancelled node
else
// 超时直接返回null
return null;
// 没有设置超时
} else if (casHead(h, s = snode(s, e, h, mode))) { // 将h设为自己的next节点
// deadline 死亡时间,如果设置了超时时间的话,死亡时间等于当前时间 + 超时时间,否则就是 0
long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int stat = -1; // -1: may yield, +1: park, else 0
SNode m; // await fulfill or cancel
// 空旋或者阻塞直到s结点被FulFill操作所匹配
while ((m = s.match) == null) {
// 当前线程有无被打断,如果过了超时时间,当前线程就会被打断
if ((timed &&(nanos = deadline - System.nanoTime()) <= 0) || w.isInterrupted()){
if (s.tryCancel()) {
clean(s); // 节点被取消了
return null;
}
//匹配上退出循环
} else if ((m = s.match) != null) {
break; // recheck
} else if (stat <= 0) {
if (stat < 0 && h == null && head == s) {
stat = 0; // yield once if was empty
Thread.yield();
} else {
stat = 1;
s.waiter = w;
}
} else if (!timed) {
//设置当前对象为阻塞资源
LockSupport.setCurrentBlocker(this);
try {
ForkJoinPool.managedBlock(s);
} catch (InterruptedException cannotHappen) { }
LockSupport.setCurrentBlocker(null);
} else if (nanos > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanos);
}
//当一个线程将要阻塞时设置其waiter字段,然后在真正park之前至少再检查一次状态,从而涵盖了竞争与实现者的关系,若此时匹配到了并且waiter非空,应将其唤醒。
if (stat == 1)
s.forgetWaiter();
Object result = (mode == REQUEST) ? m.item : s.item;
// 找到匹配的线程了
// h == head 可能已经已经被匹配
// h.next 等于s 不同类型
if (h != null && h.next == s)
// 弹出h 和 s
casHead(h, s.next); // help fulfiller
return (E) result;
}
// 未匹配
} else if (!isFulfilling(h.mode)) { // try to fulfill // 尝试匹配节点(sheng)
if (h.isCancelled()) // already cancelled // 节点被取消
casHead(h, h.next); // pop and retry // 修改头结点
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
// 没有下一个节点了,结束这次循环,走最外层循环重新开始
if (m == null) { // all waiters are gone // m等于null
casHead(s, null); // pop fulfill node // cas 设置head
s = null; // use new node next time
break; // restart main loop // 结束循环
}
SNode mn = m.next;
if (m.tryMatch(s)) { // 尝试匹配,成功
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match // 失败,说明m的线程匹配了,或者取消了
s.casNext(m, mn); // help unlink // 修改next节点
}
}
} else { // help a fulfiller 正在匹配
SNode m = h.next; // m is h's match
if (m == null) // waiter is gone 匹配完成了
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}
- 判断是 put 方法还是 take 方法
- 判断栈头数据是否为空,如果为空或者栈头的操作和本次操作一致,
- 判断操作有无设置超时时间,如果设置了超时时间并且已经超时,返回 null
- 如果栈头为空,把当前操作设置成栈头,或者栈头不为空,但栈头的操作和本次操作相同,也把当前操作设置成栈头,并看看其它线程能否满足自己,不能满足则阻塞自己。比如当前操作是 take,但队列中没有数据,则阻塞自己
- 如果栈头已经是阻塞住的,需要别人唤醒的,判断当前操作能否唤醒栈头
- 把自己当作一个节点,赋值到栈头的 match 属性上,并唤醒栈头节点
- 栈头被唤醒后,拿到 match 属性,就是把自己唤醒的节点的信息,返回
公平模式
公平模式下,底层实现使用的是TransferQueue这个内部队列使用FIFI顺序存储元素,它有一个head和tail指针,用于指向当前正在等待匹配的线程节点。
volatile QNode next
当前元素的下一个元素
volatile Object item // CAS’ed to or from null
当前元素的值,如果当前元素被阻塞住了,等其他线程来唤醒自己时,其他线程会把自己 set 到 item 里面
volatile Thread waiter // to control park/unpark
阻塞线程
final boolean isData
true 是 put,false 是 take
static final class TransferQueue<E> extends Transferer<E> {
/*
* This extends Scherer-Scott dual queue algorithm, differing,
* among other ways, by using modes within nodes rather than
* marked pointers. The algorithm is a little simpler than
* that for stacks because fulfillers do not need explicit
* nodes, and matching is done by CAS'ing QNode.item field
* from non-null to null (for put) or vice versa (for take).
*/
/** Node class for TransferQueue. */
static final class QNode {
volatile QNode next; // next node in queue
volatile Object item; // CAS'ed to or from null
volatile Thread waiter; // to control park/unpark
final boolean isData;
QNode(Object item, boolean isData) {
this.item = item;
this.isData = isData;
}
TransferQueue 内部类的 transfer 方法
E transfer(E e, boolean timed, long nanos) {
/*
* 这个基本方法, 主要分为两种情况
*
* 1. 若队列为空 / 队列中的尾节点和自己的 类型相同, 则添加 node
* 到队列中, 直到 timeout/interrupt/其他线程和这个线程匹配
* timeout/interrupt awaitFulfill方法返回的是 node 本身
* 匹配成功的话, 要么返回 null (producer返回的), 或正真的传递值 (consumer 返回的)
*
* 2. 队列不为空, 且队列的 head.next 节点是当前节点匹配的节点,
* 进行数据的传递匹配, 并且通过 advanceHead 方法帮助 先前 block 的节点 dequeue
*/
QNode s = null; // constructed/reused as needed
boolean isData = (e != null);
for (;;) { // 队列首尾的临时变量,队列空时,t=h
QNode t = tail, h = head, m, tn; // m is node to fulfill
if (t == null || h == null)
; // inconsistent
// 首尾节点相同,队列空
// 或队尾节点的操作和当前节点操作相同
else if (h == t || t.isData == isData) { // empty or same-mode
if (t != tail) // tail 被修改,重试
;
// 队尾后面的值还不为空,说明其他线程添加了 tail.next,t 还不是队尾,直接把 tn 赋值给 t
else if ((tn = t.next) != null) // lagging tail
advanceTail(t, tn);
else if (timed && nanos <= 0L) // can't wait
return null;
else if (t.casNext(null, (s != null) ? s :
(s = new QNode(e, isData)))) {
advanceTail(t, s); // 推进 tail 节点并等待
//超时时间,time为false时,deadline为0
long deadline = timed ? System.nanoTime() + nanos : 0L;
Thread w = Thread.currentThread();
int stat = -1; // same idea as TransferStack 和TransferStack一样的思想
Object item;
while ((item = s.item) == e) {
if ((timed &&
(nanos = deadline - System.nanoTime()) <= 0) ||
w.isInterrupted()) {
if (s.tryCancel(e)) {
clean(t, s);
return null;
}
} else if ((item = s.item) != e) {
break; // recheck
} else if (stat <= 0) {
if (t.next == s) {
if (stat < 0 && t.isFulfilled()) {
stat = 0; // yield once if first
Thread.yield();
}
else {
stat = 1;
s.waiter = w;
}
}
} else if (!timed) {
LockSupport.setCurrentBlocker(this);
try {
ForkJoinPool.managedBlock(s);
} catch (InterruptedException cannotHappen) { }
LockSupport.setCurrentBlocker(null);
}
else if (nanos > SPIN_FOR_TIMEOUT_THRESHOLD)
LockSupport.parkNanos(this, nanos);
}
if (stat == 1)
s.forgetWaiter();
if (!s.isOffList()) { // not already unlinked
advanceHead(t, s); // unlink if head
if (item != null) // and forget fields
s.item = s;
}
return (item != null) ? (E)item : e;
}
//队列不为空, 且队列的 head.next 节点是当前节点匹配的节点
} else if ((m = h.next) != null && t == tail && h == head) {
Thread waiter;
Object x = m.item;
boolean fulfilled = ((isData == (x == null)) &&
x != m && m.casItem(x, e));
//推进head 节点, 下次就调用 h.next 节点进行匹配(这里调用的是 advanceHead, 因为代码能执行到这边说明h已经是 head.next 节点了)
advanceHead(h, m);
if (fulfilled) {
if ((waiter = m.waiter) != null)
LockSupport.unpark(waiter);
// 队列不为空,并且当前操作和队尾不一致
// 也就是说当前操作是队尾是对应的操作
// 比如说队尾是因为 take 被阻塞的,那么当前操作必然是 put
return (x != null) ? (E)x : e;
}
}
}
}
线程被阻塞住后,当前线程是如何把自己的数据传给阻塞线程的。
假设线程 1 从队列中 take 数据 ,被阻塞,变成阻塞线程 A 然后线程 2 开始往队列中 put 数据 B,大致的流程如下:
-
线程 1 从队列 take 数据,发现队列内无数据,于是被阻塞,成为 A
-
线程 2 往队尾 put 数据,会从队尾往前找到第一个被阻塞的节点,假设此时能找到的就是节点 A,然后线程 B 把将 put 的数据放到节点 A 的 item 属性里面,并唤醒线程 1
-
线程 1 被唤醒后,就能从 A.item 里面拿到线程 2 put 的数据了,线程 1 成功返回。
在这个过程中,公平主要体现在,每次 put 数据的时候,都 put 到队尾上,而每次拿数据时,并不是直接从堆头拿数据,而是从队尾往前寻找第一个被阻塞的线程,这样就会按照顺序释放被阻塞的线程。
LinkedTransferQueue
ConcurrentLinkedQueue
基于链接节点的、线程安全的无界队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像:
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了cas来实现 ,并发度高
基于无锁算法实现的,所以当出现多个线程同时进行修改队列的操作(比如同时入队),很可能出现CAS修改失败的情况,那么失败的线程会进入下一次自旋,再尝试入队操作,直到成功。所以,在并发量适中的情况下,ConcurrentLinkedQueue一般具有较好的性能。
事实上, ConcurrentLinkedQueue应用还是非常广泛的
例如 Tomcat的 Connector结构时, Acceptor作为生产者向 Poller消费者传递事件信息时,正是采用
了 Concurrent LinkedQueue将 SocketChannel给 Poller使用
ConcurrentLinkedQueue内部就是一个简单的单链表结构,每入队一个元素就是插入一个Node类型的结点。字段head
指向队列头,tail
指向队列尾,通过Unsafe
来CAS操作字段值以及Node对象的字段值。
ConcurrentLinkedQueue特点:
1:不允许null入列
2:在入队的最后一个元素的next为null
3:队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
4:删除节点是将item设置为null, 队列迭代时跳过item为null节点
5:head节点跟tail不一定指向头节点或尾节点,可能存在滞后性
offer()
/**
* 在队尾入队元素e, 直到成功
*/
public boolean offer(E e) {
final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
for (Node<E> t = tail, p = t; ; ) { // 自旋, 直到插入结点成功
Node<E> q = p.next;
if (q == null) { // 正常情况下, 新结点直接插入到队尾
if (NEXT.compareAndSet(null, newNode)) {
// CAS竞争插入成功
if (p != t) // CAS竞争失败的线程会在下一次自旋中进入该逻辑
TAIL.weakCompareAndSet(t, newNode); // 重新设置队尾指针tail
//weakCompareAndSet只保存自身的可见性和有序性,性能较 CompareAndSet稍高,其不满足happen-bofore原则
//即不保证该操作前面和后面的其他读操作的有序性及可见性
return true;
}
// CAS竞争插入失败,则进入下一次自旋
} else if (p == q) // 发生了出队操作
p = (t != (t = tail)) ? t : head;
else
// 将p重新指向队尾结点
p = (p != t && t != (t = tail)) ? t : q;
}
}
在并发环境,ConcurrentLinkedQueue入列线程安全考虑具体可分2类:
- 线程1线程2同时入列
线程1,线程2不管在offer哪个位置开始并发,他们最终的目的都是入列,也即都需要执行casNext方法, 我们只需要确保所有线程都有机会执行casNext方法,并且保证casNext方法是原子操作即可。casNext失败的线程,可以进入下一轮循环,人品好的话就可以入列,衰的话继续循环 - 线程1遍历,线程2入列
ConcurrentLinkedQueue 遍历是线程不安全的, 线程1遍历,线程2很有可能进行入列出列操作, 所以ConcurrentLinkedQueue 的size是变化。换句话说,要想安全遍历ConcurrentLinkedQueue 队列,必须额外加锁。
pool()
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
//入列的tail,那出列的就是head
E item = p.item;
//出列判断依据是节点的item=null
//item != null, 并且能将操作节点的item设置null, 表示出列成功
if (item != null && p.casItem(item, null)) {
if (p != h)
//一旦出列成功需要对head进行移动
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
//推动head节点移动
p = q;
}
}
}
ConcurrentLinkedDeque
基于链表的无界的同时支持FIFO、LIFO的非阻塞并发双端队列 ConcurrentLinkedDeque作为双端队列,可以当作“栈”来使用,并且高效地支持并发环境。
ConcurrentLinkedDeque和ConcurrentLinkedQueue一样,采用了无锁算法,底层基于自旋+CAS的方式实现。
可以说高并发下需要线程安全的栈就需要使用到它
它采用了与ConcurrentLinkedQueue一样的松弛阀值设计(松弛阀值都是1),即head、tail并不总是指向队列的第一个、最后一个节点,而是保持head/tail距离第一个/最后一个节点的距离不超过1个节点的距离,从而减少了更新head/tail指针的CAS次数。
ConcurrentLinkedDeque的内部和ConcurrentLinkedQueue类似,不过是一个双链表结构,每入队一个元素就是插入一个Node类型的结点。字段head
指向队列头,tail
指向队列尾,通过Unsafe来CAS操作字段值以及Node对象的字段值。
ConcurrentLinkedDeque包含两个特殊字段:PREV_TERMINATOR、NEXT_TERMINATOR。
这两个字段初始时都指向一个值为null的空结点,这两个字段在结点删除时使用
入队
双端队列与普通队列的入队区别是:双端队列既可以在“队尾”插入元素,也可以在“队首”插入元素。ConcurrentLinkedDeque的入队方法有很多:addFirst(e)
、addLast(e)
、offerFirst(e)
、offerLast(e)
:
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
public boolean offerFirst(E e) {
linkFirst(e);
return true;
}
public boolean offerLast(E e) {
linkLast(e);
return true;
}
linkFirst(e)
方法,而队尾“入队”是调用了 linkLast(e)方法。队首“入队”——linkFirst(e):
/**
* 在队首插入一个元素.
*/
private void linkFirst(E e) {
final Node<E> newNode = newNode(Objects.requireNonNull(e)); // 创建待插入的结点
restartFromHead:
for (; ; )
for (Node<E> h = head, p = h, q; ; ) {
//前驱不为null前驱的前驱也不为null(有线程刚刚从对头入队了一个节点,还没来得及修改head
if ((q = p.prev) != null && (q = (p = q).prev) != null)
//每次跳2个结点时检查头结点是否更新
// If p == q, 需要遵循使用头结点替换.
//队首没有被其他线程修改,p=h否则p=q( 取前驱的前驱)
p = (h != (h = head)) ? h : q;
else if (p.next == p) // p 自连接说明已经出队列重试 PREV_TERMINATOR
continue restartFromHead;
else {
// p is first node
NEXT.set(newNode, p); // 新结点的next指向队首结点p
if (PREV.compareAndSet(p, null, newNode)) {//队首结点的prev指针指向“新结点”
// Successful CAS is the linearization point
// for e to become an element of this deque,
// and for newNode to become "live".
if (p != h) // ConcurrentLinkedDeque其实是以每次跳2个结点的方式移动指针,这主要考虑到并发环境以这种hop跳的方式可以提升效率。
HEAD.weakCompareAndSet(this, h, newNode); //重置head头指针
return;
}
//执行到此处说明CAS操作失败,有其它线程也在队首插入元素 , 重新找前驱
}
}
}
从head节点往头寻找第一个节点p(不论item是不是null),找到之后将新节点链接到它的前驱,同时当head的松弛阈值超过1时更新head。linkFirst分别被offerFirst、addFirst、push方法直接或间接调用。
出队
ConcurrentLinkedDeque的出队一样分为队首、队尾两种情况:removeFirst()
、pollFirst()
、removeLast()
、pollLast()
。
其实内部都调用了对应的poll方法, 队尾的“出队”——pollLast方法:
public E pollLast() {
restart: for (;;) {
for (Node<E> last = last(), p = last;;) {
final E item;
if ((item = p.item) != null) {
// recheck for linearizability
if (last.next != null) continue restart;
if (ITEM.compareAndSet(p, item, null)) {
unlink(p); //方法断开p结点的链接
return item;
}
}
if (p == (p = p.prev)) continue restart;
if (p == null) {
if (last.next != null) continue restart;
return null;
}
}
}
}
last方法用于寻找队尾结点,即满足p.next == null && p.prev != p
的结点:
Node<E> last() {
restartFromTail:
for (; ; )
for (Node<E> t = tail, p = t, q; ; ) {
if ((q = p.next) != null &&
(q = (p = q).next) != null)
// Check for tail updates every other hop.
// If p == q, we are sure to follow tail instead.
p = (t != (t = tail)) ? t : q;
else if (p == t
// It is possible that p is NEXT_TERMINATOR,
// but if so, the CAS is guaranteed to fail.
|| TAIL.compareAndSet(this, t, p))
return p;
else
continue restartFromTail;
}
}
void unlink(Node<E> x) {
// assert x != null;
// assert x.item == null;
// assert x != PREV_TERMINATOR;
// assert x != NEXT_TERMINATOR;
final Node<E> prev = x.prev;
final Node<E> next = x.next;
if (prev == null) { //前驱为空,表示是第一个节点
unlinkFirst(x, next);
} else if (next == null) {//后继为空,表示是最后一个节点
unlinkLast(x, prev);
} else {
//内部节点出队
// This is the common case, since a series of polls at the same end will be "interior" removes, except perhaps for the first one, since end nodes cannot be unlinked.
// 这是常见的情况,因为在同一端的一系列poll都将被“内部”删除,除了第一个,因为末端节点不能被取消链接。
// At any time, all active nodes are mutually reachable by following a sequence of either next or prev pointers.
// 在任何时候,所有活动节点都可以通过循着next或prev指针相互访问。
// Our strategy is to find the unique active predecessor and successor of x. Try to fix up their links so that they point to each other, leaving x unreachable from active nodes. 我们的策略是找到x唯一的活动前驱和后继节点。并尝试修改它们的链接,使它们彼此指向对方,使x节点无法从活动节点被访问。
//If successful, and if x has no live predecessor/successor, we additionally try to gc-unlink, leaving active nodes unreachable from x, by rechecking that the status of predecessor and successor are unchanged and ensuring that x is not reachable from tail/head, before setting x's prev/next links to their logical approximate replacements, self/TERMINATOR.
//如果成功,并且如果x没有活动前驱/后继,我们进行gc-unlink,使x无法访问到活动节点,通过查看前驱和后继的状态没有被改变,确保x节点不能从head/tail节点被访问,
//之前设置x前驱/后继的链接到它们的逻辑近似替代,自身/终结者。
Node<E> activePred, activeSucc;
boolean isFirst, isLast;
int hops = 1;
// 从被删除节点往前找到第一个有效前驱节点
for (Node<E> p = prev; ; ++hops) {
if (p.item != null) { //找到有效节点
activePred = p;
isFirst = false;
break;
}
Node<E> q = p.prev;
if (q == null) { //已经到对头了
if (p.next == p) //发现自连接,直接返回
return;
activePred = p;
isFirst = true;
break;
}
else if (p == q)//发现自连接,直接返回
return;
else //更新循环指针
p = q;
}
// 从被删除节点往后找到第一个有效后继节点
for (Node<E> p = next; ; ++hops) {
if (p.item != null) {//找到有效节点
activeSucc = p;
isLast = false;
break;
}
Node<E> q = p.next;
if (q == null) {//已经到对尾了
if (p.prev == p) //发现自连接,直接返回
return;
activeSucc = p;
isLast = true;
break;
}//发现自连接,直接返回
else if (p == q)
return;
else
p = q; //更新循环指针
}
// TODO: better HOP heuristics
//如果已经积累了超过阈值的逻辑删除节点,或者是内部节点删除,我们需要进一步处理unlink/gc-unlink
if (hops < HOPS
// always squeeze out interior deleted nodes
&& (isFirst | isLast))
return;
// Squeeze out deleted nodes between activePred and activeSucc, including x.
// 移除有效前驱和后继节点之间的那些节点(都是逻辑删除的节点),包括x节点本身
// 就是使有效前驱好后继节点互连(unlink)
skipDeletedSuccessors(activePred);
skipDeletedPredecessors(activeSucc);
// Try to gc-unlink, if possible
//如果更新的开头或者结尾,那么就可以尝试进行gc-unlink
if ((isFirst | isLast) &&
//确保前驱和后继的状态没有被改变
// Recheck expected state of predecessor and successor
(activePred.next == activeSucc) &&
(activeSucc.prev == activePred) &&
(isFirst ? activePred.prev == null : activePred.item != null) &&
(isLast ? activeSucc.next == null : activeSucc.item != null)) {
//确保x节点不能从head/tail节点被访问,
updateHead(); // Ensure x is not reachable from head
updateTail(); // Ensure x is not reachable from tail
// Finally, actually gc-unlink
PREV.setRelease(x, isFirst ? prevTerminator() : x);//前向终结节点
NEXT.setRelease(x, isLast ? nextTerminator() : x);//后继终结节点
}
}
}
//跳过已经删除的前驱节点
//其实就是从x开始往(左)依次找到第一个有效节点,直至找到或者到达队列的对头,
//然后将该节点设置成x的前驱
private void skipDeletedPredecessors(Node<E> x) {
whileActive:
do {
Node<E> prev = x.prev;
// assert prev != null;
// assert x != NEXT_TERMINATOR;
// assert x != PREV_TERMINATOR;
Node<E> p = prev;
findActive:
for (;;) {
if (p.item != null) //找到有效前驱p
break findActive;
Node<E> q = p.prev;
if (q == null) { //p已经是第一个节点
if (p.next == p)//发现自连接,重新开始
continue whileActive;
break findActive;
}
else if (p == q) //发现自连接,重新开始
continue whileActive;
else
p = q; //循环指针指向前驱
// 如果找到的有效前驱本身就是x的直接前驱,不做处理直接返回,否则将x的前驱指向该新的前驱
if (prev == p || PREV.compareAndSet(x, prev, p))
return;
} while (x.item != null || x.next == null);//x是有效节点,或是队列最后一个节点
}
ConcurrentLinkedDeque相比ConcurrentLinkedQueue,功能更丰富,但是由于底层结构是双链表,且完全采用CAS+自旋的无锁算法保证线程安全性 ConcurrentLinkedDeque使用了自旋+CAS的非阻塞算法来保证线程并发访问时的数据一致性。由于队列本身是一种双链表结构,所以虽然算法看起来很简单,但其实需要考虑各种并发的情况,实现复杂度较高,并且ConcurrentLinkedDeque不具备实时的数据一致性,实际运用中,如果需要一种线程安全的栈结构,可以使用ConcurrentLinkedDeque。
相关文档
并发编程JUC 笔记 Typora 版