常见数据结构浅析

常见数据结构浅析

1.ArrayList、LinkedList和CopyOnWriteArrayList

ArrayList

特点

  1. 线程不安全
  2. 底层数据结构是数组(查询快,增删慢,支持快速随机访问)
  3. 内存占用会存在部分浪费,末尾会预留一部分容量空间

容量

当创建一个ArrayList对象时,它会分配一定的初始容量,通常为10
private static final int DEFAULT_CAPACITY = 10;

添加元素:

1.判断需要的容量是不是大于数组长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);
2.扩容 扩为原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);

3.复制原数据到新数组
elementData = Arrays.copyOf(elementData, newCapacity);

4.把新元素添加到数组末尾
elementData[size++] = e;

移除元素:

1.计算一个元素得位置
int numMoved = size - index - 1;
2.复制
把原数组的第index+1后面的数据复制到原数组的index位置复制长度为size - index - 1
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
3.把数组最后一个元素置空
elementData[–size] = null;

解决ArraryList线程不安全

1.vector
2.Collections.synchronizedList(new ArrayList<>());
3.CopyOnWriteArrayList

LinkedList
add

LinkedList底层采用双向链表结构:具有增删快、查询慢(与ArrayList对比)
添加元素:add 源码

   void linkLast(E e) {
        final Node<E> l = last;
        //构建元素的指向关系
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

//执行 final Node newNode = new Node<>(l, e, null);会进入如下代码

 Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }

