Java集合、多线程、反射和Spring框架总结,源码解析

一、集合 - 通过不同的数据结构存储以及操作数据的工具

1.1 Collection

1.1.1 ArrayList、Vector

1.1.1.1 底层原理

ArrayList和Vector底层都是由动态数组实现的

1.1.1.2 ArrayList VS Vector

ArrayList是线程不安全的集合,而Vector是线程安全的集合。Vector本质是JDK1.0的产物,但是集合体系是JDK1.2才推出的新特性。因此,JDK1.2之后sun公司强行的让Vector类实现了List接口,从而导致Vector之中有很多功能重复的方法。虽然现在为止Vector都没有过时,但是基本上已经不再使用Vector集合,哪怕是多线程环境,也不推荐直接使用Vector来保证线程安全。

1.1.1.3 什么是动态数组?

本质上数组是不能动态的,因为Java中数组一旦初始化好之后,就不能再改变数组长度。但是ArrayList和Vector底层是通过创建新的数组的方式,来达到数组动态扩展的目的。这种动态"改变"数组长度的方式,称之为动态数组




java中下标越界代码 java下标越界怎么解决_java怎么判断数组下标越界


1.1.1.4 源码解析

ArrayList - 构造方法

构造方法中就是将一个长度为空的数组,赋值给一个Object数组的引用(elementData)。JDK1.8之后,ArrayList初始化时,不再默认的初始化一个长度为10的数组(懒加载)。

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};transient Object[] elementData;..../** * 构造方法 - 初始化底层的数组  */public ArrayList() {   this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

add - 添加元素

public boolean add(E e) {   //判断当前底层的数组是否需要扩容,如果需要扩容则调用grow方法,进行扩容   ensureCapacityInternal(size + 1);    //将元素设置到数组size的位置,并且size自增   elementData[size++] = e;   return true;}//核心扩容方法,参数minCapacity表示当前最小需要扩容的容量private void grow(int minCapacity) {   //获得旧的数组容量   int oldCapacity = elementData.length;   //计算新的数组容量,根据旧的容量1.5倍扩容   //右移一位相当于除以2,左移一位相当于乘以2   int newCapacity = oldCapacity + (oldCapacity >> 1);   //如果新容量没有达到最小容量的要求,则直接用最小容量顶替新容量   if (newCapacity - minCapacity < 0)       newCapacity = minCapacity;   if (newCapacity - MAX_ARRAY_SIZE > 0)       newCapacity = hugeCapacity(minCapacity);   //数组扩容   //Arrays.copyOf表示将根据参数二(newCapacity)的大小,创建一个新的空数组,并且将参数一(elementData)中的元素,全部拷贝过去,并且返回新数组   elementData = Arrays.copyOf(elementData, newCapacity);}

add - 插入(往中间添加)元素的方法

//插入元素element到下标为index的位置public void add(int index, E element) {  //检测index下标是否越界    rangeCheckForAdd(index);  //判断是否需要扩容    ensureCapacityInternal(size + 1);  //做一个index位置的元素整体后移,空出index位置来    System.arraycopy(elementData, index, elementData, index + 1,                         size - index);  //将新的元素放到index位置处    elementData[index] = element;  //元素数量加1    size++;}

**注意:**在ArrayList中,记性数组扩容或者元素移位时,底层都是调用的native方法实现,native本身没有方法体,方法实现是由C/C++实现的,以此来提高扩容的效率。

1.1.2 LinkedList

1.1.2.1 底层原理

Linked底层是由双向链表实现

1.1.2.2 什么是链表?什么是双向链表?

链表是一种非常常见的线性数据结构(和数组类似),由一个一个的节点组成,每个节点都有一个指针,指向链表的下一个节点,因为有"指针"的存在,所以链表在内存空间上可以地址不连续,因此链表没有长度的限制(数组在内存空间上地址必须连续,长度有限)。

双向链表就是普通链表的节点多了一个指向上一个节点的指针


java中下标越界代码 java下标越界怎么解决_ci_02


1.1.2.3 Java如何实现一个双向链表?

链表的关键其实就是节点,链表由一个一个节点组成,指向实现了节点的结构,那么链表就能实现。

链表节点的实现:

//LinkedList中底层节点的实现private static class Node {        //数据部分,存储节点的元素        E item;        //尾部指针,指向下一个节点        Node next;        //头部指针,指向上一个节点        Node prev;        Node(Node prev, E element, Node next) {            this.item = element;            this.next = next;            this.prev = prev;        }}

1.1.2.4 源码解析

LinkedList的一些基本属性

//first在LinkedList中永远指向链表的头节点,如果没有元素时,就为nulltransient Node first;//last在LinkedList中永远指向链表的尾节点,如果没有元素时,就为nulltransient Node last;

add添加元素的方法

//添加元素public boolean add(E e) {    //调用该方法添加元素到链表的尾部    linkLast(e);    return true;}//添加元素e到链表的尾部void linkLast(E e) {    //让l指向尾节点,第一次添加时,因为没有任何节点,所以l和last都是null    final Node l = last;    //将新元素封装成新节点,并且新节点的头指针指向l    final Node newNode = new Node<>(l, e, null);    //再将尾指针指向新节点    last = newNode;    //判断l是否为null,如果为null,表示新节点就是第一个节点    if (l == null)      //first再指向新节点         first = newNode;    else      //l节点的尾指针指向新节点        l.next = newNode;    size++;    modCount++;}

get获取元素的方法

//获取下标index除外的元素public E get(int index) {    //检查下标越界    checkElementIndex(index);    //找到index位置的节点,获得节点的数据部分    return node(index).item;}//node方法,返回index处的节点对象Node node(int index) {        //判断查找的下标是靠前还是靠后        if (index < (size >> 1)) {            //如果靠前,就从头节点开始,依次遍历往后查找            Node x = first;            for (int i = 0; i < index; i++)                x = x.next;            return x;        } else {            //如果靠后,就从尾节点开始,依次遍历往前查找            Node x = last;            for (int i = size - 1; i > index; i--)                x = x.prev;            return x;        }}

add插入(从中间添加)元素的方法

//插入元素到index下标的位置public void add(int index, E element) {        //检查下标是否越界        checkPositionIndex(index);        if (index == size)            //说明当前的插入位置为末尾,直接调用追加元素的方法            linkLast(element);        else            //插入元素到指定节点之前            linkBefore(element, node(index));}....//插入数据e到succ节点的前面    void linkBefore(E e, Node succ) {        final Node pred = succ.prev;        final Node newNode = new Node<>(pred, e, succ);        succ.prev = newNode;        if (pred == null)            first = newNode;        else            pred.next = newNode;        size++;        modCount++;}

1.1.4 ArrayList VS LinkedList

插入性能对比:**尾部:**ArrayList和LinkedList性能相近**中间:**ArrayList的速度 >> LinkedList的速度,ArrayList虽然存在移位,但是底层通过C++直接操作内存的方式进行了优化,而LinkedList每次插入都需要通过遍历的方式找到元素,所以性能有所下降。**头部:**LinkedList的速度 >> ArrayList的速度,ArrayList往头部插入元素,也就意味着每次都需要位移整个数组,虽然有优化,但是也不等于没有耗时,但是LinkedList查找靠前/后的元素速度很快,同时插入性能也很快,所以相对ArrayList性能更优

查询性能对比:**尾部:**ArrayList和LinkedList性能相近**中间:**ArrayList >>>>>>>> LinkedList**头部:**ArrayList和LinkedList性能相近

1.1.3 HashSet、LinkedHashSet、TreeSet

Set各个实现类的特点:HashSet:无序、不可重复**LinkedHashSet:**不可重复,但是元素有序(插入顺序)**TreeSet:**不可重复,但是元素有序(字典顺序)

底层实现:

底层都是通过对应的Map集合实现

1.2 Map

1.2.1 HashMap、Hashtable

1.2.1.1 底层原理

HashMap和Hashtable底层都是由哈希表实现的,Hashtable和HashMap的关系与Vector和ArrayList之间的关系类似,Hashtable线程安全HashMap线程不安全

1.2.1.2 哈希表(重要)

什么是哈希表?

哈希表是一种用于快速树索数据结构,精准查询(通过key查询value)效率极高,和元素的数量(理想型,实际过程中,多少还是有点关系)无关,所以时间复杂度为O(1)


java中下标越界代码 java下标越界怎么解决_java怎么判断数组下标越界_03


什么是哈希碰撞(哈希冲突)?

哈希碰撞不是一件好事,但是不可避免,所以任何一张好的哈希表必须对哈希碰撞有良好的解决方案。

所谓的哈希碰撞就是指,不同的key,通过哈希函数,计算出的下标相同了。

哈希碰撞的解决方式(HashMap的解决方案)

链地址法:将发生碰撞的元素通过链表连接起来
(JDK1.8之后,是通过
链表 + 红黑树的方式解决的哈希碰撞)

哈希表的扩容(底层数组的扩容)

一个哈希表,哈希碰撞是不可避免的,如果频繁的发生哈希碰撞,那么会导致大量的链表生成,对查询性能影响很严重。因此哈希表必须通过适当的扩容,来降低哈希碰撞发生的概率,以及优化查询性能。

扩容的好处:1、降低后续发生哈希碰撞的概率2、打散现有的碰撞的链表

什么时候扩容?

哈希表有一个参数,称之为填充因子(加载因子,HashMap默认为0.75),当添加的元素数量/数组长度时,一旦达到了填充因子的比例,就会触发一次扩容。数组长度(100) * 填充因子(0.75) = 扩容阈值(75)

如果碰到一些精准定位、去重、判断是否存在等诸如此类的问题,都可以先考虑一下哈希表或者哈希表的一些变种结构(布隆过滤器)

1.2.1.3 红黑树(简单介绍)

什么是红黑树?

红黑树本身是一种特殊的二叉树,是用于快速查询数据结构

什么是二叉搜索树?

在一颗二叉树中,任意节点的左子树节点都比当前节点要小,右子树节点都比当前节点要大,那么这颗二叉树就称之为二叉搜索树

二叉搜索树的缺点:如果插入的元素有一定的顺序,那么就可以导致树的失衡,从而严重降低树的查询能力

红黑树就是为了解决二叉搜索树,失衡问题而设计的一颗二叉搜索平衡树


java中下标越界代码 java下标越界怎么解决_链表_04


1.2.1.4 HashMap的源码解析(JDK1.8)

HashMap中的一些属性设置

//默认的哈希表容量(数组长度)- 16    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;     //哈希表的最大容量(最大的数组长度) - 2^30    static final int MAXIMUM_CAPACITY = 1 << 30;    //默认的填充因子 - 0.75    static final float DEFAULT_LOAD_FACTOR = 0.75f;    //链表转成树的默认长度    static final int TREEIFY_THRESHOLD = 8;    //树转回链表的默认长度    static final int UNTREEIFY_THRESHOLD = 6;    //当哈希表的容量达到该值时,才会发生链表转树的情况    static final int MIN_TREEIFY_CAPACITY = 64;    //当前的扩容阈值 - 当哈希表的元素数量达到这个阈值时,就会触发扩容 阈值 = 容量 * 填充因子    int threshold;    //当前有效的填充因子值    final float loadFactor;    //底层的哈希表transient Node[] table;

HashMap的构造方法

//无参构造public HashMap() {    //设置当前的填充因子为默认填充因子    this.loadFactor = DEFAULT_LOAD_FACTOR; }//2个参数的构造方法//参数1 - 哈希表的初始化容量//参数2 - 自定义填充因子public HashMap(int initialCapacity, float loadFactor) {    if (initialCapacity < 0)            throw new IllegalArgumentException("Illegal initial capacity: " +                                               initialCapacity);    if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;    if (loadFactor <= 0 || Float.isNaN(loadFactor))            throw new IllegalArgumentException("Illegal load factor: " +                                               loadFactor);    this.loadFactor = loadFactor;    //根据自定义的容量设置扩容阈值    this.threshold = tableSizeFor(initialCapacity);}

HashMap元素的节点类

//一个key-value就对应一个Node对象static class Node implements Map.Entry {        final int hash;//当前key的哈希值        final K key;//key        V value;//value        Node next;//指向下一个节点的引用 - 链表        Node(int hash, K key, V value, Node next) {            this.hash = hash;            this.key = key;            this.value = value;            this.next = next;        }      ...}

HashMap的扩容方法 - resize()

HashMap构造方法并不会初始化底层的哈希表(数组),所以第一次添加元素时,一定会触发一次resize方法扩容,这次扩容的目的是为了初始化哈希表

/*哈希表扩容的方法如果是第一次添加元素,则该方法就会初始化哈希表*/final Node[] resize() {        //将旧的哈希表赋值给oldTab变量,如果是第一次添加元素,table必然为null        Node[] oldTab = table;        //计算旧的哈希表容量,如果是第一次添加元素,旧哈希表的容量也必然为0        int oldCap = (oldTab == null) ? 0 : oldTab.length;        //当前的拓展阈值 容量(16) * 加载因子(0.75f) = 12    //第一次添加时,oldThr threshold 也是为0        int oldThr = threshold;        //newCap - 扩容后新的哈希表容量        //newThr - 扩容后新的扩展阈值        int newCap, newThr = 0;            //判断旧的哈希表容量是否大于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)                //新的扩展阈值也设置为旧阈值的2倍                newThr = oldThr << 1; // double threshold        }        else if (oldThr > 0) // initial capacity was placed in threshold            newCap = oldThr;        else {                    //说明当前哈希表还未被初始化            //将默认的哈希表长度16 设置为newCap属性            newCap = DEFAULT_INITIAL_CAPACITY;            //计算新的扩展阈值 16 * 0.75 = 12            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);        }            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"})        Node[] newTab = (Node[])new Node[newCap];        //将新的哈希表赋值给全局的table变量,后续所有对table的操作,其实就是在操作新的哈希表了        table = newTab;            //判断是否存在旧的哈希表,如果是第一次添加元素,那么oldTable必然为null,resize方法就到此为止了。如果不是第一次添加元素,那么就会存在旧的哈希表,就需要对旧的哈希表进行重新计算        if (oldTab != null) {            //重新计算旧哈希表的每个元素,赋值到新的哈希表中 ?????????                        //循环旧的哈希表中的各个哈希桶            for (int j = 0; j < oldCap; ++j) {                Node e;                if ((e = oldTab[j]) != null) {                    //当前j位置的哈希桶中有元素节点,将该元素节点赋值给e变量                    //将桶j的位置设置为null                    oldTab[j] = null;                    if (e.next == null)                        //说明当前e对应的数据节点只有一个节点(不是链表也不是树)                        //通过重新计算哈希位置,将节点e放入新的哈希表中                        newTab[e.hash & (newCap - 1)] = e;                    else if (e instanceof TreeNode)                        //当前节点e是一颗红黑树,直接走红黑树重新设置的方法                        ((TreeNode)e).split(this, newTab, j, oldCap);                    else {                         //当前节点e是一个链表,将链表进行高位和低位的拆分,重新放入到新的哈希表中,相当于将原来的一个链表,打散成了2个链表                        Node loHead = null, loTail = null;                        Node hiHead = null, hiTail = null;                        Node next;                        do {                            next = e.next;                            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;                            newTab[j] = loHead;                        }                        if (hiTail != null) {                            hiTail.next = null;                            newTab[j + oldCap] = hiHead;                        }                    }                }            }        }        return newTab;}

设置key-value的方法 - put方法

//给哈希表设置key - value//hash(key) - 根据key计算哈希值 -> 在哈希表中的下标public V put(K key, V value) {   return putVal(hash(key), key, value, false, true);}//HashMap底层的哈希函数 - 将任意类型的key转成一个int类型的哈希值(现在还没有落在哈希表的范围内)static final int hash(Object key) {   int h;   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}//实际添加元素的方法//参数1-hash:通过key计算的哈希值//参数2-key:保存的key//参数3-value:保存的value//参数4-onlyIfAbsent:用来确定key相同时,value是否覆盖,false表示覆盖,true表示不覆盖final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {        //初始化一些临时变量        //tab - 当前的哈希表        //p - 当前要保存的key-value对应的哈希桶的中的元素        //n - 当前哈希表的容量        //i - key-value计算出来的哈希表的下标        Node[] tab; Node p; int n, i;        //获得全局哈希表,判断哈希表是否为空(没有初始化过),意味着本次添加元素是第一次添加        if ((tab = table) == null || (n = tab.length) == 0)            //调用resize方法进行初始化            //将初始化的哈希表赋值给tab变量            //在计算新的哈希表的长度,赋值给n变量            n = (tab = resize()).length;            //(n - 1) & hash - 通过key的哈希值,计算哈希表的下标 !!!!!!!!        //任意类型的int值 & x 得到的结果一定是0 ~ x范围内        //计算出下标位置后,赋值给i变量        if ((p = tab[i = (n - 1) & hash]) == null)            //恭喜你没有发生哈希碰撞           //思考:为什么这里要调用newNode方法,在方法里初始化Node对象,而不是直接初始化Node对象            tab[i] = newNode(hash, key, value, null);            //tab[i] = new Node(hash, key, value, null); 为什么不这么写????        else {            //很遗憾,本次添加发生了哈希碰撞                            //临时变量e - 当前临时的节点,用于后续的计算            //临时变量k - 当前发生碰撞的节点p的key值            Node e; K k;            //判断添加的元素key和碰撞的元素key是否相等            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                //如果key相同,则将碰撞的节点p设置给临时变量e                e = p;            else if (p instanceof TreeNode)                //当前桶i的位置是一颗红-黑树                //执行红黑树添加节点的方法,如果返回null,就表示节点正常添加到红黑树中                //如果返回的节点不为null,说明添加的数据和红黑树中某个子节点key相同了                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);            else {                //当前桶i的位置是一个链表(长度有可能为1)                               //循环当前哈希桶位置的整个链表                //变量binCount代表当前链表的                for (int binCount = 0; ; ++binCount) {                    if ((e = p.next) == null) {                        //将添加的数据封装成Node节点对象,插入到链表的末端(尾插)                        p.next = newNode(hash, key, value, null);                        //如果当前链表的长度达到了TREEIFY_THRESHOLD值                        if (binCount >= TREEIFY_THRESHOLD - 1)                            //可能将该桶的链表转成红-黑树或者进行哈希表的2倍扩容                            treeifyBin(tab, hash);                        break;                    }                    //找到了链表中key和添加元素key相同的节点,跳出循环直接走value覆盖逻辑                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = e;                }            }                        //如果e不为空,就执行value覆盖的代码,具体覆盖的逻辑需要结合参数onlyIfAbsent            if (e != null) {                V oldValue = e.value;                if (!onlyIfAbsent || oldValue == null)                    e.value = value;                afterNodeAccess(e);                return oldValue;            }        }        ++modCount;        //key-value已经添加到哈希表中        //++size哈希表的元素数量+1        //当前添加的元素数量超过了扩展阈值(容量 * 加载因子),当前需要扩容        if (++size > threshold)            //执行扩容方法            resize();        afterNodeInsertion(evict);        return null;}//tab哈希表的hash值对应的桶的链表转换成红黑树的方法final void treeifyBin(Node[] tab, int hash) {        int n, index; Node e;        //如果哈希表为空,或者哈希表的容量没有达到转树的最小容量(64)        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)            //这个时候是不会转树的,而是直接进行一次扩容            resize();        else if ((e = tab[index = (n - 1) & hash]) != null) {            //才将桶index位置的链表转换成红-黑树            TreeNode hd = null, tl = null;            do {                TreeNode 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底层是通过先比较两个key的hash值再比较对象的内存地址 || 比较对象的equals方法来判断这两个key是否相等

1.2.2 LinkedHashMap

1.2.2.1 特点

key有序(插入顺序),不可重复

1.2.2.2 底层原理

在HashMap的基础上,新增了一个链表结构(和解决哈希冲突的链表是两回事),这个链表是用来维护元素的插入顺序的。

//每次添加元素时,都会生成一个特殊的节点(除了HashMap里面需要的属性外,额外的多了两个属性,头尾节点的引用)Node newNode(int hash, K key, V value, Node e) {        LinkedHashMap.Entry p =            new LinkedHashMap.Entry(hash, key, value, e);        //将新的元素节点,添加到整个链表的尾部        linkNodeLast(p);        return p;}

**注意:**本质上LinkedHashMap和HashMap的实现方式几乎一致,只是添加元素时,需要额外的去维护一个插入顺序的链表,所以添加元素的性能比HashMap略低,但是正因为有这个链表的存在,所以遍历哈希表的性能反而要高于HashMap

1.2.3 TreeMap

1.2.3.1 特点

key有序(字典排序),不可重复

1.2.3.2 底层原理

没有哈希表,底层完全有红-黑树实现。查询性能和添加性能平均情况都低于HashMap,所以除非真的需要key有顺序,否则我们都应该使用HashMap,而不是TreeMap

**注意:**在TreeMap中判断两个key是否相同的方式,不是用==,也不是用equals方法,而是通过比较器(compare)的方法判断是否返回0,如果比较器返回0了,就表示key相同了。

二、多线程

2.1 什么是线程安全的问题?

多线程环境下,同时并发的操作(增删改)同一份资源时,因为线程的调度不确定性,所以可能导致资源最后的执行状态和理想状态有一定的出入,这种问题就是所谓的线程安全问题

2.2 什么情况下,程序会有线程安全的问题?


java中下标越界代码 java下标越界怎么解决_链表_05


2.3 引发线程安全问题的三个因素

可见性 - 某个线程对一个数据的修改,其他线程是立即可见的

原子性 - 对某个数据的的操作是原子并且不可分割的

有序性 - 程序会按照代码编写的顺序依次执行

在多线程并发操作中,如果以上3个特性有任何一个特性没有得到保证,就会引发线程安全的问题

2.3.1 可见性


java中下标越界代码 java下标越界怎么解决_ci_06


可见性问题的代码:

public class Test1 {    private static boolean flag = true;    public static void main(String[] args) {        //创建一个新的线程        new Thread(() -> {            //            try {                Thread.sleep(5000);            } catch (InterruptedException e) {                e.printStackTrace();            }            flag = false;            System.out.println("flag设置为false!");        }).start();        //死循环        while(flag){        }    }}

因为可见性的原因,导致子线程对flag的修改,主线程不能立即可见,因此还是无限死循环

2.3.2 原子性(大概率线程安全的问题都是由原子性导致)

i = 10; - 原子操作i++; - 非原子操作 x = i+1; | i=x;

public class Test2 {    private static int x = 0;    public static void main(String[] args) {        for (int i = 0; i < 1000; i++) {            new Thread(() -> {                x++;            }).start();            //x=900            //线程1 - x++ -> (901=900+1; 打断 x=901;)            //线程2 - x++ -> (901=900+1; 901=y;)        }        try {            Thread.sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("x当前的结果:" + x);//结果又可能小于1000    }}

因为x++是非原子性的,所以在执行过程中有可能被其他线程打断,从而导致最后的结果一致性出现问题

注意:Java中两个原子性操作,放在一起就不是原子性的了。

2.3.3 有序性

什么是指令重排?

在某些情况下,CPU基于执行效率优化的考虑,可能会改变代码的执行顺序(代码的编写顺序和执行顺序不一样),这种情况就称为指令重排

指令重排的保证:在单线程情况下,重排后的执行结果与顺序执行的执行结果一定一致。

public class Test3 {    private static Object obj = null;    private static boolean flag = true;    public static void main(String[] args) {        //线程1        while(flag){            System.out.println("死循环中....");        }        //调用hashcode方法        obj.hashCode();//不会报空指针异常        //线程2        obj = new Object();        flag = false;    }}

2.4 如何保证线程安全

1)使用volatile关键字

添加了volatile关键字的属性,具有可见性,对该属性的所有修改,其他线程全部立即可见。同时volatile关键字可以保证局部有序性。但是volatile不能保证原子性。是一种相对于加锁来说更加轻量级的解决方案。

2)使用concurrent包下的一些AtomicXxxx类来进行原子操作

AtomicXxxxx类中的方法都是可以保证原子性的,但是两个原子性的方法并不能保证原子性

3)使用AtomicXxxx类来实现CAS(compare and swap)

CAS本质上是基于乐观锁的思想实现的技术,在某些场景下可以帮助我们实现线程安全,比Lock和synchronized加锁的方式会轻量级一些。

public class Test4 {    private static volatile String name;    private static volatile Integer age;    private static volatile        AtomicInteger flag = new AtomicInteger(0);//boolean flag = false;    public  static void main(String[] args) {        //线程1        new Thread(() -> {            //如果flag的值等于第一个参数,就设置为第二个参数,并且返回true,否则不设置,返回false            //这个比较并且设置的过程是具有原子性的            //flag.compareAndSet(false, true)            //设置小明,年龄为16            if (flag.compareAndSet(0, 1)){//true                name = "小明";                age = 16;                System.out.println("线程1设置成功!");            } else {                System.out.println("线程1设置失败!");            }        }).start();        //线程2        new Thread(() -> {            //设置小红,年龄为18            if (flag.compareAndSet(0, 1)){//false                name = "小红";                age = 18;                System.out.println("线程2设置成功!");            } else {                System.out.println("线程2设置失败!");            }        }).start();        //线程3        new Thread(() -> {            //设置小刚,年龄为12            if (flag.compareAndSet(0, 1)){//false                name = "小刚";                age = 12;                System.out.println("线程3设置成功!");            } else {                System.out.println("线程3设置失败!");            }        }).start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(name + " - " + age);    }}

4)synchronized加锁,同时解决可见性、原子性和有序性问题

synchronized加锁具体应该锁什么对象?- 将可能降低锁的细粒度

synchronized修饰方法时,如果是非静态方法,默认锁的是this(当前对象)如果是静态方法,默认锁的是当前类的class对象

5)通过Lock对象加锁

重入锁机制,是可以完全取代synchronized关键字,相对来说比synchronized更加灵活

//重入锁对象        Lock lock = new ReentrantLock();        new Thread(() -> {            //线程1            lock.lock();//加锁            System.out.println("执行线程1的业务!!!");            try {                Thread.sleep(2000);            } catch (InterruptedException e) {                e.printStackTrace();            }            lock.unlock();        }).start();        new Thread(() -> {            //线程1            lock.lock();//加锁            System.out.println("执行线程2的业务!!!");            try {                Thread.sleep(2000);            } catch (InterruptedException e) {                e.printStackTrace();            }            lock.unlock();        }).start();

重入读写锁机制

//重入读写锁        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();//写锁        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();//读锁        new Thread(() -> {            //线程1            writeLock.lock();//加锁            System.out.println("执行线程1的业务!!!");            try {                Thread.sleep(2000);            } catch (InterruptedException e) {                e.printStackTrace();            }            writeLock.unlock();        }).start();        new Thread(() -> {            //线程1            readLock.lock();//加锁            System.out.println("执行线程2的业务!!!");            try {                Thread.sleep(2000);            } catch (InterruptedException e) {                e.printStackTrace();            }            readLock.unlock();        }).start();

读写锁中,读锁 兼容 读锁,读锁 互斥 写锁,写锁 互斥 写锁读写锁特别适合读多写少的业务

6)通过数据库的锁保证线程安全(表锁、行锁[共享锁、排他锁])

7)通过redis的lua脚本实现线程安全

8)通过分布式锁(zookeeper、redis)

2.5 编写一个线程安全的单例模式(懒汉式)

CAS版的线程安全的懒汉式

public class SingleClass {    //私有化构造方法    private SingleClass(){}    private static AtomicReference singleClass     = new AtomicReference<>();    /**     * 返回单例对象     * @return     */    public static SingleClass getInstance(){        singleClass.compareAndSet(null, new SingleClass());        return singleClass.get();    }}

双重锁判定版本

public class SingleClass {    //私有化构造方法    private SingleClass(){}    private volatile static SingleClass singleClass;    /**     * 返回单例对象     *     * 只有第一次创建对象时有线程安全的问题,一旦创建好之后,就没有线程安全问题了     * @return     */    public static SingleClass getInstance(){        //线程2        if (singleClass == null){            synchronized (SingleClass.class) {                if(singleClass == null) {                    //1、申请堆内存地址                    //2、初始化堆内存地址                    //3、将引用指向的内存地址                    singleClass = new SingleClass();                }            }        }        return singleClass;    }}

2.6 集合中线程安全的问题

ArrayList、HashMap等集合线程不安全,体现在什么地方?会带来什么问题?

2.6.1 如何解决多线程下集合安全的问题?

可以使用一些线程安全的集合,比如Vector、Hashtable,底层是通过加锁的方式,保证操作的原子性,以此达到线程安全。但是实际开发过程中,不推荐使用这些集合,因为锁的细粒度太大,导致集合的并发性能很差,JDK1.4之后,提供了JUC(java.util.concurrent)的工具包,JUC包中包含了很多线程安全的集合类,这些类采用了特别的机制来保证线程安全,相对来说比Vector、Hashtable并发性更好。

注意:线程安全的集合,不等于所有对集合的操作都是线程安全。

所谓的线程安全集合,指的是集合的每个方法独立调用时,是线程安全的。但是将这些线程安全的方法组合在一起调用,就不一定是线程安全的了。

2.6.2 CopyOnWriteArrayList类 - ArrayList线程安全的解决方案

CopyOnWriteArrayList底层采用了写入时复制的机制,来保证写数据的线程安全,同时提高读数据的并发性能(所有读的操作是不加锁),特别适合读多写少的场景。但是该类没办法保证数据的强一致性(读的时候有可能读到的是过期的数据),对于某些数据一致性没有绝对要求的场景才能使用,在分布式项目中,往往追求的都是最终一致性(并不会要求强一致),所以CopyOnWriteArrayList在分布式环境下可以使用。如果对数据的一致性要求很高,应该采用List list = Collections.synchronizedList(new ArrayList<>());方式获得线程安全的集合使用。

添加元素的方法 - add(写入时复制)

public boolean add(E e) {        final ReentrantLock lock = this.lock;        lock.lock();        try {            //获得当前底层的数据对象            Object[] elements = getArray();            //获得数组的长度            int len = elements.length;            //基于原来的数组,复制一个长度+1的新数组            Object[] newElements = Arrays.copyOf(elements, len + 1);            //将新元素写入新数组中            newElements[len] = e;            //将新的数组赋值给全局变量,以后读取的就是这个新数组了            setArray(newElements);            return true;        } finally {            lock.unlock();        }}

读取元素时的方法 - get

//读取的方法没有任何加锁操作public E get(int index) {        return get(getArray(), index);}1234

2.6.3 ConcurrentHashMap - 线程安全的HashMap

ConcurrentHashMap底层数据结构和HashMap完全一样,采用数组 + 链表 + 红黑树的方式实现的哈希表。JDK1.7之前,ConcurrentHashMap采用分段锁的方式保证线程安全JDK1.8之后,采用CAS + synchronized方式,保证线程安全,并发性能相对于1.7之前会更高

添加元素的方法 - put(key,value)

//添加元素时的方法final V putVal(K key, V value, boolean onlyIfAbsent) {    //key和value不允许为null    if (key == null || value == null) throw new NullPointerException();    //通过key,计算hash值(后续通过hash值计算哈希桶的位置)    int hash = spread(key.hashCode());    //计数器    int binCount = 0;    //死循环 - ??????????? 自旋    //tab指向当前的哈希表(底层数组)    for (Node[] tab = table;;) {        //声明一些局部变量        //f - 当前key对应的哈希桶的第一个节点对象        //n - 当前哈希表的容量(数组的长度)        //i - key计算出来的哈希桶的位置        //fh - 当前桶的第一个元素f的状态位        Node f; int n, i, fh;        //判断哈希表是否为空(如果未空,表示还没初始化,需要初始化哈希表)        if (tab == null || (n = tab.length) == 0)            //初始化哈希表,返回新的哈希表,赋值给tab变量            tab = initTable();                //i - 根据key的哈希值和哈希表的容量,计算的哈希桶的位置        //tabAt(tab, i = (n - 1) & hash)) - 可以理解为tab[i]        //因为tab有可见性,但是tab中的节点并不具备可见性,        //方法tabAt(tab, i = (n - 1) & hash))可以直接根据内存地址去内存中读取该位置的节点(可加性)        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {            //能进入该代码块,说明当前没有发生哈希碰撞,直接将节点设置到指定桶位置            if (casTabAt(tab, i, null, new Node(hash, key, value, null)))                break;                       }        //如果桶i的位置有节点元素,就说明发生了碰撞        //fh赋值为桶i的头节点f的哈希值,但是如果头节点f的哈希值为-1        //表示当前的哈希桶i的位置的所有节点正在参与扩容迁移        else if ((fh = f.hash) == MOVED)            //当前线程会帮助扩容线程进行桶i位置的元素转移            //转移完成后,获得扩容后新的哈希表            tab = helpTransfer(tab, f);        else {            //说明当前哈希桶i位置有节点,发生了哈希碰撞,然后桶i位置的节点也没有进行迁移            //尝试将key-value放入哈希桶i的位置            V oldVal = null;            synchronized (f) {                if (tabAt(tab, i) == f) {                    //如果fh(f节点的哈希值)>0,表示当前哈希桶是一个链表                    if (fh >= 0) {                        binCount = 1;                        for (Node 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 pred = e;                            if ((e = e.next) == null) {                                //找到尾节点,进行尾插                                pred.next = new Node(hash, key,                                                          value, null);                                break;                            }                        }                    }                    //如果f instanceof TreeBin,表示当前哈希桶是一颗红-黑树                    else if (f instanceof TreeBin) {                        Node p;                        binCount = 2;                        if ((p = ((TreeBin)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

//初始化哈希表//线程1 - initTable//线程2 - initTableprivate final Node[] initTable() {    //tab - 当前哈希表的引用    //sc - 状态值    Node[] tab; int sc;    //循环??????- 自旋    while ((tab = table) == null || tab.length == 0) {        //判断sizeCtl(默认为0)状态值,赋给sc变量        if ((sc = sizeCtl) < 0)            //如果sizeCtl<0,说明有人改过这个状态值,可能其他线程已经进来初始化哈希表了            //当前线程直接让步            Thread.yield();        //else 表示没有人修改过sizeCtl变量,那么当线下就修改并且开始进行初始化操作        //U.compareAndSwapInt(this, SIZECTL, sc, -1) - CAS(具有原子性)        //如果变量sizeCtl和sc值相等,那么就给sizeCtl变量赋值为-1,并且整个方法返回true        //如果不相等,就不赋值,并且返回false        //简单来说,这段代码的意思,就是为了保证一定只有一个线程能将sizeCtl变量赋值为-1,一定只有一个线程进入else代码块        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")                    //初始化长度为n的哈希表                    Node[] nt = (Node[])new Node,?>[n];                    table = tab = nt;                    sc = n - (n >>> 2);                }            } finally {                sizeCtl = sc;            }            break;        }    }    //返回哈希表    return tab;}

三、反射

什么是反射?

反射就是在运行时获得动态解析类的能力。

简单来说,反射可以帮助我们创建对象,获得对象的属性,方法…

为什么我们需要通过反射创建对象、获取属性、调用方法?

在实际架构设计过程中,很多时候,我们需要设计一种更为通用的方法来处理某些问题,但是我们可能并不能确定很多对象的类型,这时传统的创建对象、获取属性、调用方法就行不通了,那么就可以通过反射来编写通用的代码解决问题。

架构师,功能 -> 对象转换成json字符串

/**     * 对象转json的方法 - 使用反射通用实现(简单版本)     * @return     */    private static String obj2Json(Object obj){        StringBuilder sb = new StringBuilder();        sb.append("{");        //从对象中获取所有属性,拼接成一个json字符串        Class> clazz = obj.getClass();        //通过反射的class对象获得对象中的所有属性        Arrays.stream(clazz.getDeclaredFields()).filter(field -> {            //判断当前属性是否有NoJSON注解            boolean has = field.isAnnotationPresent(NoJson.class);            return !has;        }).forEach(field -> {            //给属性授权,可以操作私有            field.setAccessible(true);            //获取所有属性名称            String fielgName = field.getName();            //获得所有的属性值            Object fieldValue = null;            try {                fieldValue = field.get(obj);            } catch (IllegalAccessException e) {                e.printStackTrace();            }            if (sb.length() != 1){                sb.append(",");            }            //封装json            sb.append(""" + fielgName +"":").append(""").append(fieldValue).append(""");        });        sb.append("}");        return sb.toString();    }

工作中运用反射解决的问题


java中下标越界代码 java下标越界怎么解决_java怎么判断数组下标越界_07


四、Spring框架

4.1 IOC和AOP

什么是IOC?

控制反转 - 将对象创建的权利交给第三方容器(Spring容器),简单点的理解,就是让容器给你New对象,你自己不要new对象了。

为什么需要IOC容器帮助我们new对象?我们自己new对象有啥不好?

解耦 - 解耦能带来什么好处?????

实际开发过程中,当A类直接new B类的对象,我们称之为硬编码耦合。如果随着业务的升级迭代,B类的构造形式有可能发生变化(比如需要构造代理对象),因为B类的构造方式变化了,就必须去A类中修改B对象初始化的代码,如果很多地方new B类的对象,就需要改很多地方,程序的维护性、拓展性就严重下降,Spring的IOC就是为了解除这种耦合性。

什么是AOP?

AOP表示面向切面编程,AOP是一种思想,而Spring是实现了这种思想的框架。对于很多业务来说,通常可以分为核心业务和非核心业务两部分。每个业务的核心业务都不相同,但是很多业务的非核心业务部分却是有着相同的功能(开启事务、回收资源、记录日志…),将这些非核心业务的部分抽取出来,形成一个个所谓的切面,针对这些切面的编程方式,就是所谓的面向切面编程

AOP的底层实现

核心原理:动态代理


java中下标越界代码 java下标越界怎么解决_java中下标越界代码_08


4.2 猜测一下,Spring IOC实现的基本流程


java中下标越界代码 java下标越界怎么解决_数组_09


4.3 实际SpringIOC的结构流程图(简化)


java中下标越界代码 java下标越界怎么解决_java中下标越界代码_10


AnnotationConfigApplicationContext构造方法

//注解的配置解析器 //解析类上@ComponentScan("com.qf") this.reader = new AnnotatedBeanDefinitionReader(this); //解析包字符串进行扫包的 this.scanner = new ClassPathBeanDefinitionScanner(this); ..... //父类的构造方法中调用,初始化BeanFactory this.beanFactory = new DefaultListableBeanFactory(); 12345678910

scan 扫描包路径的方法

BeanDefinitionRegistry registry; // - 这个对象用来将BeanDefinition对象注册到BeanFactory中 doScan(basePackages); 123

doScan 扫描包路径生成对应的Bean的BeanDefinition对象

protected Set doScan(String... basePackages) {   Assert.notEmpty(basePackages, "At least one base package must be specified");   Set beanDefinitions = new LinkedHashSet<>();   for (String basePackage : basePackages) {      //根据包路径扫描包下所有的Bean对象,生成对应的BeanDefinition对象        Set candidates = findCandidateComponents(basePackage);      for (BeanDefinition candidate : candidates) {         ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);         candidate.setScope(scopeMetadata.getScopeName());         String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);         if (candidate instanceof AbstractBeanDefinition) {            postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);         }         if (candidate instanceof AnnotatedBeanDefinition) {            AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);         }         if (checkCandidate(beanName, candidate)) {            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);            definitionHolder =                  AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);            beanDefinitions.add(definitionHolder);            registerBeanDefinition(definitionHolder, this.registry);         }      }   }   return beanDefinitions;}

refresh方法

@Overridepublic void refresh() throws BeansException, IllegalStateException {   synchronized (this.startupShutdownMonitor) {      //准备阶段      prepareRefresh();      //获得前面阶段初始化过的BeanFactory对象(bean工厂)      //Bean工厂 -> Map      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();      //准备BeanFacotory对象,就是给Bean工厂设置一些属性      prepareBeanFactory(beanFactory);      try {         //后置处理器的空方法,留给以后拓展使用,暂时没有作用         postProcessBeanFactory(beanFactory);         //重要!!!!         //调用BeanFactoryPostProcessor后置处理器自定义处理Bean工厂         invokeBeanFactoryPostProcessors(beanFactory);         //重要!!!         //找到内部的和自定义的BeanPostProcess对象,添加到BeanFactory的list集合中保存起来         //List beanPostProcessors = new CopyOnWriteArrayList<>();         //现在并没有执行          registerBeanPostProcessors(beanFactory);         //初始化一些消息设置(比如国际化)         initMessageSource();         //初始化容器事件广播器         initApplicationEventMulticaster();         // Initialize other special beans in specific context subclasses.         onRefresh();         //注册各种监听         registerListeners();         //重要!!!!          //初始化所有的Bean对象(单例)         finishBeanFactoryInitialization(beanFactory);         // Last step: publish corresponding event.         finishRefresh();      }      catch (BeansException ex) {         if (logger.isWarnEnabled()) {            logger.warn("Exception encountered during context initialization - " +                  "cancelling refresh attempt: " + ex);         }         // Destroy already created singletons to avoid dangling resources.         destroyBeans();         // Reset 'active' flag.         cancelRefresh(ex);         // Propagate exception to caller.         throw ex;      }      finally {         // Reset common introspection caches in Spring's core, since we         // might not ever need metadata for singleton beans anymore...         resetCommonCaches();      }   }}ostProcessor后置处理器自定义处理Bean工厂         invokeBeanFactoryPostProcessors(beanFactory);         //重要!!!         //找到内部的和自定义的BeanPostProcess对象,添加到BeanFactory的list集合中保存起来         //List beanPostProcessors = new CopyOnWriteArrayList<>();         //现在并没有执行          registerBeanPostProcessors(beanFactory);         //初始化一些消息设置(比如国际化)         initMessageSource();         //初始化容器事件广播器         initApplicationEventMulticaster();         // Initialize other special beans in specific context subclasses.         onRefresh();         //注册各种监听         registerListeners();         //重要!!!!          //初始化所有的Bean对象(单例)         finishBeanFactoryInitialization(beanFactory);         // Last step: publish corresponding event.         finishRefresh();      }      catch (BeansException ex) {         if (logger.isWarnEnabled()) {            logger.warn("Exception encountered during context initialization - " +                  "cancelling refresh attempt: " + ex);         }         // Destroy already created singletons to avoid dangling resources.         destroyBeans();         // Reset 'active' flag.         cancelRefresh(ex);         // Propagate exception to caller.         throw ex;      }      finally {         // Reset common introspection caches in Spring's core, since we         // might not ever need metadata for singleton beans anymore...         resetCommonCaches();      }   }}