java多线程(2)

356 阅读16分钟

多线程中并发容器的实现方式

我们可以知道,原有的java.util中的集合类 ,比如arrayList,map等等,都是线程不安全的,也就是不适合于多线程中使用,那么线程安全的集合类有哪些呢?

1. 用collection中的方法实现:

如下方代码

Map map = Collections.synchronizedMap(new HashMap<String, String>());

其实就是用Collections下面的多线程方法来包裹住原有的线程不安全的集合类: 简单来看一下Collections.synchronizedMap的实现,里面维护了一个普通的map: m和 静态的互斥锁mutex,任何操作都用通过这个互斥锁的判断,所以这个方式的线程安全是通过对Map接口中的方法使用synchronized 同步关键字来保证对Map的操作是线程安全的。


private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private static final long serialVersionUID = 1978198479659022715L;

    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize

    SynchronizedMap(Map<K,V> m) {
        this.m = Objects.requireNonNull(m);
        mutex = this;
    }

    SynchronizedMap(Map<K,V> m, Object mutex) {
        this.m = m;
        this.mutex = mutex;
    }
    public int size() {
        synchronized (mutex) {return m.size();}
  

    private transient Set<K> keySet;
    private transient Set<Map.Entry<K,V>> entrySet;
    private transient Collection<V> values;

    public Set<K> keySet() {
        synchronized (mutex) {
            if (keySet==null)
                keySet = new SynchronizedSet<>(m.keySet(), mutex);
            return keySet;
        }
    }

    public Set<Map.Entry<K,V>> entrySet() {
        synchronized (mutex) {
            if (entrySet==null)
                entrySet = new SynchronizedSet<>(m.entrySet(), mutex);
            return entrySet;
        }
    }

Collection.synchronized中的方法都是这种实现的方式,这里就不一一展开来说了。 在synchronized优化以前效率不好,在synchronized方法优化以后现在性能尚可。

优点:易于使用,调试,实现非常简单自由。

2. java.util.concurrent包 中的实现:

2.1 ConcurrentLinkedQueue

首先来看这个

class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {

ConcurrentLinkedQueue首先是一个有序队列,队列都遵循LinkedList相关的各种特性 不同的点在于ConcurrentLinkedQueue的节点存放数据,添加节点,都做了Cas操作,所有的操作都是无锁的,通过队列算法来保障并发不出现问题。

private static class Node<E> {
    volatile E item;
    volatile Node<E> next;

 
    Node(E item) {
        UNSAFE.putObject(this, itemOffset, item);
    }

    boolean casItem(E cmp, E val) {
        return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
    }

    // Unsafe mechanics

    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);
        }
    }
}

比如说UNSAFE.compareAndSwapObject,就是简单讲一下这个UNSAFE类。Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。这个UNSAFE类有一部分就是cas的实现,像UNSAFE.compareAndSwapObject 比较和交换Object的方法,这里不展开讨论cas的具体内容。我们只需要知道整个java.util.concurrent包中的无锁方法全部都是依靠UNSAFE类中的CAS方法来实现的。 简单讨论一下 offer 方法:

public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) {
        Node<E> q = p.next;
        循环找到整个node最后的节点
        if (q == null) {
            // p 是最后的节点
            if (p.casNext(null, newNode)) {
                //成功的CAS会使队列直线处理
                //要使e成为此队列中的一个元素,
                //让newNode成为真正队列的节点。
                if (p != t) // 如果一次性cas进去了两个节点
                    casTail(t, newNode);  
                    // casTail处理尾节点 ,失败也是可以接受的
                return true;
            }
            // 如果在和另一个线程的cas竞争中失败; 直接重新读取下一个节点 
        }
        else if (p == q)
            p = (t != (t = tail)) ? t : head;
        else
            // 两次跳跃以后查看结尾的节点
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

casNext方法就是 这个offer方法中具体调用cas算法的地方 这个方法是插入节点使用的

boolean casNext(Node<E> cmp, Node<E> val) {
    return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

casTail 方法是用来定位尾部节点用的,ConcurrentLinkedQueue中的方法基本都是通过cas算法实现

private boolean casTail(Node<E> cmp, Node<E> val) {
    return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val);
}

其他的add(), remove(),peek()等等方法的实现方式都是一致的,这里就不一一来讨论了;

2.2 BlockingQueue 阻塞队列

BlockingQueue 有两个子类 LinkedBlockingQueueArrayBlockingQueue

class BlockingQueue<E> extends Queue<E>
     java.io.Serializable

这里首先来看ArrayBlockingQueue

final Object[] items;

final ReentrantLock lock;

private final Condition notEmpty;

private final Condition notFull;

BlockingQueue 中阻塞的具体实现是依靠两个Condition 一个没有满,一个没有空,和一个可重入锁ReentrantLock来控制的 首先来看offer 方法:

public boolean offer(E e) {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    //判断一下 队列是否已经满了,满就直接返回不能
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

这里首先用可重入锁锁住了整个增加数据的方法,然后进行enqueue()

private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();
}
/** 队列 中给下一个put, offer,或者add 的指针位置  */
int putIndex;

enqueue()方法在 putindex的位置去放置数据,放置完毕以后count++,并且通知 非空的这个condition条件notEmpty.signal()。 整个队列的阻塞条件就是通过两个 condition的通知来实现的。 同时我们可以看到 add(), offer()都是用 lock.lock() 方法来加锁

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();

而put方法使用了lock.lockInterruptibly() ,阅读api文档以后说明是:

  • lock.lock():尝试获取锁。当该函数返回时,处于已经获取锁的状态。如果当前有别的线程获取了锁,则睡眠。

  • lockInterruptibly():尝试获取锁。如果当前有别的线程获取了锁,则睡眠。当该函数返回时,有两种可能:

    a. 已经获取了锁

    b. 获取锁不成功,但是别的线程打断了它。则该线程会抛出IterruptedException异常而返回,同时该线程的中断标志会被清除。

    如果 offer()方法没有获取锁,方法会等待锁的释放,而put则直接抛出IterruptedException等待外面的方法来处理这个异常。 同时put的不同点还有队列满的情况

 while (count == items.length)
   notFull.await();
  • add():把Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回异常

  • offer():表示如果可能的话,将Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.

  • put():把Object加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续. BlockingQueue示例 这里我们实现一个示例代码,实现一个简单的阻塞队列来更好的说明BlockingQueue内部的一些变化

public class CustomBlockingQueue<T> {
    /** 队列长度*/
    private final int size;

    /** 链表用于保存数据*/
    private final LinkedList<T> list = new LinkedList<>();

    /** 重入锁*/
    private final Lock lock = new ReentrantLock();

    /** 队列满时的等待条件*/
    private final Condition notFull = lock.newCondition();

    /** 队列空时的等待条件*/
    private final Condition notEmpty = lock.newCondition();

    public CustomBlockingQueue(final int size) {
        this.size = size;
    }

    /**
     * 如果队列满, 则阻塞
     * */
    public void add(T value) throws InterruptedException {
        lock.lock();
        try {
            while(list.size() == size) {
                System.out.println("队列已满 -- 入队过程进入等待");
                notFull.await();
            }
            /** 入队到链表末尾*/
            list.add(value);
            /** 唤醒在 notEmpty条件内等待的线程*/
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 如果队列为空, 则阻塞
     * */
    public T take() throws InterruptedException {
        T value;
        lock.lock();
        try {
            while(list.size() == 0) {
                System.out.println("队列为空 -- 出队过程进入等待");
                notEmpty.await();
            }
            /** 移除并返回链表头部的元素*/
            value = list.removeFirst();
            /** 唤醒在 notFull条件内等待的线程*/
            notFull.signal();
        } finally {
            lock.unlock();
        }

        return value;
    }

}

下面是调用的代码

public class QueueTest {
    static final CustomBlockingQueue<Integer> queue = new CustomBlockingQueue<>(3);

    public static void main(String[] args) {
        /** 多线程入队*/
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(10);
                    System.out.println("入队: " + i);
                    queue.add(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();

        /** 多线程出队*/
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                Thread.sleep(10);
                System.out.println("入队: " + i);
                queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();
    }
}

屏幕快照 2021-12-27 下午11.43.32.png 基本实现了一个简单的阻塞队列

2.3 CopyOnWriteList

读写分离的List,高效的读取,保证读取的数据速度尽量可能的快 在多数的常用业务场景中,读操作大大的多于写操作,CopyOnWriteList读操作不修改原有数据,写操作进行一次自我复制,并将修改内容写入副本,然后进行替换. 举例CopyOnWriteArrayList

class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable,

可以看到CopyOnWriteArrayList实现了RandomAccess和Cloneable 首先我们来看 get方法

private E get(Object[] a, int index) {
    return (E) a[index];
}

get方法时没有加锁的,所以可以理解为这个其实是线程不安全的。 然后我们来看set方法

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

这里在加锁以后进行了全局的拷贝方法, 然后返回了老的数据,从而达到了写操作的线程安全性。 这种方法只适合读操作非常多,写非常少的业务模式:比如微博,新闻,等等写少,读大大多于写的业务场景。

2.4 ConcurrentHashMap

首先我们要了解ConcurrentHashMap的背景,我们都知道HashMap是线程不安全的,而HashTable是线程安全的,但是HashTable的实现中使用了大量的synchronized关键字去保证线程安全,等于用了一把大锁去锁住了整个map,导致HashTable的运行的效率相较于HashMap低了非常多,在这样的情况下,jdk引入了线程安全的Map结构-ConcurrentHashMap 。

jdk里面的介绍: However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

简单说下就是完全实现了Hashtable所有功能,所有操作都是线程安全的,并且以不锁整个表结构的方式去提供所有的入口,不用synchronization的方式实现整个结构。

jdk 1.7和1.8中ConcurrentHashMap进行了改写,有了很大的区别,其实1.7和1.8的HashMap也改了很多地方,jdk1.8 引入了红黑树在大数据量HashMap的处理上,也就是1.7的map的实现是数组+链表结构 ,而1.8的HashMap使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(nlogN)提高了效率。这个思想也同时体现在了ConcurrentHashMap的实现中, Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,但是保证了线程安全 。

JDK 1.7实现ConcurrentHashMap的方法

在JDK1.7中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry(Hash表)组成的,HashEntry 用来封装映射表的键 / 值对;每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

ConcurrentHashMap 默认有 16 个 Segments,所以理论上,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。每个Segment内部更像是一个hashmap。

image.png

JDK1.8与1.7ConcurrentHashMap的区别

  • 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  • 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
  • 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
  • 定位结点的hash算法简化,会带来弊端:Hash冲突加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
  • 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

jdk 1.8里面实现是这样的

class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>

代码分析

首先根据入参的initialCapacity控制整个ConcurrentHashMap的初始容量,计算了sizeCtl,如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32。

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}
put

然后我们简单分析一下 put操作

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    //首先获得hash值
    int binCount = 0;
       //binCount是用来计算链表的长度
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果数组为"空",进行数组初始化
        if (tab == null || (n = tab.length) == 0)
        //初始化整个数组
            tab = initTable();
         //不为空时寻找 hash 值对应的数组下标,得到第一个节点 f
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        // 如果数组该位置为空,
      // 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
     // 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //这里是在扩容
        else if ((fh = f.hash) == MOVED)
        //做数据迁移
            tab = helpTransfer(tab, f);
        else {
        //到这里的判断条件是,f 是该位置的头节点,并且不为空
            V oldVal = null;
            //获得数组该位置的头节点的监视器的锁
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                // 头结点的 hash 值大于 0,说明是链表
                    if (fh >= 0) {
                    //记录这个链表的长度
                        binCount = 1;
                        //遍历整个链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 结束
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 到了链表的最末端,将这个新值放到链表的最后面
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //如果头节点是红黑树的类型
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 调用红黑树的插值方法插入新节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // binCount != 0 说明上面在做链表操作
            if (binCount != 0) {
            // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
                if (binCount >= TREEIFY_THRESHOLD)
                  // 这个方法不是一定会进行红黑树转换
                  // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

从put的操作中我们可以总结出这么几个ConcurrentHashMap的要点:

  1. 如果是在链表中插入,则插入的位置是在链表的尾部。
  2. ConcurrentHashMap会根据节点的数量binCount 去切换存储的结构- 链表或者红黑树,链表转为红黑树的临界值和 1.8的HashMap一样,都是8
  3. 如果当前的数组tab的长度在MIN_TREEIFY_CAPACITY 默认64 之下的话 ,会做扩容而不是转红黑树 转红黑树的代码:
private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        //如果整个table的数量小于64,就扩容至原来的一倍,不转红黑树了
        //因为这个阈值扩容可以减少hash冲突,不必要去转红黑树
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        //封装成TreeNode
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    //通过TreeBin对象对TreeNode转换成红黑树
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}
  1. 对于put写操作,如果当前链表已经迁移完成,那么头节点会被设置成fwd节点,此时写线程会帮助扩容,如果扩容没有完成,当前链表的头节点会被锁住,所以写线程会被阻塞,直到扩容完成。
初始化 initTable
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //空的table进入初始化
    while ((tab = table) == null || tab.length == 0) {
    //如果sizeCtl<0代表别的线程已经开始初始化了,当前线程挂起
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
            // CAS 一下,将 sizeCtl 设置为 -1,抢到锁并进行初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                // 初始化数组,长度为 16 或初始化时提供的长度
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    // 将这个数组赋值给 table,table 是 volatile 的
                    table = tab = nt;
                  // 如果 n 为 16 的话,那么这里 sc = 12
                 // 其实就是 0.75 * n
                    sc = n - (n >>> 2);
                }
            } finally {
            
              //设置默认的sizeCtl 完成初始化
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
扩容:tryPresize
// 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
private final void tryPresize(int size) {
    // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // 这个 if 分支和前面的初始化数组的代码基本上是一样的,不再说明
        if (tab == null || (n = tab.length) == 0) {
            n = (sc > c) ? sc : c;
            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2); // 0.75 * n
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {
            int rs = resizeStamp(n);
 
            if (sc < 0) {
                Node<K,V>[] nt;
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                //  用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法
                //    此时 nextTab 不为 null
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)           //     我是没看懂这个值真正的意义是什么?不过可以计算出来的是,结果是一个比较大的负数
            //  调用 transfer 方法,此时 nextTab 参数为 null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}

这个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt)。

数据迁移:transfer
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    if (fh >= 0) {
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}
数据获取:get
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //计算两次hash
    int h = spread(key.hashCode()); 
    if ((tab = table) != null && (n = tab.length) > 0 &&
    //读取首节点的Node元素
        (e = tabAt(tab, (n - 1) & h)) != null) {
         //如果该节点就是首节点就返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //查找,查找到就返回
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //既不是首节点也不是ForwardingNode,那就往下遍历
       while ((e = e.next) != null) {
       
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

ConcurrentHashMap的get操作的流程很简单,也很清晰,可以分为三个步骤来描述

没加锁

  1. 计算hash值,根据 hash 值找到数组对应位置: (n – 1) & h 根据该位置处结点性质进行相应查找,定位到该table索引位置,如果该位置为 null,那么直接返回 null 就可以了,如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
  2. 如果该位置节点的 hash 值小于 0 ,说明遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  3. 以上都不符合的话,那就是链表,就往下遍历节点,匹配就返回,否则最后就返回null

总结与思考

其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考

  1. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

  2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了

  3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档

  4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点

    1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
    2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
    3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

2.5 ConcurrentSkipListMap