zoukankan      html  css  js  c++  java
  • Java Collection秋招复习

    抽象类和接口的区别

    我们先来看一下抽象类

    /**
     * @auther draymonder
     */
    public abstract class AbstractClassTest {
        private int Test1;
    
        public int Test2;
    
        public void test1() {
            return ;
        }
    
        protected void test2() {
            return ;
        }
    
        private void test3() {
            return ;
        }
    
        void test4() {
            return ;
        }
    
        public abstract void test5();
    
        protected abstract void test6();
    
        public static void test7() {
            return ;
        }
    }
    

    我们再来看一下接口

    /**
     * @auther draymonder
     */
    public interface IntefaceTest {
        public int Test1 = 0;
    
        void test1();
    
        default void test2() {
            return ;
        }
    
        public static void test3() {
            return ;
        }
    }
    

    由此我们可以知道

    1. 接口中没有构造方式
    2. 接口中的方法必须是抽象的(在JDK8interface可以使用default实现方法)
    3. 接口中除了static、final变量,不能有其他变量
    4. 接口支持多继承

    Java集合

    ArrayList

    数组的默认大小为 10。

    private static final int DEFAULT_CAPACITY = 10;
    

    添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

    Vector

    数组的默认大小为 10。

    Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。

    Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
    可以使用collections的同步list的方法

    List<String> list = new ArrayList<>();
    List<String> synList = Collections.synchronizedList(list);
    

    CopyOnWriteArrayList

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    
    final void setArray(Object[] a) {
        array = a;
    }
    

    适用场景

    CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

    但是 CopyOnWriteArrayList 有其缺陷:

    • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
    • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
      所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

    hashMap

    hash为2的幂的作用

    1. key & (hash - 1)等同于key % hash,但前者效率比后者高
    2. 扩容的时候,table cap变为2 * table cap,rehash仅仅需要判断key & table cap如果为0,还是原来的table[old],否则是table[old+table cap]

    mask码的作用

    先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:

    mask |= mask >> 1    11011000
    mask |= mask >> 2    11111110
    mask |= mask >> 4    11111111
    

    mask+1 是大于原始数字的最小的 2 的 n 次方。

    num     10010000
    mask+1 100000000
    

    以下是 HashMap 中计算数组容量的代码:

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    

    JDK7版本下的链表循环

    在扩容时候,由于是头插法,所以,原来是A->B,但是多线程情况下会出现。
    线程1刚刚拿出A,此时A—>B 然后线程2拿到了B, 然后头插法 A插入到B后面, 最后情况就成了A->B->A
    链表循环
    ZGmqsI.png

    hashmap为什么load factor为0.75

    如果load factor太小,那么空间利用率太低;如果load factor太大,那么hash冲撞就会比较多

    JDK8下hashmap为什么为长度为8链表转为红黑树

    我们来看一下hashmap的注释

    Because TreeNodes are about twice the size of regular nodes, we
    use them only when bins contain enough nodes to warrant use
    (see TREEIFY_THRESHOLD). And when they become too small (due to
    removal or resizing) they are converted back to plain bins.  In
    usages with well-distributed user hashCodes, tree bins are
    rarely used.  Ideally, under random hashCodes, the frequency of
    nodes in bins follows a Poisson distribution
    (http://en.wikipedia.org/wiki/Poisson_distribution) with a
    parameter of about 0.5 on average for the default resizing
    threshold of 0.75, although with a large variance because of
    resizing granularity. Ignoring variance, the expected
    occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
    factorial(k)). The first values are:
    
    0:    0.60653066
    1:    0.30326533
    2:    0.07581633
    3:    0.01263606
    4:    0.00157952
    5:    0.00015795
    6:    0.00001316
    7:    0.00000094
    8:    0.00000006
    more: less than 1 in ten million
    

    我们去有道翻译translate一下

    因为树节点的大小大约是普通节点的两倍,所以我们
    只有当容器中包含足够的节点以保证使用时才使用它们
    (见TREEIFY_THRESHOLD)。当它们变得太小的时候
    移除或调整大小)它们被转换回普通的箱子。在
    使用分布良好的用户哈希码,树箱是
    很少使用。理想情况下,在随机哈希码下
    箱中的节点遵循泊松分布
    (http://en.wikipedia.org/wiki/Poisson_distribution)
    默认大小调整的参数平均约为0.5
    阈值为0.75,虽然由于方差较大
    调整粒度。忽略方差,得到期望
    列表大小k的出现次数为(exp(-0.5) * pow(0.5, k) /
    阶乘(k))。第一个值是:
    0:0.60653066
    1:0.30326533
    2:0.07581633
    3:0.01263606
    4:0.00157952
    5:0.00015795
    6:0.00001316
    7:0.00000094
    8:0.00000006
    多于:少于千分之一
    

    所以,节点插入遵循泊松分布,因此出现一个桶内8个节点是极小概率事件,所以遇到这种情况我们可以用红黑树加速get操作

    红黑树的性质

    1. 每个节点要么是红色,要么是黑色
    2. 根节点是黑色的
    3. 每个叶节点(NULL)是黑色的
    4. 如果一个节点是红色的,那么他的子节点都是黑色的
    5. 对于每个节点,从该节点到后代叶节点的简单路径,包含相同数目的黑色节点

    红黑树插入过程 参考博客
    动态插入演示

    ConcurrentHashMap

    不支持 key为null 也不支持 value为null

    JDK7版本下的

    //默认的数组大小16(HashMap里的那个数组)
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    //扩容因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
     
    //ConcurrentHashMap中的数组
    final Segment<K,V>[] segments
    
    //默认并发标准16
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    
    //Segment是ReentrantLock子类,因此拥有锁的操作
     static final class Segment<K,V> extends ReentrantLock implements Serializable {
      //HashMap的那一套,分别是数组、键值对数量、阈值、负载因子
      transient volatile HashEntry<K,V>[] table;
      transient int count;
      transient int threshold;
      final float loadFactor;
    
      Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
                this.loadFactor = lf;
                this.threshold = threshold;
                this.table = tab;
            }
     }
     
     //换了马甲还是认识你!!!HashEntry对象,存key、value、hash值以及下一个节点
     static final class HashEntry<K,V> {
            final int hash;
            final K key;
            volatile V value;
            volatile HashEntry<K,V> next;
     }
    //segment中HashEntry[]数组最小长度
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
    
    //用于定位在segments数组中的位置,下面介绍
    final int segmentMask;
    final int segmentShift;
    

    put函数

    public V put(K key, V value) {
        Segment<K,V> s;
        //步骤①注意valus不能为空!!!
        if (value == null)
            throw new NullPointerException();
        //根据key计算hash值,key也不能为null,否则hash(key)报空指针
        int hash = hash(key);
        //步骤②派上用场了,根据hash值计算在segments数组中的位置
        int j = (hash >>> segmentShift) & segmentMask;
        //步骤③查看当前数组中指定位置Segment是否为空
        //若为空,先创建初始化Segment再put值,不为空,直接put值。
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
    

    ensureSegement

    可以看到JDK7版本下,ConcurrentHashMapsegment也是使用写时复制的,并且使用CAS算法来将副本替换

    private Segment<K,V> ensureSegment(int k) {
        //获取segments
        final Segment<K,V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // raw offset
        Segment<K,V> seg;
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
            //拷贝一份和segment 0一样的segment
            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
            //大小和segment 0一致,为2
            int cap = proto.table.length;
            //负载因子和segment 0一致,为0.75
            float lf = proto.loadFactor;
            //阈值和segment 0一致,为1
            int threshold = (int)(cap * lf);
            //根据大小创建HashEntry数组tab
            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
            //再次检查
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                == null) { // recheck
                根据已有属性创建指定位置的Segment
                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                       == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }
    

    put value

    首先lock获取 tab[hash(key)]
    然后进行操作

    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        //步骤① start
        HashEntry<K,V> node = tryLock() ? null :
            scanAndLockForPut(key, hash, value);
        //步骤① end
        V oldValue;
        try {
            //步骤② start
            //获取Segment中的HashEntry[]
            HashEntry<K,V>[] tab = table;
            //算出在HashEntry[]中的位置
            int index = (tab.length - 1) & hash;
            //找到HashEntry[]中的指定位置的第一个节点
            HashEntry<K,V> first = entryAt(tab, index);
            for (HashEntry<K,V> e = first;;) {
                //如果不为空,遍历这条链
                if (e != null) {
                    K k;
                    //情况① 之前已存过,则替换原值
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        oldValue = e.value;
                        if (!onlyIfAbsent) {
                            e.value = value;
                            ++modCount;
                        }
                        break;
                    }
                    e = e.next;
                }
                else {
                    //情况② 另一个线程的准备工作
                    if (node != null)
                        //链表头插入方式
                        node.setNext(first);
                    else //情况③ 该位置为空,则新建一个节点(注意这里采用链表头插入方式)
                        node = new HashEntry<K,V>(hash, key, value, first);
                    //键值对数量+1
                    int c = count + 1;
                    //如果键值对数量超过阈值
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        //扩容
                        rehash(node);
                    else //未超过阈值,直接放在指定位置
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    //插入成功返回null
                    oldValue = null;
                    break;
                }
            }
        //步骤② end
        } finally {
            //步骤③
            //解锁
            unlock();
        }
        //修改成功,返回原值
        return oldValue;
    }
    

    scanAndLockForPut

    retries64次,不行的话,才用ReentrantLock重入锁

    private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
        //通过Segment和hash值寻找匹配的HashEntry
        HashEntry<K,V> first = entryForHash(this, hash);
        HashEntry<K,V> e = first;
        HashEntry<K,V> node = null;
        //重试次数
        int retries = -1; // negative while locating node
        //循环尝试获取锁
        while (!tryLock()) {
            HashEntry<K,V> f; // to recheck first below
            //步骤①
            if (retries < 0) {
                //情况① 没找到,之前表中不存在
                if (e == null) {
                    if (node == null) // speculatively create node
                        //新建 HashEntry 备用,retries改成0
                        node = new HashEntry<K,V>(hash, key, value, null);
                    retries = 0;
                }
                //情况② 找到,刚好第一个节点就是,retries改成0
                else if (key.equals(e.key))
                    retries = 0;
                //情况③ 第一个节点不是,移到下一个,retries还是-1,继续找
                else
                    e = e.next;
            }
            //步骤②
            //尝试了MAX_SCAN_RETRIES次还没拿到锁,简直B了dog!
            else if (++retries > MAX_SCAN_RETRIES) {
                //泉水挂机
                lock();
                break;
            }
            //步骤③
            //在MAX_SCAN_RETRIES次过程中,key对应的entry发生了变化,则从头开始
            else if ((retries & 1) == 0 &&
                     (f = entryForHash(this, hash)) != first) {
                e = first = f; // re-traverse if entry changed
                retries = -1;
            }
        }
        return node;
    }
    

    最后的put流程

    rehash的话 同jdk8版本下的rehash
    ConcurrentHashMap JDK7下的put流程

    size

    retries2次 如果还是不同,那么就reentranLock依次等待unlock计算每个tab的size

    JDK8下的ConcurrentHashMap

    put

    public V put(K key, V value) {
        return putVal(key, value, false);
    }
    
    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key/value不能为空!!!
        if (key == null || value == null) throw new NullPointerException();
        //计算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //注释① 表为null则初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //CAS方法判断指定位置是否为null,为空则通过创建新节点,通过CAS方法设置在指定位置
            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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    //链表中节点个数超过8转成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //注释③ 添加节点
        addCount(1L, binCount);
        return null;
    }
    

    为什么tab[hash(key)]用cas,但put里面的元素都需要用synchronized呢

    其实hash冲撞的几率蛮低的,所以synchronized调用的次数并不多,更多的是在cas那里...
    然后就是cas比synchronized的优点...

    size

    每次put完毕,都会调用addCount方法

    private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
    
  • 相关阅读:
    还得还得学啊
    感觉自己写的东西很死板啊
    好烦啊,不知道选哪个?
    222
    111
    愁人啊
    ssm+ajax实现登陆
    ssm框架搭建
    抽象类与接口
    代理设计模式
  • 原文地址:https://www.cnblogs.com/Draymonder/p/11114263.html
Copyright © 2011-2022 走看看