关闭

JDK源码——java.util.concurrent(八)

标签: juc源码分析jdk多线程
265人阅读 评论(0) 收藏 举报
分类:

测试代码:
https://github.com/kevindai007/springboot_houseSearch/tree/master/src/test/java/com/kevindai/juc

LinkedBlockingQueue

上章说到了ArrayBlockingQueue,现在来看看LinkedBlockingQueue.LinkedBlockingQueue也是一个阻塞的有界队列,其用法与ArrayBlockingQueue基本一致,只不过内部实现不一样,这里不再写demo咱们直接来看看起源码.首先咱们看看其关键属性和构造方法

//node节点,只有一个next,所以是单向链表
static class Node<E> {
        //存储节点元素
        E item;
        //指向下一节点
        Node<E> next;
        Node(E x) { item = x; }
}
//容量
private final int capacity;
//当前元素数量
private final AtomicInteger count = new AtomicInteger(0);
//头节点
private transient Node<E> head;
//尾节点
private transient Node<E> last;
//take, poll等取出方法的锁
private final ReentrantLock takeLock = new ReentrantLock();
//取出元素方法等待条件
private final Condition notEmpty = takeLock.newCondition();
//put, offer等添加方法的锁
private final ReentrantLock putLock = new ReentrantLock();
//添加元素方法等待条件
private final Condition notFull = putLock.newCondition();


public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        //头结点、尾节点都是Null
        last = head = new Node<E>(null);
    }

咱们先来看看其add()方法

    //add方法定义与于父类AbastactQueue中,实际上是调用实现类中的offer()方法
    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
    public boolean offer(E e) {
        //传入元素为空,直接报空指针异常
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        //如果元素数量等于最大容量,即队列满了
        if (count.get() == capacity)
            return false;
        int c = -1;
        //构造节点
        Node<E> node = new Node(e);
        //加锁
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            //如果未满,则入列,元素数量+1
            if (count.get() < capacity) {
                enqueue(node);
                c = count.getAndIncrement();
                if (c + 1 < capacity)
                    //唤醒在不为满(即put())条件等待的线程
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        //这里c默认是-1,经过enqueue入队,c=count.getAndIncrement(),如果变成0,说明链表队列原来是空的,现在有元素了
        if (c == 0)
            //唤醒在不为满(即take())条件等待的线程
            signalNotEmpty();
        return c >= 0;
    }
    private void enqueue(Node<E> node) {
        //当前节点加到链中,并置为尾节点
        last = last.next = node;
    }
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

与ArrayBlockingQueue一样,offer()方法并不阻塞.

再来看看put()和take()方法

    public void put(E e) throws InterruptedException {
        //元素为Null抛出异常
        if (e == null) throw new NullPointerException();
        int c = -1;
        //构造节点
        Node<E> node = new Node(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //可中断
        putLock.lockInterruptibly();
        try {
            //如果队列已满,则阻塞等待
            while (count.get() == capacity) {
                notFull.await();
            }
            //被唤醒或队列未满,则元素入列,更改元素数量
            enqueue(node);
            c = count.getAndIncrement();
            //队列未满,则唤醒所有put()上等待的队列
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            //唤醒在不为满(即take())条件等待的线程
            signalNotEmpty();
    }

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)
                //如果还有元素,则唤醒在take()方法上等待的线程
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        //默认c为-1,经过take后c=count.getAndDecrement();说明原来队列是满的,take后不满,就可以唤醒notFull条件队列
        if (c == capacity)
            signalNotFull();
        return x;
    }
    private E dequeue() {
        //获取头节点
        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;
    }
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

再来看看remove()

//因为remove操作需要遍历整个链表,所以加2把锁遍历
public boolean remove(Object o) {  
    if (o == null) return false;  
    fullyLock();  
    try {  
        for (Node<E> trail = head, p = trail.next;  
             p != null;  
             trail = p, p = p.next) {  
            if (o.equals(p.item)) {  
                unlink(p, trail);  
                return true;  
            }  
        }  
        return false;  
    } finally {  
        fullyUnlock();  
    }  
}  
void fullyLock() {  
    putLock.lock();  
    takeLock.lock();  
}  

void fullyUnlock() {  
    takeLock.unlock();  
    putLock.unlock();  
}  
void unlink(Node<E> p, Node<E> trail) {  
    p.item = null;  
    trail.next = p.next;  
    if (last == p)  
        last = trail;  
    if (count.getAndDecrement() == capacity)  
        notFull.signal();  
}  

这个类很简单,相信大家也是要看就懂,在此不做过多分析,开始学习下一个

ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个非阻塞的无界的线程安全队列(基于cas实现),下面看看其是如何实现的

private static class Node<E> {
        //存放元素
        volatile E item;
        //指向下一元素
        volatile Node<E> next;
        //构建node节点
        Node(E item) {
            UNSAFE.putObject(this, itemOffset, item);
        }
        //cas更新Item
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }

        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }
        //cas跟新next节点
        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }

        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;

        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
    //头节点
    private transient volatile Node<E> head;
    //尾节点
    private transient volatile Node<E> tail;
    public ConcurrentLinkedQueue() {
        head = tail = new Node<E>(null);
    }

