一、Collection
集合内部存储的基本类型的数据都会被自动装箱
集合的顶级接口,是Iterable的儿子
public interface Collection<E> extends Iterable<E>
1. Iterable
- java.lang包中
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
进行迭代的接口
-
iterator,一个迭代器
-
默认方法foreach,传入一个comsumer类型的参数先判断是否为空,然后通过1.5的增强for循环进行遍历
-
使用
spliterator
方法进行分割迭代- 该方法提供了一个可以并行遍历元素的迭代器,以适应现在cpu多核时代并行遍历的需求。简单说:分割,增加并行处理能力
2. Iterator
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
- boolean hasNext():如果被迭代遍历的集合还没有被遍历完,返回True
- Object next():返回集合里面的下一个元素(默认指针指向第一个元素之上)
- remove():删除集合里面上一次next()方法返回的元素
- void forEachRemaining(Consumer action):JDK 1.8后新增默认方法 使用Lambda表达式来遍历集合元素
forEachRemaining()与forEach()方法之间的区别?
相同点:
- 都可以遍历集合
- 都是接口的默认方法
- 都是1.8版本引入的
区别:
- forEachRemaining()方法内部是通过使用迭代器Iterator的所有元素,forEach()方法内部使用的是增强for循环。
- forEach()方法可以多次调用,forEachRemaining()方法第二次调用不会做任何操作,因为不会有下一个元素。
3. 源码
- 14个普通方法,4个default方法
3.1 方法
// Query Operations
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
// Modification Operations
boolean add(E e);
boolean remove(Object o);
// Bulk Operations
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
- &iterator :abstractCollection类定义为抽象方法,要求子类必须实现
- &size:abstractCollection类定义为抽象方法,要求子类必须实现
- isEmpty
- contains
- toArray
修改类型的
- add:abstractCollection类不能插入,直接抛出异常
- remove
所有的操作
- containsAll
- addAll
- removeAll
- retainAll
- *clear
3.2默认方法
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
- removeIf (Predicate) 通过predicate方式,满足条件则删除,底层用iterator迭代,并通过remove方法删除
- 返回boolean类型
- spliterator 主要用于流的生成
- stream,顺序流
- parallelStream,并行流
4.AbstractCollection
并不属于Collection类有关,他是对Collection类的实现,list,set接口都是间接实现它
public abstract class AbstractCollection<E> implements Collection<E>
- 实现Collection接口,对Collection接口的方法几乎实现了,
Collection 下的大多数子类都继承 AbstractCollection ,比如 List 的实现类, Set的实现类。
它实现了一些方法,也定义了几个抽象方法留给子类实现,因此它是一个抽象类
4.1 抽象方法
public abstract Iterator<E> iterator();
public abstract int size();
这两个抽象方法,需要子类必须去实现
4.2 继承方法
实现了
- add
- 不允许添加,直接抛出异常
- remove
- 通过迭代器的方式删除元素
- removeAll
- 删除指定集合中包含在本集合的元素
- clear
- 通过迭代器全部删除
- addAll
- 添加一个集合的所有元素
- contains
- 通过迭代器的方式判断是否包含
- containsAll
- 是否包含指定集合中的全部元素:
- isEmpty
- 是否为空
- retainAll
- 保留共有的,删除指定集合中不共有的
- toArray(), toArray(T[] contents)
- 转换成数组
- toString
- 通过迭代器的方式实现了toString方法
二、List
- Collection接口的子接口
public interface List<E> extends Collection<E>
- 继承Collection,具有Collection的所有方法
1. 源码
1.1 方法
boolean addAll(int index, Collection<? extends E> c);
void add(int index, E element);
E remove(int index);
E set(int index, E element);
E get(int index);
int indexOf(Object o);//第一次出现的索引
int lastIndexOf(Object o);//最后一次出现的索引
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
- *add ---->abstractList实现了
add(size(), e); return true;
- *remove------->抛出异常
- *set------->抛出异常
- get------->实现了
- addAll------->实现了
- indexOf------->实现了
- lastIndexOf------->实现了
1.2默认方法
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
Stream没有重写
2. 实现类ArrayList
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- 实现serializable接口
- 实现List接口
- 实现RandomAccess
- 什么都没有 RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。也就是说,实现了这个接口的集合是支持 快速随机访问 策略的。
- 如果是实现了这个接口的 List,那么使用for循环的方式获取数据会优于用迭代器获取数据。
- 实现Cloneable-----》可克隆
- 继承AbstractList类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3h029on-1593257346329)(http://13.114.194.127:8888/assets/1591326346253.png)]
AbstractList
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E>
- 抽象类
- 继承AbstractCollection
- 实现List
它实现了 List 的一些位置相关操作(比如 get,set,add,remove),是第一个实现随机访问方法的集合类,但不支持添加和替换。
来自list的set/add/remove都是抛出异常
AbstractCollection 要求子类必须实现两个方法: iterator() 和 size()。但是它实现了iterator
public Iterator<E> iterator() {
return new Itr();
}
size方法还是抽象,等着子类实现
抽象方法
- abstract public E get(int index);
- 它老爸的Size方法也没有实现
独特方法
- removeRange(int,int)
- 通过迭代器删除指定范围的数据,clear重写就是调用它
- equals
- 实现了Equals方法
AbstractList 作为 List 家族的中坚力量
- 既实现了 List 的期望
- 也继承了 AbstractCollection 的传统
- 还创建了内部的迭代器 Itr, ListItr
- 还有两个内部子类 SubList 和 RandomAccessSublist;
- 百废俱兴,AbstractList 博采众长,制定了 List 家族的家规,List 家族基础已经搭建的差不多了。
- Arraylist默认初始化的容量是
static final int DEFAULT_CAPACITY = 10;
- 存储数据类型
static final Object[] elementData
- 2个默认大小的空数组
static final EMPTY_ELEMENTDATA = {};
static final DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
与上面的区分开了,为了方便知道添加第一个元素的时候填充多少
- private int size;
1. 构造函数
如果参数大于0,则直接创建一个数组,大小为参数,如果等于0,则是个空数组
用的是的空数组
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
无参
用的是默认的空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
Collection接口参数
将集合中的数据copyof到数组中
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
1.1 属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6MWlCKj-1593257346330)(http://13.114.194.127:8888/assets/1591326414868.png)]
2. 流程分析
添加的时候
- 先拿着size+1把它当成minCapacity,然后会进行2此校验
- 第一次校验是通过判断底层数组(elementData)是否是默认的数组,如果是则取minCapacity与10的最大值为mincapacity,如果不是则直接返回minCapacity
- 第二次校验首先记录数组修改次数+1,通过比较minCapacity>数组长度,满足则进行数组elementData扩容
- 记录elementData长度为oldCapacity,newCapacity为oldCapacity的1.5倍
- 又是两次检验
- 如果minCapacity大于newCapacity则直接使用minCapacity为数组长度
- 如果newCapacity大于最大数组容量(Integer.MAX_VALUE - 8)则使用newCapacity为最大容量
- 当newCapacity>Integer.MAX_VALUE - 8;则hugeCapacity(minCapacity);
- 在hugeCapacity中小于0,抛出outofMemoryError
- 否则:minCapacity > Integer.MAX_VALUE - 8 ? Integer.MAX_VALUE : Integer.MAX_VALUE - 8 ;
- 当newCapacity>Integer.MAX_VALUE - 8;则hugeCapacity(minCapacity);
- 然后确定newCapacity,通过Arrays.copyOf创建一个新数组,将原数组的容量给新数组
- 最后插入到elementData数组中
add(int index, E element
- 检查角标
- 空间检查,如果有需要进行扩容
- 插入元素
get(index)
- 检查角标
- 返回元素
set(index,E element)
- 检查角标
- 替代元素
- 返回旧值
remove(index)
- 检查角标
- 删除元素
- 计算出需要移动的个数,并移动
- 设置为null,让Gc回收
3. 实现类LinkedList
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
- 继承AbstractSequentialList
- 实现list
- 实现deque
- 实现Cloneable
- 实现serializable
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vGGEBCj5-1593257346333)(assets/1593224626638.png)]
底层是链表,添加删除很快
方法 | 作用 |
---|---|
public void addFirst(E e) | 头部添加指定元素 |
public void addLast(E e) | 末尾添加指定元素 |
public E getFirst() | 获取头部元素 |
public E getLast() | 获取尾部的元素 |
public E removeFirst() | 删除头部元素并返回 |
public E removeLast() | 删除尾部元素并返回 |
4. 实现类Vector
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
同Arraylist
初始10
扩容2倍
线程安全类
加的是方法锁,锁的是实例对象
三、Set
public interface Set<E> extends Collection<E>
- 相当于复制Collection,什么都没添加
1. 实现类HashSet
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
- 继承AbstractSet
- 实现set接口
- Cloneable接口
- serializable接口
- 没有实现RandomAccess接口
AbstractSet
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E>
- 实现了AbstractCollection
- Set接口实现
只实现了3个方法
- equals
- hashCode
- removeAll
1. 构造函数
无参
//default initial capacity (16) and load factor (0.75).
public HashSet() {
map = new HashMap<>();
}
指定初始化参数
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
指定初始化以及加载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
指定集合
- 计算集合大小除加载因子与16的比较
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
无修饰符的一个构造方法,主要用于new LinkedHashMap<>(initialCapacity, loadFactor);
用的是hashmap的key,详情分析hashmap
流程分析
jdk7
- put
- 1直接初始化大小,默认是16,且加载因子是.75
- 计算出位置,然后去判断是否为null
- 如果为null则直接插入
- 如果不为null,则进行Equals方法,或者是==比较
- 如果不一样,则按照链表的方式进行存储
hash计算
通过函数计算出hash值,然后进行hash&(length-1)
2. 实现类LinkedHashSet
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable
- hashset的儿子
只有4个构造方法
都是调用无修饰符的构造方法
加载因子。75,初始化大小16(hashmap中的初始化,为了保持一致)
每个数据都维护了两个引用,记录了前后顺序
3. 实现类TreeSet
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable
可以按照添加顺序添加对象的指定属性进行排序
树结构
1. 二叉树
包含
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQGpoyPo-1593257346334)(assets/1593227768277.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ykPSpXIT-1593257346335)(assets/1593228062982.png)]
二叉查找树
又称二叉搜索树或者二叉排序树
特点
- 左子树永远比树节点小
- 右字数永远比树节点大
- 一个节点只有左右节点
平衡二叉树
特点
- 左右字树的高度相差不能超过1
- 任意一个节点的左右两个字树都是平衡二叉树
平衡二叉树当添加元素不再是平衡二叉树时需要旋转
- 左旋
- 就是将根节点的右侧,往左拉,原先的右子节点变成新的父节点,
- 如果父节点有左子节点,则将左子节点变为原先父节点的右子节点
- 右旋
- 左旋的相反
红黑树
平衡二叉树当只有高度差超过1时会发生旋转
红黑手是一个二叉查找树:但是不是高度平衡的.满足红黑条件
- 每一个节点是红色或者黑色
- 根节点必须是黑色
- 如果一个节点没有子节点或者父节点则该节点相应的指针属性值为nil,这些nil视为叶节点,每个叶节点都是黑色
- 如果一个节点是红色,则子节点必须是黑色(不能出现红红相接)
- 对于每个节点,从该节点,到后代叶节点的简单路径上,必须包含相同数量的黑色节点
添加元素时 ,节点为红色,添加的效率最高
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9NVatLZC-1593257346336)(assets/1593238250707.png)]
如果叔叔节点为黑色,则1.修改父节点为黑色,将祖父节点变成红色,然后以祖父节点为支点进行旋转
四、map
是kv类型的顶级接口
- key就是set
- value就是Collection
1. 源码
1.1 方法
// Query Operations
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
// Modification Operations
V put(K key, V value);
V remove(Object key);
// Bulk Operations
void putAll(Map<? extends K, ? extends V> m);
void clear();
// Views
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
1.2 内部接口Entry
-
entrySet返回一个Map中的内部接口Entry
-
K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); comparingByKey comparingByValue comparingByKey comparingByValue 这4个是static类型
-
1.3 默认方法
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
。。。
getOrDefault
forEach
replaceAll
putIfAbsent
remove
replace
replace
computeIfAbsent
computeIfPresent
compute
merge
五、hashmap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
- 继承AbstractMap
- map接口的实现
- Cloneable接口的实现
- serializable接口的实现
初始容量太大,加载因子太小,对于遍历很不好
加载因子设置高会增大开销,遍历消耗更高
Josh Bloch 正是 HashMap
作者,
Doug Lea (ConCurrentHashMap) 并不喜欢 null,认为 null 就是个隐藏的炸弹。
AbstractMap
public abstract class AbstractMap<K,V> implements Map<K,V>
直接实现了map接口
a 抽象方法
public abstract Set<Entry<K,V>> entrySet();
- 返回的是Map中的内部接口Entry,继承此抽象类的所有子类都要实现此方法
b 方法
- 重写了toString
- 重写了Equals
- 重写了clone
- 重写了hashcode
- 重写了map接口的方法
一、Map和AbstractMap
Map是接口
AbstractMap实现了Map接口的部分方法
二、Entry和SimpleEntry关系
Map.Entry是接口
AbstractMap.SimpleEntry实现了Map.Entry接口
c1 内部类SimpleEntry
public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable
- 实现map。Entry接口
private final K key;
private V value;
构造方法
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
public SimpleEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
方法
getKey
getValue
setValue
equals
hashCode
*toString
c2 内部类 SimpleImmutableEntry
-
同SimpleEntry一样,只是value定义为final
-
private final K key; private final V value;
1. 构造方法
无参
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
无参数构造器,传递加载因子为0.75
int类型参数
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
初始化空间大小,默认是16,可以通过它指定
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);
}
加载因子:不能小于0,且必须是数字
初始容量不能小于0,最大容量是2^30
初始容量会调用tableSizeFor方法,返回一个大于等于且最接近 容量 的2的幂次方整数
传入map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
加载因子:0.75
2. 流程分析jdk8
- 默认大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
- 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
- 加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- 计算扩容阈值
- 红黑树化阈值是
TREEIFY_THRESHOLD=8
- 链表长度达到了8开始树化,且容量大于64
- 树退化阈值
static final int UNTREEIFY_THRESHOLD = 6;
- MIN_TREEIFY_CAPACITY=64 参数,只有容量大于 64 时才会开启树化
内部类
-
Node<K,V>
实现了Map.entry接口-
final int hash; final K key; V value; Node<K,V> next;
-
hash计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扰动函数
key.hashCode()返回int类型的散列值,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。内存是放不下的
jdk8之后是以下方式 (h = key.hashCode()) ^ (h >>> 16);
一共32位,h>>>16,是高位,然后与低位异或运算取哈希
为啥这样搞?
1.当高16位与低16为进行异或运算后,这样他的高位也参与运算,就可以使碰撞化的几率降低
2.tab[i = (n - 1) & hash]散列表长度固定是2的幂次方比如2^n,索引的计算是n-1 与 hash,n-1从2进制计算来说就是n个1,默认初始化的话n是16,就相当于4个1,
3.4个1与哈希进行与运算,就算出索引位置,这样使得他的高位,低位都参与运算了
数据单元是Node类型(Node的内部类,实现Map.Entry接口)包含有
- final int hash;
- final K key;
- V value;
- Node<K,V> next;
- 当发生hash冲突时,当前桶位中的node与冲突的node连成一个链表要用的字段
put流程
4种情况
- slot==null
- slot!=null node没有链化
- 首先对比一下hash是否相等,且key一样 或者Equals比较通过,就会替换
- 否则,比较是不是数节点,是,则添加到树节点中
- 最后插入到链表中
- 链表比较长,看是否满足树化条件满足的化直接树化,然后插入
- 不满足树化则尾插到链表中
- slot!=null已经链化
- 转化为红黑树
// 参数onlyIfAbsent表示是否替换原值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 空表,需要初始化
if ((tab = table) == null || (n = tab.length) == 0)
// resize()不仅用来调整大小,还用来进行初始化配置
n = (tab = resize()).length;
// (n - 1) & hash这种方式也熟悉了吧?都在分析ArrayDeque中有体现
//这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
if ((p = tab[i = (n - 1) & hash]) == null)
// 将元素直接插进去
tab[i] = newNode(hash, key, value, null);
else {
//这时就需要链表或红黑树了
// e是用来查看是不是待插入的元素已经有了,有就替换
Node<K,V> e; K k;
// p是存储在当前位置的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //要插入的元素就是p,这说明目的是修改值
// p是一个树节点
else if (p instanceof TreeNode)
// 把节点添加到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 这时候就是链表结构了,要把待插入元素挂在链尾
for (int binCount = 0; ; ++binCount) {
//向后循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表比较长,需要树化,
// 由于初始即为p.next,所以当插入第8个元素才会树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了对应元素,就可以停止了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 继续向后
p = e;
}
}
// e就是被替换出来的元素,这时候就是修改元素值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 默认为空实现,允许我们修改完成后做一些操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size太大,达到了capacity的0.75,需要扩容
if (++size > threshold)
resize();
// 默认也是空实现,允许我们插入完成后做一些操作
afterNodeInsertion(evict);
return null;
}
扩容后,数据如何迁移
-
slot==null
-
slot!=null node没有链化
- 说明没有hash冲突,直接迁移,计算出新数组中的索引就可以
-
slot!=null已经链化
- 将链表拆成2个链表,高位链和低位链
- 低位链就是老表的table.length-1转化出来的二进制有效位,都一样的
- 迁移过去的话跟老表的下标是一样的,
- 高位链是不一样的,所以迁移到新表中的位置就是不一样的
- 迁移过去,就是老表的table.length+老表的下标位
- 低位链就是老表的table.length-1转化出来的二进制有效位,都一样的
- 将链表拆成2个链表,高位链和低位链
-
红黑树
remove实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PH6xGzcZ-1593257346339)(http://13.114.194.127:8888/assets/1591327146400.png)]
LinkedHashMap
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aGnb4EAM-1593257346339)(http://13.114.194.127:8888/assets/1591327336721.png)]
- 继承图
- 底层是散列表和双向链表
- 允许为null,不同步
- 插入的顺序是有序的(底层链表致使有序)
- 装载因子和初始容量对LinkedHashMap影响是很大的~
在构建新节点时,构建的是LinkedHashMap.Entry
不再是Node
构造方法
- 默认使用的是插入顺序
LinkedHashMap有5个构造方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h2i4MuUq-1593257346341)(http://13.114.194.127:8888/assets/1591327494142.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SrCitrrn-1593257346341)(http://13.114.194.127:8888/assets/1591327515717.png)]
阅读源码的时候我们会发现多态是无处不在的~子类用父类的方法,子类重写了父类的部分方法即可达到不一样的效果!
- 比如:LinkedHashMap并没有重写put方法,而put方法内部的
newNode()
方法重写了。LinkedHashMap调用父类的put方法,里面回调的是重写后的newNode()
,从而达到目的!
LinkedHashMap可以设置两种遍历顺序:
- 访问顺序(access-ordered)
- 插入顺序(insertion-ordered)
- 默认是插入顺序的
对于访问顺序,它是LRU(最近最少使用)算法的实现,要使用它要么重写LinkedListMap的几个方法(removeEldestEntry(Map.Entry<K,V> eldest)
和afterNodeInsertion(boolean evict)
),要么是扩展成LRUMap来使用,不然设置为访问顺序(access-ordered)的用处不大~
LinkedHashMap遍历的是内部维护的双向链表,所以说初始容量对LinkedHashMap遍历是不受影响的
六、面试题
微信途径
- Collections.synchronizedMap是怎么实现线程安全的你有了解过么?
- 在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex
- 我们在调用这个方法的时候就需要传入一个Map,可以看到有两个构造器,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象
- 如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap的对象
- 创建出synchronizedMap之后,再操作map的时候,就会对方法上锁
- Hashtable呢?
- 在对数据操作的时候都会上锁,所以效率比较低下。
- Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
- 如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。
从存储结构和实现来讲基本上都是相同的。它和HashMap的最大的不同是它是线程安全的,另外它不允许key和value为null。Hashtable是个过时的集合类,不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换
- Hashtable有contains方法
- HashMap把Hashtable的contains方法去掉了,改成了containsValue和containsKey
安全失败机制(fail-safe)
由于迭代时是对原集合的拷贝的值进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会出发ConcurrentModificationException
缺点:
- 基于拷贝内容的优点是避免了
ConcurrentModificationException
,但同样地, 迭代器并不能访问到修改后的内容 (简单来说就是, 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的)
java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改.
Java集合的快速失败机制 “fail-fast”?
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:
1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
2. 使用CopyOnWriteArrayList来替换ArrayList
java.util包下的集合类都是快速失败机制的, 不能在多线程下发生并发修改(迭代过程中被修改).
-
HashMap的内部数据结构?
- JDK1.8版本的,内部使用数组 + 链表 / 红黑树;
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7cJIN0G-1593257346342)(http://13.114.194.127:8888/assets/1591320816933.png)]
-
HashMap的数据插入原理吗?
- 判断数组是否为空,为空进行初始化;
- 不为空,计算 k 的 hash 值,通过
(n - 1) & hash
计算应当存放在数组中的下标 index ; - 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
- 存在数据,说明发生了hash冲突, 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
- 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创建树型节点插入红黑树中;
- 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
- 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。
-
HashMap的哈希函数怎么设计的吗?
- hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。
-
为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?
- 因为 key.hashCode() 函数调用的是key键值类型自带的哈希函数,返回int型散列值。int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。
- 源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。
- 顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。
-
1.8还有三点主要的优化:
- 数组+链表改成了数组+链表或红黑树;
- 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
- 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
| 不同 | JDK 1.7 | JDK 1.8 |
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| 存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 初始化方式 | 单独函数:inflateTable()
| 直接集成到了扩容函数resize()
中 |
| hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
| 存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
| 插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
| 扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
-
你分别跟我讲讲为什么要做这几点优化;
- 防止发生hash冲突,链表长度过长,将时间复杂度由
O(n)
降为O(logn)
; - 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;
- 由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1
- 防止无效扩容
- 防止发生hash冲突,链表长度过长,将时间复杂度由
-
HashMap是线程安全的吗?
- 不是,在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题。
-
那你平常怎么解决这个线程不安全的问题?
- Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。
- HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大;
- Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现;
- ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。
-
你前面提到链表转红黑树是链表长度达到阈值,这个阈值是多少?为什么是8,不是16,32甚至是7 ?又为什么红黑树转链表的阈值是6,不是8了呢?
10. 因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 -
跟我讲讲TreeMap怎么实现有序的?
- TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用户key的比较。
奇葩报错
1. Arrays.asList
String[] arrays = {"1", "2", "3"};
List<String> list =Arrays.asList(arrays);
list.add("4");
- 因为Arrays.asList()返回的是Arraylist内部类,其实
class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable
- add和remove都是来自AbstractList接口,而父类方法恰恰都会抛出
UnsupportedOperationException
2.subList
修改原始元素也会改变视图的问题
List<String> list = new ArrayList<>(Arrays.asList(arrays));
我们将新的 List 集合与原始数组解耦,不再互相影响.这是真的Arraylist,不用担心 add/remove
报错了。
表面是一个不可改的list,其实能改
通过改list就可以改了
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
List<String> strings = Collections.unmodifiableList(list);
strings.add("sd");
### 3. foreach 增加/删除元素大坑
String[] arrays = {"1", "2", "3"};
List<String> list = new ArrayList<>(Arrays.asList(arrays));
for (String str : list) {
if (str.equals("1")) {
list.remove(str);
}
}
编译正常,但是运行时,程序将会发生异常
因为 foreach 这种方式实际上 Java 给我们提供的一种语法糖,编译之后将会变为另一种方式。
String[] arrays = {"1", "2", "3"};
List<String> list = new ArrayList<>(Arrays.asList(arrays));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if("1".equals(next)){
list.remove(next);
}
}
list.remove(next)你说能不报错么,用迭代器的remove()不香么
奇葩
String[] arrays = {"1", "2", "3"};
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
for (String str : list) {
if (str.equals("2")) {
list.remove(str);
}
}
System.out.println(list);
修改第二个就不报错了
可用debug解释
七、Collections
工具类
构造方法私有化
常用方法
1. 排序
sort(List<T> list)
- 调用List的sort方法
- 最终调用Arrays.sort
- 调用List的sort方法
sort(List<T> list, Comparator<? super T> c)
- 定制排序
reverse(List<?> list)
- 反转
shuffle(List<?> list)
- 使用默认随机源对列表进行置换,所有置换发生的可能性都是大致相等的。
shuffle(List<?> list, Random rand)
- 使用指定的随机源对指定列表进行置换,所有置换发生的可能性都是大致相等的,假定随机源是公平的。
swap(List list, int i , int j)
- 交换两个索引位置的元素
void rotate(List list, int distance)
- 旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面。
2. 查找、替换
int binarySearch(List list, Object key)
- 对List进行二分查找,返回索引,注意List必须是有序的
int max(Collection coll)
- 根据元素的自然顺序,返回最大的元素。
- 类比int min(Collection coll)
- 根据元素的自然顺序,返回最大的元素。
int max(Collection coll, Comparator c)
- 根据定制排序,返回最大元素,排序规则由Comparatator类控制。
- 类比int min(Collection coll, Comparator c)
- 根据定制排序,返回最大元素,排序规则由Comparatator类控制。
void fill(List list, Object obj)
- 用元素obj填充list中所有元素
int frequency(Collection c, Object o)
- 统计元素出现次数
int indexOfSubList(List list, List target)
- 统计targe在list中第一次出现的索引,找不到则返回-1,
- 类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal)
- 用新元素替换旧元素。
八、Collectors
public final class Collectors
注意区别Collections类
常用方法参考Stream流中的Collect终止方法
九、ConcurrentHashMap
底层:散列表+红黑树,与HashMap是一样的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eOWm8rrJ-1593257346343)(http://13.114.194.127:8888/assets/1591586433284.png)]
实现map和iterator的所有方法
不允许k-v为null
提供支持批量操作
当很多key的hash相同时会非常消耗性能,key实现comparable接口会好点
线程安全,并且检索操作不加锁,get为非阻塞方法
## JDK7
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QMxpvLf3-1593257346344)(http://13.114.194.127:8888/assets/1591585883710.png)]
ConcurrentHashMap
初始化时,计算出Segment
数组的大小ssize
和每个Segment
中HashEntry
数组的大小cap
并初始化Segment
数组的第一个元素;其中ssize
大小为2的幂次方,默认为16,cap
大小也是2的幂次方,最小值为2
-
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
-
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。
-
分段锁
-
volatile V value; volatile HashEntry<K,V> next;
JDK8
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RioPGsEU-1593257346345)(http://13.114.194.127:8888/assets/1591585901331.png)]
插入元素过程(建议去看看源码):
- 如果相应位置的Node还没有初始化,则调用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 }
-
如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于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; } } }
-
-
如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点;如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
-
如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount;
1. CAS
CAS(比较与交换,Compare and swap) 是一种有名的无锁算法
CAS有3个操作数
- 内存值V
- 旧的预期值A
- 要修改的新值B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
CAS就一定能保证数据没被别的线程修改过么?
并不是的,比如很经典的ABA问题,CAS就无法判断了。
就是说来了一个线程把值改回了B,又来了一个线程把值又改回了A,对于这个时候判断的线程,就发现他的值还是A,所以他就不知道这个值到底有没有被人改过,其实很多场景如果只追求最后结果正确,这是没关系的。
但是实际过程中还是需要记录修改过程的,比如资金修改什么的,你每次修改的都应该有记录,方便回溯。
怎么解决ABA问题?
用版本号去保证就好了,就比如说,我在修改前去查询他原来的值的时候再带一个版本号,每次判断就连值和版本号一起判断,判断成功就给版本号加1。
除了版本号还有别的方法保证么?
其实有很多方式,比如时间戳也可以,查询的时候把时间戳一起查出来,对的上才修改并且更新值的时候一起修改更新时间,这样也能保证,方法很多但是跟版本号都是异曲同工之妙,看场景大家想怎么设计吧。
2. volatile关键字
volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
-
保证该变量对所有线程的可见性
-
- 在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
-
不保证原子性
-
- 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的。
volatile的特性是啥?
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
域对象
- table
- Node数组,散列表
- nextTable
- node数组,散列表,除了扩容时候,其他时间都是null
- baseCount
- 通过CAS来更新的计数器
- sizeCtl
- 初始化和扩容都需要它
- 负数,他正在初始化或者扩容
- -1 表示正在初始化
- -N,表示N-1个线程在进行扩容
- 默认是0,初始化之后保存着下一次扩容的大小
- transferIndex
- 分割表时用的索引值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5yyZX8b7-1593257346345)(http://13.114.194.127:8888/assets/1591587062445.png)]
## 构造方法
同Linkedhashmap4个一样,只有3参数的为(int,float,int)
- 当ConcurrentHashMap(int initialCapacity)
- tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
- 也是2的次方靠近
- 赋值给sizeCtl
put方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5eC0duw4-1593257346346)(http://13.114.194.127:8888/assets/1.webp)]
初始化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Y3yGPo1-1593257346346)(http://13.114.194.127:8888/assets/1591588890847.png)]
只让一个线程对散列表进行初始化!
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
(先链表的方式处理如果不是链表,最后以树的方式插入,最终判断是否树化。hashmap是先判断是否为树,不是的话向下走,插入链表的时候先考虑有没有树化,实在没有了,直接插入链表)
jdk1.7
首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut()
自旋获取锁。
- 尝试自旋获取锁。
- 如果重试的次数达到了
MAX_SCAN_RETRIES
(在多处理器环境下,重复次数为64,单处理器重复次数为1)则改为阻塞锁获取,保证能获取成功。
get方法
不用加锁的,是非阻塞的
Node节点是重写的,value和next设置了volatile关键字修饰,致使它每次获取的都是最新设置的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Lsq2Ui3-1593257346347)(http://13.114.194.127:8888/assets/1591597421356.png)]
jdk1.7
get 逻辑比较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
jdk1.8
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。
小结:1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)
),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
synchronized为什么用的多?
CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?
- synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。
- 针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果以上都失败就升级为重量级锁。
- 所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。
对象锁可能存在的4中状态:无锁->偏向锁->轻量级锁->重量级锁,并且其中锁的升级是不可逆的。
-
偏向锁
- 该锁的着重点在于偏字上,它会偏向与第一个获取它的线程。当一个线程进入到被Synchronized修饰的代码时,将当前偏向锁状态改为1(偏向锁上锁的过程),那么代表当前线程已经获取执行同步代码块的权利了。
- 后续该线程进来时,如果该锁没有被其他锁获取到或者没发生锁竞争,那么就会再有任何的同步措施,只是简单判断一下当前线程id是否与Markword当中的线程id是否一致。
- 出现了锁竞争的话,当前线程也会判断之前拥有锁的线程是否存在或者存在但没拥有该锁状态,就进行重置该偏向锁,并重新进行上锁过程,若仍然存在,此时该偏向锁就会升级为轻量级锁。
- 升级的过程中就会涉及到锁撤销的过程,锁撤销的过程也是满复杂的,资源的消耗也挺大的,可以在启动的时候设置-XX:-UseBiasedLocking = false,即该应用就不存在偏向锁了。
-
轻量级锁
- 轻量级锁有两种:自旋锁和自适宜自旋锁
- 自旋锁
- java对其做了规定,默认每个线程最多执行10次
- 自适宜自旋锁
- jdk1.6的时候被引入,线程的自旋次数不再是固定值了而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,为了避免浪费处理器资源,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接升级为重量级锁
-
重量级锁
-
对包含Synchronized的代码进行反编译之后会发现在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。
-
monitorenter
-
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
-
-
monitorexit
-
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
从上面对于monitorenter与monitorexit的描述也可以看出Synchronized是一个重入锁,每次重入对应的monitor的进入数+1,退出减1.
-
-
1.6之前建议不要使用Synchronized而使用ReentrantLock锁,在JDK1.6之后为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”,最终使得Synchronized的效率与ReentrantLock相差无几,甚至在某些场景下还优胜与ReentrantLock。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ol9IJ9D-1593257346348)(http://13.114.194.127:8888/assets/1591609673099.png)]