get
查询元素

  Node<E> node(int index) {
        // assert isElementIndex(index);
		//判断要查的元素小于集合长度的一半 则从前往后遍历
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
        //判断要查的元素大于集合长度的一半 则从后往前遍历
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

CopyOnWriteArrayList

写时复制: CopyOnWrite 容器即写时复制容器。往一个容器添加元素时,不直接往Object[]添加,而是先将当前容器Object[]进行copy 复制出一个新得容器,Object[] newElements,然后往新的容器newElements里添加元素,添加完元素后再将原容器的引用指向新得容器setArray(newElements)。这样做的好处是可以对CopyOnWrite容器进行并发读,而不需要加锁,因为当前容器不添加任何元素。所以CopyOnWrite是一种读写分离思想,即读和写用的不同容器

写的时候使用了ReentrantLock枷锁

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

数组array本身使用了volatile,保证多线程可见性

private transient volatile Object[] array;

2.HashSet和CopyOnWriteArraySet

HashSet线程不安全其底层就是一个HashMap<E,Object> HashSet的add方法

 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

PRESENT是(常量):

 private static final Object PRESENT = new Object();

CopyOnWriteArraySet

底层依然是CopyOnWriteArrayList,线程安全

3.HashMap与ConcurrentHashMap(jdk1.8)

hashMapconcurrentHashMap
线程不安全的线程安全
数组 + 链表 + 红黑树分段数组 + 链表 + 红黑树
高并发情况下,put、remove 成员变量时可能产生线程安全问题,需加锁线程安全,因为底层代码在操作每一个Node时都会对Node加锁synchronized,保证线程安全
读取不加锁读取数据时不加锁,高效,且因为map中的value值是添加volatile关键字修饰的,可保证读取到最新值,降低CPU负载
元素插入后判断数组长度是否超阈,默认阈值0.75,若超阈则进行扩容,扩容大小为原数组的2的幂次方(原数组长度往左位移1),若原数组所在内存上没有连续的可用空间,则申请新的可用连续空间,将旧数组复制到新的地址,再将旧数组置为null,等待GC回收同hashMap

hash冲突:当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。
开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法.(链表/红黑树就是为了解决hash冲突而存在的)
HashMap即是采用了链地址法.
JDK7 使用了数组+链表的方式
JDK8 使用了数组+链表+红黑树的方式

扩容:

1.先把原数组的大小扩为原来的一倍,比如现在是16,扩容后就是32

java newCap = oldCap << 1
2.把旧数组的元素赋值给新得数组
newTab[e.hash & (newCap - 1)] = e;

触发扩容的条件:

扩容的方法叫resize
1.进入的时候判断Node的长度为0

 if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

2.当数组长度达到 加载因子*数字最大长度

newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 threshold = newThr;
if (++size > threshold)
            resize();

HashMap在put一个值的流程是什么?如何扩容?
1、通过Hash算法获得key值对应的hash值
2、根据hash值确定当前key所在node数组的索引 (n - 1) & hash,如果node[i]==null 则直接创建新数组
3、如果node[i]不是空
-----------3.1 判断 当前node的头结点的 hash和key是否都相等, 相等则需要操作的就是该node
-----------3.2 判断当前节点是否为TreeNode,对TreeNode进行操作,并返回结果e
-----------3.3 如果是链表则遍历链表,key存在则返回节点e,不存在则赋值
-----------3.4 判断节点e有没有被赋值,覆盖旧值
4、hashMap size进行加1,同时判断新size是否大于扩容阈值从而判断是否需要扩容
5、扩容阈值是旧扩容阈值的2倍。newThr = oldThr << 1;

 public V put(K key, V value) {
 		// 1.通过Hash算法获得key值对应的hash值
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 声明Node数组tab, Node节点
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 对tab数组赋值为当前HashMap的table, 并判断是否为空, 或者长度为0
        // 为0则进行resize()数组, 并对 n赋值为当前tab的长度
        // resize() 对HashMap的table扩容, 并返回扩容后的新数组
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 2、对 node p 进行赋值, 数组所在位置 即 node p 如果是null 则直接赋值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //3、 p 不为null, 声明 node e, key k
            Node<K,V> e; K k;
            //  3.1、如果hash值相等且key相等, 直接将 e 赋值为当前node的头节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            // 3.2、  如果是红黑树, 则对树进行操作, 返回节点e
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 3.3、 对链表进行遍历, 找到对应的节点
                for (int binCount = 0; ; ++binCount) {
                    // 将 e 赋值为  头节点p的next, 如果下一个节点为null
                    if ((e = p.next) == null) {
                        // 对节点进行赋值
                        p.next = newNode(hash, key, value, null);
                        // 如果长度到达数转换阈值, 则需要转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果e节点的hash相等, key相等, 则 直接跳出循环 e 已经被赋值为 p.next
                    // 3.4、此时e节点的value没有被赋值
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 指针指向下一个节点, 继续遍历
                    p = e;
                }
            }
 
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 对旧值进行覆盖, 并返回旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 4、 是否需要扩容 threshold = 当前容量*0.75
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
 

扩容

final Node<K,V>[] resize() {
    // 旧数组
    Node<K,V>[] oldTab = table;
    // 旧数组长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧的扩容阈值
    int oldThr = threshold;
    // 新的数组长度和新扩容阈值
    int newCap, newThr = 0;
    // 旧数组存在
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新数组长度为旧数组长度的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 5、扩容阈值是旧扩容阈值的2倍
            newThr = oldThr << 1; // double threshold
    }
    // 旧数组不存在, 相当于首次put(K, V)时, 将数组长度置为扩容阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 旧数组不存在, new HashMap()未指定长度, 初次put(K, V), 设置为默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的扩容阈值是0, 则将扩容阈值设置为 新数组长度*负载因子
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 对全局的扩容阈值进行赋值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 创建新数组, 长度为新长度, 即原数组长度的2倍
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将table复制为新数组
    table = newTab;
    if (oldTab != null) {
        // 对旧数组进行遍历
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 旧节点node赋值
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                // 只有头结点, 直接计算新的位置并赋值
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                // 树单独处理
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        // next节点
                        next = e.next;
                        // 节点hash与旧数组长度 & 的结果来决定元素所在位置, 参考上面图示所讲
                        if ((e.hash & oldCap) == 0) {
                            // 在元索引处创建新链表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            // 新索引出创建链表
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        // 索引j处直接赋值
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 索引 j + 老数组长度位置存放hiHead
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
 

4 队列

ArrayBlockingQueue:是一个基于数组结构的有界队列按FIFO原则对元素排序
LinkedBlockingQueue:一个基于链表的阻塞队列按FIFO原则对元素排序,吞吐量高于ArrayBlockingQueue
SynchronousQueue:一个不存储元素得队列,每个插入操作必须等到另一个线程的调用移除,否则插入操作一直处于阻塞状态,吞吐量要高于LinkedBlockingQueue

队列常用API:
在这里插入图片描述
依次对应上面的每一列API
在这里插入图片描述

Executors.newCachedThreadPool() 底层是SynchronousQueue
其他方式创建的是LinkedBlockingQueue

5.LinkedHashMap 数据淘汰

LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序
插入顺序:保存了键值对的插入顺序,当迭代器遍历 LinkedHashMap 时,会按照键值对的插入顺序进行。
访问顺序:保存了键值对的访问顺序,当迭代器遍历 LinkedHashMap 时,会按照键值对最后被访问的顺序进行

手写LRU

public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private int capacity;

    public LRUCache(int capacity) {
    //如果为true,就是数据按访问顺序,最近访问的在尾部
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }
	//删除头部元素
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
	//测试
    public static void main(String[] args) {
        LRUCache<String, String> lruCache = new LRUCache<>(3);
        lruCache.put("a", "a");
        lruCache.put("b", "b");
        lruCache.put("c", "c");
        lruCache.put("d", "d");

        lruCache.forEach((k, v) -> {
            System.out.println(k + "," + v);
        });

    }
}

6.HashMap如何降低Hash冲突概率?

计算key的hash值 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
寻址 (n - 1) & hash

① key为空则取0
② 如果key不为空 则把key的hash值 与 hash值无符号右移16位 做异或运算 得到结果 异或操作为0或1的几率为各50%,计算的值更加散列
③ 寻址的过程是 table的长度减1 再与hash进行 与运算

hashMap允许key或value为null,concurrentHashMap不允许key和value为null

7.jdk1.7 hashMap出现扩容产生死循环问题?

// 把hashMap的元素迁移到新的table

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
	//判断每个下标对应存放的链表是否为空
        while(null != e) {
		//如果table的位置有元素,那就使用头插法,把元素放在该位置
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

HashMap扩容导致死循环的主要原因在于扩容过程中使用头插法将oldTable中的单链表中的节点插入到newTable的单链表中, 所以newTable中的单链表会倒置oldTable中的单链表。那么在多个线程同时扩容的情况下就可能导致扩容后的HashMap中存在一个有环的单链表,从而导致后续执行get操作的时候,会触发死循环,引起CPU的100%问题。所以一定要避免在并发环境下使用HashMap
扩容之后的newTable中的单链表形成了一个环,后续执行get操作的时候,会触发死循环,引起CPU的100%问题

HashMap死循环解决方案:

1.使用线程安全的ConcurrentHashMap替代HashMap,推荐

2.使用线程安全的容器Hashtable替代,性能低,不建议

3.使用synchronized或Lock加锁,会影响性能,不建议

8.为什么重写Equals还要重写HashCode方法?

如果两个对象的Hashcode值相等的情况下,对象的内容值不一定相等;,例如 hash碰撞问题
如果使用equals方法比较两个对象内容值相等的情况下,则两个对象的Hashcode值相等

注意:equals方法默认的情况下Object类中采用==比较对象的内存地址是否相等。

9.jdk8的concurrentHashMap底层结构实现线程安全

CAS 在没有hash冲突时,Node放在数组上 U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v)

synchronized 在出现hash冲突时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神雕大侠mu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值