可以看到ConcurrentBlockingQueue并没有容量等相关属性,因此是一个无界队列
下面咱们看看其入列方法

    public boolean add(E e) {
        return offer(e);
    }
    public boolean offer(E e) {
        checkNotNull(e);
        //构建节点
        final Node<E> newNode = new Node<E>(e);
        //t为tail节点,p为尾节点,默认相等,采用自旋+cas方式,直到入队成功
        for (Node<E> t = tail, p = t;;) {
            //获得p的下一个节点
            Node<E> q = p.next;
            // 如果下一个节点是null,即p节点就是尾节点
            if (q == null) {
                //将入队节点newNode设置为当前队列尾节点p的next节点
                if (p.casNext(null, newNode)) {
                    // 如果p.casNext有个线程成功了,p=newNode   
                    // 比较 t (tail) 是不是 最后一个节点  
                    if (p != t)
                        // 如果不等,就利用cas将,尾节点移到最后  
                        // 如果失败了,那么说明有其他线程已经把tail移动过,也是OK的
                        casTail(t, newNode); 
                    return true;
                }
            }
            else if (p == q)
                // 有可能刚好插入一个,然后P 就被删除了,那么 p==q  
                // 这时候在头结点需要从新定位。  
                p = (t != (t = tail)) ? t : head;
            else
                //p有next节点,表示p的next节点是尾节点,则需要重新更新p后将它指向next节点
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

从源代码我们看出入队过程中主要做了三件事情

  1. 定位出尾节点
  2. 使用CAS+自旋指令将入队节点设置成尾节点的next节点
  3. 重新定位tail节点

再看看出列方法

    public E poll() {
        // 设置起始点  
        restartFromHead:  
        for (;;) {  
            for (Node<E> h = head, p = h, q;;) {  
                E item = p.item;  
                // 利用cas 将第一个节点,设置未null  
                if (item != null && p.casItem(item, null)) {  
                    // 和上面类似,p的next被删了,  
                    // 然后然后判断一下,目的为了保证head的next不为空  
                    if (p != h) // hop two nodes at a time  
                        updateHead(h, ((q = p.next) != null) ? q : p);  
                    return item;  
                }  
                else if ((q = p.next) == null) {  
                    // 有可能已经被另外线程先删除了下一个节点  
                    // 那么需要先设定head 的位置,并返回null  
                    updateHead(h, p);  
                    return null;  
                }  
                else if (p == q)  
                    // 这个一般是删完了(有点模糊)  
                    continue restartFromHead;  
                else  
                    // 和offer 类似,这历使用保证下一个节点有值,才能删除  
                    p = q;  
            }  
        } 
    }

首先获取head节点的元素,并判断head节点元素是否为空,如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走;如果不为空,则使用CAS的方式将head节点的引用设置成null,如果CAS成功,则直接返回head节点的元素;如果CAS不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取head节点。如果p节点的下一个节点为null,则说明这个队列为空(此时队列没有元素,只有一个伪结点p),则更新head节点。

再来看看size()方法

public int size() {  
        int count = 0;  
        //这里会遍历所有节点,效率较低  
        for (Node<E> p = first(); p != null; p = succ(p))  
            if (p.item != null){ 
                if (++count == Integer.MAX_VALUE)  
                    break;  
            }  
        return count;  
    }  

size()方法会遍历所有节点,效率较低,因此如果想判断队列是否为空是可考虑使用empty()

public boolean isEmpty() {
        return first() == null;
    }
0
0
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源码分析

ThreadPoolExecutor是Executor执行框架最重要的一个实现类,提供了线程池管理和任务管理是两个最基本的能力。这篇通过分析ThreadPoolExecutor的源码来看看如何设计和实现一个基于生产者消费者模型的执行器。 生产者消费者模型 生产者消费者模型包含三个角色:生产者,...
  • ITer_ZC
  • ITer_ZC
  • 2015-07-16 16:27
  • 3165

有关阅读JDK源码的看法

源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心。  说到技术基础,我打个比方吧,如果你从来没有学过Java,或是任何一门编程语言如C++,一开始去啃《Core Java》,你是很难从中吸收到营养的,特别是《深入Java虚拟机》这类书,别人觉得好,未必适合现在的你。...
  • mijinghjb
  • mijinghjb
  • 2014-04-25 16:49
  • 3140

关于更改jdk源码替换应用的例子

第一个参数表示 -Xbootclasspath/p:path  让jvm优先于默认的bootstrap去加载path中指定的class 第二个表示:字节码更改的jar 切记eclipse一定要用jre而不是jdk,jre是运行环境,jvm会对jre进行先后顺序的匹配,而不是jdk,...
  • luman1991
  • luman1991
  • 2017-06-01 10:48
  • 247

JDK部分源码阅读与理解

本文为博主原创,允许转载,但请声明原文地址:http://www.coselding.cn/article/2016/05/31/JDK部分源码阅读与理解/    不喜欢重复造轮子,不喜欢贴各种东西、JDK代码什么的,让整篇文章很乱。。。JDK源码谁都有,没什么好贴...
  • u014394255
  • u014394255
  • 2016-12-03 23:41
  • 3450

如何查看JDK以及JAVA框架的源码

在初次使用java时,往往我们对最基本的java类会忽略对其内部基本的实现的了解,也往往不屑于了解其内部实现机制,以为它们本来就是这样子。而其实贯穿java的整个过程,所有上层的使用,都是源于对底层的扩展,所以要真正去了解这门语言,就必须得从其底层开始认真去了解它。而要深入了解,就需要更多去关注其内...
  • youzhouliu
  • youzhouliu
  • 2016-04-30 09:20
  • 2849

JDK源码分析-ArrayList分析

花了两个晚上的时间研究了一下ArrayList的源码, ArrayList 继承自AbstractList 并且实现了List, RandomAccess, Cloneable, Serializable 通过实现这三个接口 就具备了他们的功能 RandomAccess 用来表明其支持快速(...
  • tanggao1314
  • tanggao1314
  • 2016-03-26 23:34
  • 3338

jdk类库源码分析-String类

从今天开始研究jdk基础类库的源码,从中学习优秀的设计思想,提高自己的能力。至于怎么查看源码,这里不多说,网上很多教程,直接上干货儿,欢迎大家提出宝贵意见,共同学习,一起进步。(这里我研究的是jdk1.7) String源码分析 一 、String类 在java.lang包中,此类被final...
  • u011915230
  • u011915230
  • 2016-11-14 23:28
  • 786

JDK动态代理源码解析

分析版本jdk1.8 在分析jdk动态代理之前,先来了解java WeakReference弱引用的使用。运行期创建目标对象的代理非常耗时,使用缓存来存储生成的代理类显得尤为重要。jdk动态代理使用弱引用指向cache中的代理类,以便代理类对象能够被GC回收。 在java中,当一个对象O被创建...
  • sum__mer
  • sum__mer
  • 2016-11-15 23:34
  • 826

如何在Eclipse中查看JDK以及Java框架的源码

对于Java程序员来说,有时候是需要查看JDK或者一些Java框架的源码来分析问题的,而默认情况下,你按住Ctrl,再点击Java本身的类库(例如ArrayList)是无法查看源码的,那么如何在Eclipse中查看JDK以及Java框架的源码呢?下面,跟着我一起,一步步带你走进源码的世界。
  • qiumengchen12
  • qiumengchen12
  • 2015-04-03 22:18
  • 4245

JDK源码学习之String篇

前提:先了解下什么是声明,什么时候才算是产生了对象实例 其中x并未看到内存分配,变量在使用前必须先声明,再赋值,然后才可以使用。java基础数据类型会用对应的默认值进行初始化   一、首先看看Java虚拟机JVM的内存块及其变量、对象内存空间是怎么存储分配的 ...
  • u010942020
  • u010942020
  • 2016-07-12 22:29
  • 1116
    个人资料
    • 访问:34697次
    • 积分:921
    • 等级:
    • 排名:千里之外
    • 原创:59篇
    • 转载:1篇
    • 译文:0篇
    • 评论:0条
    文章分类
    最新评论