put 和 take方法 其实 思想就是反的 举一反三即可
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;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) { //这个值 其实会存在误判 因为 在获取后线程执行权切换的话
// 不过就算误判 后续也能被唤醒
notFull.await();// 加入等待队列 等待 被唤醒
}
// 我个人认为 精髓在这2个代码.
// 首先 put 和 take 采用了 2把锁 互不相干 但是 这个 地方 竟然允许 同时有可能有2个
// 线程同时操作 items 单向链表 我开始也觉得 不可思议 但是大师就是 大师
// 如果是put 那么 永远操作 last节点 也就是说 如果 在put 线程操作 last的时候也就是 能被take的
// 最后一个节点。就算2个线程同时在操作 但是 put线程永远操作 last节点 把 追加的item 添加到last.next
// 之中。因为由于在操作 未cas之前 就算put了 但是没有cas take线程也无法消费 这个item
// 只会把最后 一个 last 设置为 head 因为每次取出 item 都会把取出 的节点设置为 head 所以
// 即使这段代码 没有 加 take 和put 关联的锁 也能保证线程安全性 就是靠这点
// 因为 在put 之后没有cas 只是提前put进去 你消费不了
// 只要保证 添加多少个 item 就是添加多少个 消费多少个item就是消费多少个 不会出现数据不一致性
// 所以也不需要加锁了
enqueue(node);
// 这个地方 之所以一个 atomicInteger 就行 是因为 只要 items总数加加 不会出现 多线程的 原子 可见性即可
// 而take 的线程只要保证 我take了一个 总数必须减去一是原子,可见性 即可
c = count.getAndIncrement(); // ++ 返回 ++之前的值
if (c + 1 < capacity)
// 这个地方避免了 put满后 大量线程 在等待队列上 但是 后续任务的吞吐量远远不足导致 线程一直处于等待
//队列上的情况 ,因为put满后 take 只会当 C= 临界值 来signalNotFull 一次 后面吞吐量
// 跟不上 就可能出现 一直被挂着
//为什么我这么断定 是因为 为什么会item在明明小于 最大item数 竟然去唤醒 等待队列中的线程
// 我item都没塞满 为什么可能 出现线程在 等待队列上呢? 就是由于上面这点
notFull.signal();
} finally {
putLock.unlock();
}
// 如果=0 代表之前有可能会有很多线程 在takeCondition上等待 唤醒
// 为什么是C=0 才需要 唤醒 take的等待队列呢 因为 如果是 c=界限值 才唤醒 那么
// 任务是满的 还有线程在 take等待队列上 等待 这些等待 不就是 之前没有 任务拿 而阻塞来的吗。
if (c == 0)
signalNotEmpty();
}
enqueue(node);
c = count.getAndIncrement();
不管是在 poll 还是在put
都是先放 后++ 或者 先取后 - -
其实这也是 有考虑得 因为如果 是poll得情况
先减 后放 会出现 其实减了 但是没poll拿出来这个时候 如果 有线程拿到
pollLock 去放 岂不是 会真实得链表长度已经 超过界限 但是 实际显示没超
put 如果是先++ 在put 会出现 我还没放 但是 你增加了技术 去poll得线程觉得有
返回得其实是null 但是 put得线程其实还没放进去,所以 把去拿 去取 放在 cas
之前 从而规避了这些问题
大师的思想 直得 深思,可怕的是 十几年前就这么 厉害了 我就像个弟弟