jdk源码阅读-ConcurrentLinkedQueue(一)
说明
concurrentLinkedQueue为无界非阻塞队列,是线程安全的 内部结构为链表的形式, 内部使用cas保存线程安全。采用cas保证原子性
什么是CAS
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作
AtomicInteger例子
AtomicInteger atomicInteger=new AtomicInteger();
//内部通过CAS保证原子性
atomicInteger.getAndIncrement();
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
/**
* var1为对象 var2 为对象内存中的属性值 var5 为期望值 var4为修改后的值 如果cas失败则继续重试
* 从内存中取出var1对象的var2的值为内存值
*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
注意:Unsafe是c++实现的 遗憾的是这个类是jdk专用 我们并不能使用
ConcurrentLinkedQueue类图

并没有实现BlockingQueue所以是并不是阻塞队列 同时也不适合使用线程池 并发情况下队列会无线扩张 会导致内存溢出 以及线程池的maximumPoolSize 参数无效
详情看:https://www.cnblogs.com/LQBlog/p/8735356.html#autoid-1-0-0
源码
核心属性
/**
* 头节点
*/
private transient volatile ConcurrentLinkedQueue.Node<E> head; /**
* 尾节点
*/
private transient volatile ConcurrentLinkedQueue.Node<E> tail; public ConcurrentLinkedQueue() {
head = tail = new ConcurrentLinkedQueue.Node<E>(null);
}
从尾部入队 从头部出队 都用volatile修饰 表示多线程修改 其中一个线程修改 会立即刷新到主内存 对其他线程立即可见
Node源码
private static class Node<E> {
//当前元素值
volatile E item;
//下一个节点元素
volatile ConcurrentLinkedQueue.Node<E> next;
/**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(ConcurrentLinkedQueue.Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(ConcurrentLinkedQueue.Node<E> cmp, ConcurrentLinkedQueue.Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
//偏移量 cas
private static final long itemOffset;
//便宜量
private static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentLinkedQueue.Node.class;
itemOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("item"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
item 和next都是volatile修饰
几个原则
1.next和head属性都不会为null 就算队列中一个元素也没有也会同时指向同一个空节点
2.head和tail刷新并不是实时的
3.head并不一定是真正的头节点 tail并不一定是尾节点
4.tail.next=null一定是尾节点
offer源码
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
//1.检查是否为空 空元素不允许入队
checkNotNull(e);
//2.创建node节点包装类
final ConcurrentLinkedQueue.Node<E> newNode = new ConcurrentLinkedQueue.Node<E>(e);
//3映射cas修改失败重新查找尾节点的遍历重试 以及多线程并发修改 和tail不是真正未接节点的 真正尾节点查找
for (ConcurrentLinkedQueue.Node<E> t = tail, p = t; ; ) {
ConcurrentLinkedQueue.Node<E> q = p.next;
/**
* 3.q等于null代表为最后一个节点
* 映射代码5的延迟修改tail
* 映射代码6的cas修改并不是一定成功也没采取重试
* 映射多线程有其他线程抢先入队
*/
if (q == null) {
/**
* 4.cas更新 底层使用Unsafe c++实现 直接调度cpu指令实现 cas更新 并发情况下只会有一个线程成功(可以理解为乐观锁)
*/
if (p.casNext(null, newNode)) {
/**
* 5.p!=t 代表应该更新tail节点为newNode
* 当第一次for循环q!=null 循环找到真正的尾节点的时候cas修改成功的时候才成立
* 单线程情况下每次都是间隔一次入队才来更新tail 减少cas修改tail
*/
if (p != t)
casTail(t, newNode); //6.cas设置tail为当前新节点 并没有采取重试 有可能失败 失败也没关系 代码3会遍历找到真正尾节点
return true;
}
} else if (p == q) {
/**
* 7.tail自引用时发生 item就是他自身 重新根据头节点找尾节节点
* 自引用即 tail item=next 这个时候head才是真正尾节点
* (t != (t = tail))判断tail是否改变 改变了则取最新tail否则从head开始遍历往下找 找真正节点
* 这个判断是防止多线程情况下 有其他线程抢先入队 也可以理解 其他线程已经抢先走了这个分支找到真正尾节点入队并修改了tail
* 自引用:并发情况下 又在入队又在出队的情况下会发生 我测试 添加第一个元素就是自引用 后续元素则不是
*/
p = (t != (t = tail)) ? t : head;
else
/**
* 8.当tail并不是最后一个节点时
* 对应代码5的延迟修改 以及cas修改失败 或者多线程抢先入队 导致当前线程tail并不是真正尾节点
* (p != t && t != (t = tail)为判断tail是否有其他线程抢先修改 如果有则取最新tail 遍历开始找真正尾节点 否则继续向下寻找
*
*/
p = (p != t && t != (t = tail)) ? t : q;
}
}
}
代码1:
检查代码元素是否为空 如果为空则抛出异常
代码2:
创建一个node对象包装值
代码3:
判断q==null(p=tail,q=p.next) 即判断是否为最后一个节点 这里判断主要出于以下几点考虑
1.代码5的判断 表示每次都是间隔一次才更新taill 所以tail并不是实时修改的
2.代码6的cas没有重试 也表示修改taill并不是一定成功的
代码4:
如果taill是最后一个节点 则cas更新 如果cas更新是吧(表示其他线程抢先更新了 重新执行for循环)
代码5:
看到这里可能会有疑问(p=t=tail) 为什么要判断p!=t 因为第一次不更新尾节点 所以第二次offer代码3并不会成立走下面else分支重新寻找真正的尾节点 即p不会等于t
代码6:
这里cas可能会修改失败 但是并没有关系 注意外面for循环 和判断 当不是尾节点 会重新寻找真正的尾节点
代码7:
p==q(p.next=p)网上资料说poll可能会导致自引用 我jdk8初始化队列 添加第一个元素就回显了自引用(没找到原因) 自引用的时候head才是真正的尾节点
p = (t != (t = tail)) ? t : head; 这代码 第一个判断是 避免多线程情况下 其他线程入队已经修改了tail的的值 所以这个时候就不是自引用 则取taill遍历找尾节点 否则 从head重新找起
代码8:
不直接取taill.next为p加入遍历 而是加入了taill是否改变的判断 也是为了防止其他线程修改了tail 如果修改了则取最新tail遍历 否则取p.next遍历查找尾节点
可以看到整个入队都是采用无锁设计 使用cas+循环 保证入队一定会成功,但是增加了for循环遍历次数
poll源码
public E poll() {
restartFromHead:
/**
* 代码1无限循环
*/
for (;;) {
for (ConcurrentLinkedQueue.Node<E> h = head, p = h, q;;) {
/**
* 代码2从head节点开始取
*/
E item = p.item;
/**
* 代码3 如果item不为空 表示未出队 同时通过cas将item置为null
* 如果cas失败则表示其他线程抢先出队 则重新获取
*/
if (item != null && p.casItem(item, null)) {
/**
*代码4 每次出队并不会马上修改head节点 而是下一次出队时再修改
*/
if (p != h) // hop two nodes at a time
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
/**
* 代码5 表示队列为空返回null
* 如果p.next!=null则继续执行下面逻辑 注意此时q已经被赋值为p.next
*/
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
/**
* 代码6
*/
else if (p == q)
continue restartFromHead;
/**
* 代码7
* 将next节点赋值给p 遍历重新找到head节点
* 对应代码3cas失败
* 对应代码4不是实时修改head节点
* 对应代码5的判断给q复制next节点
*/
else
p = q;
}
}
}
size方法
public int size() {
// 计数
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p)) // 从第一个存活的结点开始往后遍历
if (p.item != null) // 结点的item域不为null
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE) // 增加计数,若达到最大值,则跳出循环
break;
// 返回大小
return count;
}
jdk源码阅读-ConcurrentLinkedQueue(一)的更多相关文章
- JDK源码阅读(三):ArraryList源码解析
今天来看一下ArrayList的源码 目录 介绍 继承结构 属性 构造方法 add方法 remove方法 修改方法 获取元素 size()方法 isEmpty方法 clear方法 循环数组 1.介绍 ...
- JDK源码阅读(一):Object源码分析
最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...
- 利用IDEA搭建JDK源码阅读环境
利用IDEA搭建JDK源码阅读环境 首先新建一个java基础项目 基础目录 source 源码 test 测试源码和入口 准备JDK源码 下图框起来的路径就是jdk的储存位置 打开jdk目录,找到sr ...
- JDK源码阅读-FileOutputStream
本文转载自JDK源码阅读-FileOutputStream 导语 FileOutputStream用户打开文件并获取输出流. 打开文件 public FileOutputStream(File fil ...
- JDK源码阅读-FileInputStream
本文转载自JDK源码阅读-FileInputStream 导语 FileIntputStream用于打开一个文件并获取输入流. 打开文件 我们来看看FileIntputStream打开文件时,做了什么 ...
- JDK源码阅读-ByteBuffer
本文转载自JDK源码阅读-ByteBuffer 导语 Buffer是Java NIO中对于缓冲区的封装.在Java BIO中,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接.但是在J ...
- JDK源码阅读-RandomAccessFile
本文转载自JDK源码阅读-RandomAccessFile 导语 FileInputStream只能用于读取文件,FileOutputStream只能用于写入文件,而对于同时读取文件,并且需要随意移动 ...
- JDK源码阅读-FileDescriptor
本文转载自JDK源码阅读-FileDescriptor 导语 操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数.Java虽然在设计上使用了抽象程度更高的流来作为文 ...
- JDK源码阅读-Reference
本文转载自JDK源码阅读-Reference 导语 Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象. 如果 ...
随机推荐
- 【记录】Swagger2 注解说明
Swagger是一个用来管理项目接口的非常好用的第三方插件, 程序员只需要通过在接口代码上设置Swagger注解, 就可以在Swagger UI上进行查看与验证接口. 很大程度上节省了,接口文档的制作 ...
- 聚合函数 -AVG/MAX/MIN/STDDEV/VARIANCE/SUM/COUNT/MEDIAN
------------------------------------------聚合函数--------------------------------------------- --1: AVG ...
- ANdroid手机屏幕反横向等参数设定
经过我一番百度和看Android文档,我才发现,Android对旋转屏,特别是只有横屏或者竖屏虽重力旋转的支持是到Android4.3.1才有完美支持的 unspecified - 默认值,由系统选择 ...
- linux挂载群辉的NFS共享文件夹
mount -t nfs 192.168.137.136:/volume1/NFSfile /NFSfile -o proto=tcp -o nolock df -h #查看挂载点
- ubuntu docker 安装 oracle
1.ubuntu 安装docker sudo apt-get update sudo apt-get docker.io 2.docker下载oracle镜像 sudo docker pull wna ...
- loj2471[九省联考2018]一双木棋
题意:在一个n*m的棋盘上,A和B轮流放置棋子.一个位置能够放置棋子当且仅当它上面没有棋子并且它的上面和左边一格都已经放了棋子(不难发现是一个上三角阶梯状).每个格子有两个权值,当A在上面放置棋子时A ...
- Python 直接赋值、浅拷贝和深度拷贝区别
Python 直接赋值.浅拷贝和深度拷贝区别 转自https://www.runoob.com/w3cnote/python-understanding-dict-copy-shallow-or-de ...
- ajax 实战使用
注意ajax 必须放在script脚本中使用 ajax用于前端朝后端提交数据,并且后端函数处理好结果返回给success函数作为回调函数给前端,前端拿到后端传来的值,比如code==0 来做相应的前端 ...
- Vue源码思维导图------------Vue选项的合并之$options
本节将看下初始化中的$options: Vue.prototype._init = function (options?: Object) { const vm: Component = this / ...
- NOIp2018集训test-10-19 (bike day5)
Bike老爷问了好几天到底要怎样简单的题目你们才能AK啊终于在他每天降难度直到要走了才出了一套我们能AK的题.虽然前几天的题换成llj肯定随便AK. 其实最近有点方虽然通常最后都写完了把该拿的分拿了该 ...