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底层是通过创建新的数组的方式,来达到数组动态扩展的目的。这种动态"改变"数组长度的方式,称之为动态数组
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 什么是链表?什么是双向链表?
链表是一种非常常见的线性数据结构(和数组类似),由一个一个的节点组成,每个节点都有一个指针,指向链表的下一个节点,因为有"指针"的存在,所以链表在内存空间上可以地址不连续,因此链表没有长度的限制(数组在内存空间上地址必须连续,长度有限)。
双向链表就是普通链表的节点多了一个指向上一个节点的指针
1.1.2.3 Java如何实现一个双向链表?
链表的关键其实就是节点,链表由一个一个节点组成,指向实现了节点的结构,那么链表就能实现。
链表节点的实现:
//LinkedList中底层节点的实现
private static class Node<E> {
//数据部分,存储节点的元素
E item;
//尾部指针,指向下一个节点
Node<E> next;
//头部指针,指向上一个节点
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
1.1.2.4 源码解析
LinkedList的一些基本属性
//first在LinkedList中永远指向链表的头节点,如果没有元素时,就为null
transient Node<E> first;
//last在LinkedList中永远指向链表的尾节点,如果没有元素时,就为null
transient Node<E> last;
add添加元素的方法
//添加元素
public boolean add(E e) {
//调用该方法添加元素到链表的尾部
linkLast(e);
return true;
}
//添加元素e到链表的尾部
void linkLast(E e) {
//让l指向尾节点,第一次添加时,因为没有任何节点,所以l和last都是null
final Node<E> l = last;
//将新元素封装成新节点,并且新节点的头指针指向l
final Node<E> 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<E> node(int 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;
}
}
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<E> succ) {
final Node<E> pred = succ.prev;
final Node<E> 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)
什么是哈希碰撞(哈希冲突)?
哈希碰撞不是一件好事,但是不可避免,所以任何一张好的哈希表必须对哈希碰撞有良好的解决方案。
所谓的哈希碰撞就是指,不同的key,通过哈希函数,计算出的下标相同了。
哈希碰撞的解决方式(HashMap的解决方案)
链地址法:将发生碰撞的元素通过链表连接起来
(JDK1.8之后,是通过链表 + 红黑树的方式解决的哈希碰撞)
哈希表的扩容(底层数组的扩容)
一个哈希表,哈希碰撞是不可避免的,如果频繁的发生哈希碰撞,那么会导致大量的链表生成,对查询性能影响很严重。因此哈希表必须通过适当的扩容,来降低哈希碰撞发生的概率,以及优化查询性能。
扩容的好处:
1、降低后续发生哈希碰撞的概率
2、打散现有的碰撞的链表
什么时候扩容?
哈希表有一个参数,称之为填充因子(加载因子,HashMap默认为0.75),当添加的元素数量/数组长度时,一旦达到了填充因子的比例,就会触发一次扩容。
数组长度(100) * 填充因子(0.75) = 扩容阈值(75)
如果碰到一些精准定位、去重、判断是否存在等诸如此类的问题,都可以先考虑一下哈希表或者哈希表的一些变种结构(布隆过滤器)
1.2.1.3 红黑树(简单介绍)
什么是红黑树?
红黑树本身是一种特殊的二叉树,是用于快速查询的数据结构
什么是二叉搜索树?
在一颗二叉树中,任意节点的左子树节点都比当前节点要小,右子树节点都比当前节点要大,那么这颗二叉树就称之为二叉搜索树
二叉搜索树的缺点:如果插入的元素有一定的顺序,那么就可以导致树的失衡,从而严重降低树的查询能力
红黑树就是为了解决二叉搜索树,失衡问题而设计的一颗二叉搜索平衡树
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<K,V>[] 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<K,V> implements Map.Entry<K,V> {
final int hash;//当前key的哈希值
final K key;//key
V value;//value
Node<K,V> next;//指向下一个节点的引用 - 链表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
}
HashMap的扩容方法 - resize()
HashMap构造方法并不会初始化底层的哈希表(数组),所以第一次添加元素时,一定会触发一次resize方法扩容,这次扩容的目的是为了初始化哈希表
final Node<K,V>[] resize() {
//将旧的哈希表赋值给oldTab变量,如果是第一次添加元素,table必然为null
Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将新的哈希表赋值给全局的table变量,后续所有对table的操作,其实就是在操作新的哈希表了
table = newTab;
//判断是否存在旧的哈希表,如果是第一次添加元素,那么oldTable必然为null,resize方法就到此为止了。如果不是第一次添加元素,那么就会存在旧的哈希表,就需要对旧的哈希表进行重新计算
if (oldTab != null) {
//重新计算旧哈希表的每个元素,赋值到新的哈希表中 ?????????
//循环旧的哈希表中的各个哈希桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
else {
//当前节点e是一个链表,将链表进行高位和低位的拆分,重新放入到新的哈希表中,相当于将原来的一个链表,打散成了2个链表
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> 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<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果哈希表为空,或者哈希表的容量没有达到转树的最小容量(64)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//这个时候是不会转树的,而是直接进行一次扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//才将桶index位置的链表转换成红-黑树
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底层是通过先比较两个key的hash值,再比较对象的内存地址 || 比较对象的equals方法来判断这两个key是否相等
1.2.2 LinkedHashMap
1.2.2.1 特点
key有序(插入顺序),不可重复
1.2.2.2 底层原理
在HashMap的基础上,新增了一个链表结构(和解决哈希冲突的链表是两回事),这个链表是用来维护元素的插入顺序的。
//每次添加元素时,都会生成一个特殊的节点(除了HashMap里面需要的属性外,额外的多了两个属性,头尾节点的引用)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(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 什么情况下,程序会有线程安全的问题?
2.3 引发线程安全问题的三个因素
可见性 - 某个线程对一个数据的修改,其他线程是立即可见的
原子性 - 对某个数据的的操作是原子并且不可分割的
有序性 - 程序会按照代码编写的顺序依次执行
在多线程并发操作中,如果以上3个特性有任何一个特性没有得到保证,就会引发线程安全的问题
2.3.1 可见性
可见性问题的代码:
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> singleClass
= new AtomicReference<>();
public static SingleClass getInstance(){
singleClass.compareAndSet(null, new SingleClass());
return singleClass.get();
}
}
双重锁判定版本
public class SingleClass {
//私有化构造方法
private SingleClass(){}
private volatile static SingleClass singleClass;
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);
}
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<K,V>[] tab = table;;) {
//声明一些局部变量
//f - 当前key对应的哈希桶的第一个节点对象
//n - 当前哈希表的容量(数组的长度)
//i - key计算出来的哈希桶的位置
//fh - 当前桶的第一个元素f的状态位
Node<K,V> 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<K,V>(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<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;
}
}
}
//如果f instanceof TreeBin,表示当前哈希桶是一颗红-黑树
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
//初始化哈希表
//线程1 - initTable
//线程2 - initTable
private final Node<K,V>[] initTable() {
//tab - 当前哈希表的引用
//sc - 状态值
Node<K,V>[] 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<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
//返回哈希表
return tab;
}
三、反射
什么是反射?
反射就是在运行时获得动态解析类的能力。
简单来说,反射可以帮助我们创建对象,获得对象的属性,方法…
为什么我们需要通过反射创建对象、获取属性、调用方法?
在实际架构设计过程中,很多时候,我们需要设计一种更为通用的方法来处理某些问题,但是我们可能并不能确定很多对象的类型,这时传统的创建对象、获取属性、调用方法就行不通了,那么就可以通过反射来编写通用的代码解决问题。
架构师,功能 -> 对象转换成json字符串
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();
}
工作中运用反射解决的问题
四、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的底层实现
核心原理:动态代理
4.2 猜测一下,Spring IOC实现的基本流程
4.3 实际SpringIOC的结构流程图(简化)
AnnotationConfigApplicationContext构造方法
//注解的配置解析器 //解析类上@ComponentScan("com.qf") this.reader = new AnnotatedBeanDefinitionReader(this); //解析包字符串进行扫包的 this.scanner = new ClassPathBeanDefinitionScanner(this); ..... //父类的构造方法中调用,初始化BeanFactory this.beanFactory = new DefaultListableBeanFactory();
scan 扫描包路径的方法
BeanDefinitionRegistry registry; // - 这个对象用来将BeanDefinition对象注册到BeanFactory中 doScan(basePackages);
doScan 扫描包路径生成对应的Bean的BeanDefinition对象
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
//根据包路径扫描包下所有的Bean对象,生成对应的BeanDefinition对象
Set<BeanDefinition> 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方法
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
//准备阶段
prepareRefresh();
//获得前面阶段初始化过的BeanFactory对象(bean工厂)
//Bean工厂 -> Map<beanName, BeanDefinition>
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//准备BeanFacotory对象,就是给Bean工厂设置一些属性
prepareBeanFactory(beanFactory);
try {
//后置处理器的空方法,留给以后拓展使用,暂时没有作用
postProcessBeanFactory(beanFactory);
//重要!!!!
//调用BeanFactoryPostProcessor后置处理器自定义处理Bean工厂
invokeBeanFactoryPostProcessors(beanFactory);
//重要!!!
//找到内部的和自定义的BeanPostProcess对象,添加到BeanFactory的list集合中保存起来
//List<BeanPostProcessor> 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<BeanPostProcessor> 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();
}
}
}