• 并发容器Map


    并发容器Map

    学习材料来源于网络
    如有侵权,联系删除

    HashMap 简单分析

    在了解并发容器之前呢,我们先了解Hash,这一章节是基于个人理解中快速讲解的

    我们先从Hash计算,到存储结构,再到源码分析,再到细节介绍

    Hash计算

    哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。

    哈希算法最重要的特点就是:

    • 相同的输入一定得到相同的输出;
    • 不同的输入大概率得到不同的输出。

    哈希算法的目的就是为了验证原始数据是否被篡改。

    Java字符串的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定的4字节int整数

    常用的哈希算法有:

    算法 输出长度(位) 输出长度(字节)
    MD5 128 bits 16 bytes
    SHA-1 160 bits 20 bytes
    RipeMD-160 160 bits 20 bytes
    SHA-256 256 bits 32 bytes
    SHA-512 512 bits 64 bytes

    那什么是hash冲突呢,

    hashValue = hashCode(key)。输入一个任意的key都能得到一个输出固定的字节数组hashValue。

    哈希碰撞是指,两个不同的输入得到了相同的输出,也叫hash冲突。

    在HashMap的代码中存储在这样的一个方法

    /**
         * 基本哈希箱节点,用于大多数条目。 (有关TreeNode子类的信息,请参见下文;有关其Entry子类的信息,请参见LinkedHashMap。)
         */
    static class Node<K,V> implements Map.Entry<K,V> {
       	//******
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //*******
    }
    

    这里我们可以看到对于的hashCode返回值是4个字节大小的Hash值。

    存储结构

    HashMap的存储结构主要分为两种,一种是数组+链表,第二种是数组+树。

    数组+链表

    如图1所示,可以大概的看到HashMap中的大致存储结构

    图1

    数组存储的是同组index的首部,index的计算并不是直接通过key算出hashCode大小4个字节。如果是这样,我们可以想象需要开辟太大的内存空间来存储不同的hashCode的数组大小。把这个存储链表首部的数组叫做哈希桶数组。

    在HashMap中的做法就是,

    1、计算HashCode

    2、使用HashCode对数组大小取余,得到index。

    3、当index的位置,已经存储得有其他的元素的时候,这个时候,HashMap会使用已经有的这个元素节点作为一个链表的头部,把其他相同index不同Key的数据存储在后面。

    这里需要注意的是,哈希桶中的index相同不是哈希冲突,记住,已经是通过hashValue对哈希桶大小取余了。

    其他细节性的东西在后面会介绍

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //哈希桶中的tab为空的时候创建
        if ((tab = table) == null || (n = tab.length) == 0) {
            n = (tab = resize()).length;
        }
        //计算index
        if ((p = tab[i = (n - 1) & hash]) == null) {
            tab[i] = newNode(hash, key, value, null);
        } else {
            Node<K,V> e; K k;
            //如果相同的hash的k的话,hash桶中的index的数据进行覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) {
                e = p;
            } else if (p instanceof TreeNode) {
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            } else {
                //链表插入
                for (int binCount = 0; ; ++binCount) {
                     if ((e = p.next) == null) {
                         p.next = newNode(hash, key,value,null);
                            //链表长度大于8转换为红黑树进行处理
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                             treeifyBin(tab, hash);
                         break;
                     }
                        // key已经存在直接覆盖value
                     if (e.hash == hash &&
                         ((k = e.key) == key || (key != null && key.equals(k)))) 
    							break;
                     p = e;
                 }
            }
            //***
        }
        //***
        return null;
    }
    

    数组+红黑树

    通过上面的问题,我们可以发现,hash桶中同一个index的不同key存储在一个链表中,我们都知道链表性能在数据达到一定量的时候,性能会变低,所以在JDK8中,出现了通过链表改成红黑树,而hash通中存储的是红黑树的根节点。把相同index不同key的数据存储在一棵树结构里面。

    结构如图2

    什么时候HashMap的存储结构会从 数组+链表 转变成 数组+红黑树 呢?

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    //***
                            //链表长度大于TREEIFY_THRESHOLD 8转换为红黑树进行处理
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
                             treeifyBin(tab, hash);
                         break;
                     }
    //***
    }
    

    但是在这里也不一定超过8的时候,就会转换成树结构

    final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
        //如果大于8,且是小于MIN_TREEIFY_CAPACITY=64 的时候,只是简单的扩容
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
            else if ((e = tab[index = (n - 1) & hash]) != null) {
                TreeNode<K,V> hd = null, tl = null;
                do {
                    TreeNode<K,V> p = replacementTreeNode(e, null);
                    if (tl == null)
                        hd = p;
                    else {
                        p.prev = tl;
                        tl.next = p;
                    }
                    tl = p;
                } while ((e = e.next) != null);
                if ((tab[index] = hd) != null)
                    hd.treeify(tab);
            }
        }
    

    确定哈希桶数组索引位置

    不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现(方法一+方法二):

    方法一:
    static final int hash(Object key) {   //jdk1.8 & jdk1.7
         int h;
         // h = key.hashCode() 为第一步 取hashCode值
         // h ^ (h >>> 16)  为第二步 高位参与运算
         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    方法二:
    static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
         return h & (length-1);  //第三步 取模运算
    }
    

    这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算。

    对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用方法一所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用方法二来计算该对象应该保存在table数组的哪个索引处。

    这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。

    在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

    下面举例说明下,n为table的长度。

    扩容机制

    扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。

    /**
         *初始化或增加表大小。如果为null,则根据字段阈值中保持的初始容量目标分配
         *否则,因为我们使用的是2的幂,所以每个bin中的
         *元素必须保持相同的索引,或者在新表中以2的偏移量移动
         *
         * @return the table
         */
        final Node<K,V>[] resize() {
            //******
        }
    

    同时我们需要知道的是,hash桶的数组增大的时候,不光是对数组进行增大,而且还对已经存储的数据index的位置,发送了该表,因为hashCodeValue与桶大小取余的值也会发送变化,关于这种变化,JDK7和JDK8提供两种不同的扩容方式。这些内容可以参考:美团技术博客的文章:https://tech.meituan.com/2016/06/24/java-hashmap.html

    Hashtable 线程安全

    put存储数据

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
    
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
    
        addEntry(hash, key, value, index);
        return null;
    }
    

    get获取数据

    public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }
    

    remove 删除数据

    @Override
    public synchronized boolean remove(Object key, Object value) {
        Objects.requireNonNull(value);
    
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        for (Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
            if ((e.hash == hash) && e.key.equals(key) && e.value.equals(value)) {
                modCount++;
                if (prev != null) {
                    prev.next = e.next;
                } else {
                    tab[index] = e.next;
                }
                count--;
                e.value = null;
                return true;
            }
        }
        return false;
    }
    

    ConcurrentHashMap 线程安全

    JDK 7 源码分析

    图示

    对于segment是不可用扩容的,默认的是16个,但是table下面采用的entry是可以进行扩容的,但是在entry只会连接链表。

    JDK 8 源码分析

    putVal

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                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 {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
    

    initTable

    /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
    

    图示

    ConcurrentSkipListMap

    (跳表)

    特点:有序链表实现,无锁实现;value不能为空;层级越高跳跃性越大,数据越少,查询理论变快。

    • 新的node是否抽出来作为index,随机决定;
    • index对应的level由随机数决定。(随机数比特位连续为1的数量)
    • 每层的元素,headIndex固定为所有node中最小的;

    查找数据时,按照从上到下,从左往右的顺序查找

    • 时间复杂度O(log n),空间复杂度O(n)o
    • 空间换时间,数据库索引类似的概念,skiplist在很多开源组件中有使用(level DB,Redis)

    数据结构

    ConcurrentSkipListMap就是基于这种数据结构上进行操作的,

    node

    static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;
    
        Node(K key, Object value, Node<K,V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
        Node(Node<K,V> next) {
            this.key = null;
            this.value = this;
            this.next = next;
        }
        boolean casValue(Object cmp, Object val) {
            return UNSAFE.compareAndSwapObject(this, valueOffset, cmp, val);
        }
        boolean casNext(Node<K,V> cmp, Node<K,V> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }
        boolean isMarker() {
            return value == this;
        }
    
        boolean isBaseHeader() {
            return value == BASE_HEADER;
        }
        boolean appendMarker(Node<K,V> f) {
            return casNext(f, new Node<K,V>(f));
        }
        void helpDelete(Node<K,V> b, Node<K,V> f) {
            if (f == next && this == b.next) {
                if (f == null || f.value != f) // not already marked
                    casNext(f, new Node<K,V>(f));
                else
                    b.casNext(this, f.next);
            }
        }
        V getValidValue() {
            Object v = value;
            if (v == this || v == BASE_HEADER)
                return null;
            @SuppressWarnings("unchecked") V vv = (V)v;
            return vv;
        }
        AbstractMap.SimpleImmutableEntry<K,V> createSnapshot() {
            Object v = value;
            if (v == null || v == this || v == BASE_HEADER)
                return null;
            @SuppressWarnings("unchecked") V vv = (V)v;
            return new AbstractMap.SimpleImmutableEntry<K,V>(key, vv);
        }
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        private static final long nextOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Node.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("value"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
    

    Index

    static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;
        Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
            this.node = node;
            this.down = down;
            this.right = right;
        }
    
        final boolean casRight(Index<K,V> cmp, Index<K,V> val) {
            return UNSAFE.compareAndSwapObject(this, rightOffset, cmp, val);
        }
    
        final boolean indexesDeletedNode() {
            return node.value == null;
        }
        final boolean link(Index<K,V> succ, Index<K,V> newSucc) {
            Node<K,V> n = node;
            newSucc.right = succ;
            return n.value != null && casRight(succ, newSucc);
        }
        final boolean unlink(Index<K,V> succ) {
            return node.value != null && casRight(succ, succ.right);
        }
        private static final sun.misc.Unsafe UNSAFE;
        private static final long rightOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> k = Index.class;
                rightOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("right"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
    

    doPut方法

    private V doPut(K key, V value, boolean onlyIfAbsent) {
        Node<K,V> z;             // added node
        if (key == null)
            throw new NullPointerException();
        Comparator<? super K> cmp = comparator;
        outer: for (;;) {
            for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                if (n != null) {
                    Object v; int c;
                    Node<K,V> f = n.next;
                    if (n != b.next)               // inconsistent read
                        break;
                    if ((v = n.value) == null) {   // n is deleted
                        n.helpDelete(b, f);
                        break;
                    }
                    if (b.value == null || v == n) // b is deleted
                        break;
                    if ((c = cpr(cmp, key, n.key)) > 0) {
                        b = n;
                        n = f;
                        continue;
                    }
                    if (c == 0) {
                        if (onlyIfAbsent || n.casValue(v, value)) {
                            @SuppressWarnings("unchecked") V vv = (V)v;
                            return vv;
                        }
                        break; // restart if lost race to replace value
                    }
                    // else c < 0; fall through
                }
    
                z = new Node<K,V>(key, value, n);
                if (!b.casNext(n, z))
                    break;         // restart if lost race to append to b
                break outer;
            }
        }
    
        int rnd = ThreadLocalRandom.nextSecondarySeed();
        if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
            int level = 1, max;
            while (((rnd >>>= 1) & 1) != 0)
                ++level;
            Index<K,V> idx = null;
            HeadIndex<K,V> h = head;
            if (level <= (max = h.level)) {
                for (int i = 1; i <= level; ++i)
                    idx = new Index<K,V>(z, idx, null);
            }
            else { // try to grow by one level
                level = max + 1; // hold in array and later pick the one to use
                @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                    (Index<K,V>[])new Index<?,?>[level+1];
                for (int i = 1; i <= level; ++i)
                    idxs[i] = idx = new Index<K,V>(z, idx, null);
                for (;;) {
                    h = head;
                    int oldLevel = h.level;
                    if (level <= oldLevel) // lost race to add level
                        break;
                    HeadIndex<K,V> newh = h;
                    Node<K,V> oldbase = h.node;
                    for (int j = oldLevel+1; j <= level; ++j)
                        newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                    if (casHead(h, newh)) {
                        h = newh;
                        idx = idxs[level = oldLevel];
                        break;
                    }
                }
            }
            // find insertion points and splice in
            splice: for (int insertionLevel = level;;) {
                int j = h.level;
                for (Index<K,V> q = h, r = q.right, t = idx;;) {
                    if (q == null || t == null)
                        break splice;
                    if (r != null) {
                        Node<K,V> n = r.node;
                        // compare before deletion check avoids needing recheck
                        int c = cpr(cmp, key, n.key);
                        if (n.value == null) {
                            if (!q.unlink(r))
                                break;
                            r = q.right;
                            continue;
                        }
                        if (c > 0) {
                            q = r;
                            r = r.right;
                            continue;
                        }
                    }
    
                    if (j == insertionLevel) {
                        if (!q.link(r, t))
                            break; // restart
                        if (t.node.value == null) {
                            findNode(key);
                            break splice;
                        }
                        if (--insertionLevel == 0)
                            break splice;
                    }
    
                    if (--j >= insertionLevel && j < level)
                        t = t.down;
                    q = q.down;
                    r = q.right;
                }
            }
        }
        return null;
    }
    

    casNext

    /**
     * compareAndSet next field
     */
    boolean casNext(Node<K,V> cmp, Node<K,V> val) {
        return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
    }
    

    总结

    都是基于CAS的原子性操作

    参考文献:https://www.cnblogs.com/skywang12345/p/3498556.html

    CopyOnWriteArrayList

    copyonWriteArrayList容器即写时复制的容器和ArrayList比较,

    优点是并发安全,缺点有两个:

    1、多了内存占用:写数据是copy一份完整的数据,单独进行操作。占用双份内存。

    2、数据一致性:数据写完之后,其他线程不一定是马上读取到最新内容。

    源码

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
          //
        } finally {
            lock.unlock();
        }
    }
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
           //
        } finally {
            lock.unlock();
        }
    }
    public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);
        return (index < 0) ? false : remove(o, snapshot, index);
    }
    private boolean remove(Object o, Object[] snapshot, int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    void removeRange(int fromIndex, int toIndex) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    public boolean containsAll(Collection<?> c) {
        Object[] elements = getArray();
        int len = elements.length;
        for (Object e : c) {
            if (indexOf(e, elements, 0, len) < 0)
                return false;
        }
        return true;
    }
    public boolean removeAll(Collection<?> c) {
        if (c == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    public boolean retainAll(Collection<?> c) {
        if (c == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //
        } finally {
            lock.unlock();
        }
    }
    
    记得加油学习哦^_^
  • 相关阅读:
    【编程思想】【设计模式】【结构模式Structural】适配器模式adapter
    【编程思想】【设计模式】【结构模式Structural】3-tier
    【编程思想】【设计模式】【创建模式creational】原形模式Prototype
    【编程思想】【设计模式】【创建模式creational】Pool
    【编程思想】【设计模式】【创建模式creational】lazy_evaluation
    【编程思想】【设计模式】【创建模式creational】Borg/Monostate
    【编程思想】【设计模式】【创建模式creational】抽象工厂模式abstract_factory
    【编程思想】【设计模式】【创建模式creational】建造者模式builder
    【编程思想】【设计模式】【行为模式Behavioral】策略模式strategy
    【编程思想】【设计模式】【创建模式creational 】工厂模式factory_method
  • 原文地址:https://www.cnblogs.com/shaoyayu/p/14073973.html
Copyright © 2020-2023  润新知