并发编程从入门到放弃系列开始和结束(九)

发布于 2022-6-13 17:40
浏览
0收藏

 

public void put(E e) {
  offer(e); // never need to block
}

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)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

 

这里,我们要继续关注一下这个扩容的逻辑,到底是怎么处理的?代码不长,但是看着很方的样子。

  1. 首先,先释放锁,因为下面用 CAS 处理,估计怕扩容时间太长阻塞的线程太多
  2. 然后 CAS 修改 allocationSpinLock 为1
  3. CAS 成功的话,进行扩容的逻辑,如果长度小于64就扩容一倍,否则扩容一半
  4. 之前我们说他无界,其实不太对,这里就判断是否超过了最大长度,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,判断一下有可能会抛出内存溢出异常
  5. 然后创建一个新的对象数组,并且 allocationSpinLock 重新恢复为0
  6. 执行了一次 Thread.yield(),让出 CPU,因为有可能其他线程正在扩容,让大家争抢一下
  7. 最后确保新的对象数组创建成功了,也就是扩容是没有问题的,再次加锁,数组拷贝,结束
    private void tryGrow(Object[] array, int oldCap) {
            lock.unlock(); // must release and then re-acquire main lock
            Object[] newArray = null;
            if (allocationSpinLock == 0 &&
                UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                         0, 1)) {
                try {
                    int newCap = oldCap + ((oldCap < 64) ?
                                           (oldCap + 2) : // grow faster if small
                                           (oldCap >> 1));
                    if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                        int minCap = oldCap + 1;
                        if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                            throw new OutOfMemoryError();
                        newCap = MAX_ARRAY_SIZE;
                    }
                    if (newCap > oldCap && queue == array)
                        newArray = new Object[newCap];
                } finally {
                    allocationSpinLock = 0;
                }
            }
            if (newArray == null) // back off if another thread is allocating
                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)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}

DelayQueue
DelayQueue 是支持延时的无界阻塞队列,这个在我们聊 ScheduledThreadPoolExecutor 也谈到过,里面也使用了延迟队列,只不过是它自己的一个内部类,DelayQueue 内部其实使用 PriorityQueue 来实现。

DelayQueue 的用法是添加元素的时候可以设置一个延迟时间,当时间到了之后才能从队列中取出来,使用 DelayQueue 中的对象必须实现 Delayed 接口,重写 getDelay 和  compareTo 方法,就像这样,那实现其实可以看 ScheduledThreadPoolExecutor 里面是怎么做的,这里我就不管那么多,示意一下就好了。

public class Test {

    public static void main(String[] args) throws Exception {
        DelayQueue<User> delayQueue = new DelayQueue<>();
        delayQueue.put(new User(1, "a"));
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    static class User implements Delayed {
        private Integer id;

        private String username;

        @Override
        public long getDelay(TimeUnit unit) {
            return 0;
        }

        @Override
        public int compareTo(Delayed o) {
            return 0;
        }
    }
}

我们可以看看他的属性和构造函数,呐看到了吧,使用的 PriorityQueue,另外构造函数比较简单了,不说了。

private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private Thread leader = null;
private final Condition available = lock.newCondition();

public DelayQueue();
public DelayQueue(Collection<? extends E> c);

OK,没啥毛病,这里我们要先看 take 方法,不能先看 put,否则我觉得闹不明白。

  1. 来第一步加锁,如果头结点是空的,也就是队列是空的话,阻塞,没啥好说的
  2. 反之队列有东西,我们就要去取了嘛,但是这里要看对象自己实现的 getDelay 方法获得延迟的时间,如果延迟的时间小于0,那说明到时间了,可以执行了,poll 返回
  3. 第一次,leader 线程肯定是空的,线程阻塞 delay 的时间之后才开始执行,完全没毛病,然后 leader 重新 置为 null
  4. 当 leader 不是 null 的时候,说明其他线程在操作了,所以阻塞等待唤醒
  5. 最后,leader 为 null,唤醒阻塞中的线程,解锁
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();
                if (first == null)
                    available.await();
                else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    first = null; // don't retain ref while waiting
                    if (leader != null)
                        available.await();
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }​

    然后再来看 put 就会简单多了,put 还是直接调用的 offer,看 offer 方法。

这里使用的是 PriorityQueue 的 offer 方法,其实和我们上面说到的 PriorityBlockingQueue 差不多,不再多说了,添加到队列头部之后,leader 置为 null,唤醒,结束了。

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e);
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

SynchronousQueue&LinkedTransferQueue
为什么这两个放一起说呢。。。因为这源码真的不想在这里说一遍,这俩源码可以单独出一个专题来写,长篇精悍文章不适合他他们,就简单先了解下。

SynchronousQueue 是一个不存储元素的阻塞队列,每个 put  必须等待 take,否则不能继续添加元素。

如果你还记得我们上面说到线程池的地方,newCachedThreadPool 默认就是使用的 SynchronousQueue。

他就两个构造方法,你一看就知道,对吧,支持公平和非公平,当然你也别问默认是啥,问就是非公平。

public SynchronousQueue();
public SynchronousQueue(boolean fair);

主要靠内部抽象类 Transferer,他的实现主要有两个,TransferQueue 和 TransferStack。

注意:如果是公平模式,使用的是 TransferQueue 队列,非公平则使用 TransferStack 栈。

abstract static class Transferer<E> {
 abstract E transfer(E e, boolean timed, long nanos);
}

LinkedTransferQueue 是链表组成的无界阻塞队列,看他内部类就知道了,这是个链表实现。

static final class Node {
    final boolean isData;   // 标记生产者或者消费者
    volatile Object item;   // 值
    volatile Node next;   // 下一个节点
    volatile Thread waiter;
}

LinkedBlockingDeque
LinkedBlockingDeque 是链表组成的双向阻塞队列,它支持从队列的头尾进行进行插入和删除元素。

构造函数有3个,不传初始容量就是 Integer 最大值。

public LinkedBlockingDeque() {
 this(Integer.MAX_VALUE);
}
public LinkedBlockingDeque(int capacity);
public LinkedBlockingDeque(Collection<? extends E> c);

看下双向链表的结构:

static final class Node<E> {
    E item;
    Node<E> prev;
    Node<E> next;
    Node(E x) {
        item = x;
    }
}

因为是双向链表,所以比其他的队列多了一些方法,比如 add、addFirst、addLast,add 其实就是 addLast,offer、put 也是类似。

我们可以区分看一下 putFirst 和 putLast ,主要区别就是 linkFirst 和 linkLast,分别去队列头部和尾部添加新节点,其他基本一致。

public void putFirst(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (!linkFirst(node))
                notFull.await();
        } finally {
            lock.unlock();
        }
}

public void putLast(E e) throws InterruptedException {
  if (e == null) throw new NullPointerException();
   Node<E> node = new Node<E>(e);
   final ReentrantLock lock = this.lock;
   lock.lock();
   try {
    while (!linkLast(node))
     notFull.await();
   } finally {
    lock.unlock();
   }
}

结尾

本次长篇内容参考书籍和文档

  1. Java 并发编程的艺术
  2. Java 并发编程之美
  3. Java 并发编程实战
  4. Java 8实战
  5. 极客时间:Java 并发编程实战
    OK,本期内容到此结束,我是艾小仙,我们过两个月再见。

 

文章转自公众号:艾小仙

分类
标签
已于2022-6-13 17:40:21修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