集合篇(list、map、set)

集合篇(list、map、set)

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

1、ArrayList

1)是什么

ArrayList就是数组列表,当我们装载的是基本类型的数据 int,long,boolean,short,byte,float,double,char的时候我们只能存储他们对应的包装类(有序、可重复、允许存在多个null元素),它的主要底层实现是数组Object[] elementData。

⼩结:ArrayList底层是⽤数组实现的存储。
特点: 查询效率⾼,增删效率低,线程不安全。使⽤频率很⾼。

2)有哪些用?

主要⽤来装载数据,与它类似的是LinkedList,和LinkedList相⽐,ArrayList的查找和访问元素的速度较快,但新增,删除的速度较慢。

3)为什么线程不安全还用它?

因为我们正常使⽤的场景中,都是⽤来查询,不会涉及太频繁的增删,如果涉及频繁的增删,可以使⽤LinkedList,如果你需要线程安全就使⽤Vector,这就是三者的区别了,实际开发过程中还是ArrayList使⽤最多的。

4)如何初始化的数组大小?

ArrayList可以通过构造⽅法在初始化的时候指定底层数组的⼤⼩。

一、通过⽆参构造⽅法的⽅式ArrayList()初始化,则赋值底层数Object[] elementData为⼀个默认空数组Object[ ] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} 所以数组容量为0,只有真正对数据进⾏添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。
在这里插入图片描述在这里插入图片描述
执行list.add
(1)先确定是否要扩容
(2)然后再执行赋值
在这里插入图片描述在这里插入图片描述
该方法确认第一次扩容为容量为10
在这里插入图片描述modCount:计算扩容次数
minCapacity:需要的容量
如果elementData的大小不够,就调用grow() 去扩容
在这里插入图片描述
(1)真的扩容
(2)使用扩容机制来确定要扩容多大
(3)第一次newCapacity=10
(4)第二次及其以后,按照1.5倍扩容
(5)扩容使用的是Arrays.copyOf ()
在这里插入图片描述二、通过有参构造⽅法的⽅式ArrayList()初始化
在这里插入图片描述

5)不断加数据,是如何扩容的?

⽐如我们现在有⼀个⻓度为10的数组,现在我们要新增⼀个元素,发现已经满了。

第⼀步他会重新定义⼀个⻓度为10+10/2的数组也就是新增⼀个⻓度为15的数组。
第二步把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组。

扩大1.5倍
在这里插入图片描述

6)1.7和1.8版本初始化的时候的区别?

ArrayList 1.7开始变化有点⼤,⼀个是初始化的时候,1.7以前会调⽤this(10)才是真正的容量为10,1.7即本身以后是默认⾛了空数组,只有第⼀次add的时候容量会变成10。

7)ArrayList在增删的时候是怎么做的,为什么慢?

他有指定index新增,也有直接新增的,在这之前他会有⼀步校验⻓度的判断,ensureCapacityInternal,就是说如果⻓度不够,是需要扩容的。
在这里插入图片描述

在扩容的时候,⽼版本的jdk和8以后的版本是有区别的,8之后的效率更⾼了,采⽤了位运算,右移⼀位,其实就是除以2这个操作。1.7的时候3/2+1 ,1.8直接就是3/2。
在这里插入图片描述

指定位置新增的时候,在校验之后的操作很简单,就是数组的copy。
在这里插入图片描述

8)ArrayList插入删除⼀定慢么?删除怎么实现的呢?

取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来⽤还是挺合适的,push和pop操作完全不涉及数据移动操作。
删除其实跟新增是⼀样的,不过叫是叫删除,但是在代码⾥⾯我们发现,他还是在copy⼀个数组。

9)ArrayList是线程安全的么?

不是,线程安全的数组容器是Vector。Vector的实现很简单,就是把所有的⽅法统统加上synchronized就完事了。(Vector扩容是扩大2倍)
你也可以不使⽤Vector,⽤Collections.synchronizedList把⼀个普通ArrayList包装成⼀个线程安全版本的数组容器也可以,原理同Vector是⼀样的,就是给所有的⽅法套上⼀层synchronized。

10)ArrayList用来做队列合适么?

队列⼀般是FIFO(先⼊先出)的,如果⽤ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是⽆论如何总会有⼀个操作会涉及到数组的数据搬迁,这个是⽐较耗费性能的。
结论: ArrayList不适合做队列。

11)数组适合用来做队列么?

数组是⾮常合适的。
⽐如ArrayBlockingQueue内部实现就是⼀个环形队列,它是⼀个定⻓队列,内部是⽤⼀个定⻓数组来实现的。
简单点说就是使⽤两个偏移量来标记数组的读位置和写位置,如果超过⻓度就折回到数组开头,前提是它们是定⻓数组。

12)ArrayList的遍历和LinkedList遍历性能比较如何?

论遍历ArrayList要⽐LinkedList快得多,ArrayList遍历最⼤的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存⽚段,可以⼤幅降低读取内存的性能开销。

2、LinkedList

是什么?

List接口的实现类,它是一个集合,可以根据索引来随机的访问集合中的元素,还实现了Deque接口,它还是一个队列,可以被当成双端队列来使用,底层是通过链表来实现的。

特点: 随机访问速度是比较差的,但是它的删除,插入操作会很快

每个节点都有一个前驱(之前前面节点的指针)和一个后继(指向后面节点的指针)
在这里插入图片描述
提供头插(linkFirs(E e))、尾插(linkLast(E e))和指定位置插入(add(int index, E element))

为什么查询的时候慢,增删快?

当get的时候,利用了双向链表的特性,如果index离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。这样的效率是非常低的,特别是当 index 越接近 size 的中间值时。

小结: 插入,删除都是移动指针效率很高。查找需要进行遍历查询,效率较低。

3、HashMap

1)K-V 最后是 HashMap$Node node = newNode(hash,key,value,null)
2)K-V 为了方便程序员的遍历,还会 创建 EntrySet 集合,该集合存放的元素的类型是 Entry,而一个Entry 对象就有K,V(不是真实值,而是地址) EntrySet<K,V>
3)EntrySet 中,定义的类型是 Map.Entry ,但是实际上存放的还是 HashMap $Node , 这是因为 Node<K.V>实现了 Map.Entry<K.V>
4)Map.Entry 提供了重要方法 K getKey(); V getValue()
在这里插入图片描述在这里插入图片描述keySet()方法取出所有K值,values()方法取出所有V值
在这里插入图片描述
Map集合6种遍历方式:
取所有的Key
在这里插入图片描述取所有的Value
在这里插入图片描述取所有K-V
在这里插入图片描述在这里插入图片描述

1)Java7和Java8的区别?

1、1.7数据结构是数组+单链表,内部类是Entry,1.8则是数组+单链表或红黑树,内部类是Node
2、put方法,1.7是头插法,1.8是尾插法
3、 jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀
4、 扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前

2)HashMap存取原理?

数组里面以key-value的这样的实例存储,Java7叫Entry在Java8中叫Node,因为本身所在的位置都是null,在put插入的时候会根据key的hash去计算一个index值,也就是数据插入的位置,因为数组的长度是有限的,在有限的长度里面我们使用哈希,哈希本身就存在概率性,有可能哈希出来的index值是相同的,这样就形成了链表,每个节点都会保存自身的hash值,key,value,以及下个节点,Java8之前是头插法,也就是新的值会取代原来的值,原来的值就顺推到链表中去,Java8之后就是采用的尾插法,插入到链表的尾部

3)为什么会线程不安全?

1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

4)默认容量?负载因子?为什大小都是2次幂?HashMap的扩容方式?

扩容主要受两个因素影响:
1、Capacity:hashmap当前长度
2、LoadFactor:默认0.75f
在这里插入图片描述

默认容量是16,阈值就是16*.075=12,大于12判断发现需要resize了,就进行扩容。分两步:1创建一个新的空数组,长度是原来的2倍。遍历原来的所有Node重新hash到新数组。至于为什么要重新hash,是因为数组的长度发生了变化,hash运算的出来的index值是不一样的,Hash的公式—> index = HashCode(Key) & (Length - 1)。比如原来是16,hashMap.length-1 = 15,二进制为1111,假设存入一个数据,计算其hashCode为26437,二进制为0110 0111 0011 0101,两者做与运算得出的二进制为0101,十进制为7,则index = 7。 假如Resize不为2的次方幂,则 hashMap.length-1 得出的二进制数可能为0110,0001,0100等,此时如果和数据的hashCode值做与运算极易出现index冲突,即hashMap中某些元素为null,某些元素有多个Entry,这大大降低了hashMap的使用性能

5)为什么会线程不安全?

1.在JDK1.7中,当并发执行扩容操作时会造成环形链(头插导致环形链表死循环)和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

6)HashMap是怎么处理hash碰撞的?

通过拉链法解决。
在put时:
1、通过key的hash值来确定键值对的存放位置
2、如果hash值发生碰撞,则判断该元素是否为红黑树节点,是则进行红黑树的插入,否则遍历链表进行插入
3、如果遍历过程中找到key相同的元素则替换,否则在链表尾部插入该节点
在get时:
1、计算key的hash值
2、确定键值对在数组的下标,若下标元素是空则返回null,否贼判断元素是否是待查的值,是就直接返回
3、若不是则判断元素节点是否是红黑树节点,是则进行红黑树的查找,否贼进行链表的查找

7)Hash的计算规则?

会进行从从高位到低位的传播(异或),主要还是为了解决hash碰撞问题,如果直接使用key.hashCode()作为hash值的话,存在一些问题。
举例说明,HashMap的默认长度为16,并且是通过(table.length - 1) & hash的方式得到key在table中的下标
如果key1.hashCode()=1661580827(二进制为0110,0011,0000,1001,1011,0110,0001,1011),key2.hashCode()=1661711899(二进制为0110,0011,0000,1011,1011,0110,0001,1011)
在与掩码进行与的过程中,只有后4位起作用,导致得到的下标值均为11,导致高位完全失效,加大了冲突的可能性。
如果通过高位向低位异或传播的话,高位同样参与到key在table中下标的运算,减少了碰撞的可能性
key1.hashCode() ^ (key1.hashCode() >>>16)=1661588754(二进制为0110,0011,0000,1001,1101,0101,0001,0010)
key2.hashCode() ^ (key2.hashCode() >>>16)=1661719824(二进制为0110,0011,0000,1011,1101,0101,0001,0000)
在于掩码进行与操作得到的下标分别为2和0,减少了冲突的可能性。

8)如何确保线程安全

1、使⽤Collections.synchronizedMap(Map)创建线程安全的map集合;
2、Hashtable
3、ConcurrentHashMap
不过出于线程并发度的原因,我都会舍弃前两者使⽤最后的ConcurrentHashMap,他的性能和效率明显⾼于前两者。

9)为什么Hashtable不允许键或值为null,HashMap 的键值则都可以为null?

这是因为Hashtable使⽤的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不⼀定是最新的数据。如果你使⽤null值,就会使得其⽆法判断对应的key是不存在还是为空,因为你⽆法再调⽤⼀次contain(key)来对key是否存在进⾏判断,ConcurrentHashMap同理。但是HashMap却做了特殊处理

10)Hashtable与HashMap的不同点?

实现⽅式不同: Hashtable 继承了 Dictionary类,⽽ HashMap 继承的是 AbstractMap 类。
初始化容量不同: HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因⼦默认都是:0.75。
扩容机制不同: 当现有容量⼤于总容量 * 负载因⼦时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
迭代器不同: HashMap 中的 Iterator 迭代器是 fail-fast 的,⽽ Hashtable 的 Enumerator 不是 fail-fast 的。所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出
ConcurrentModificationException 异常,⽽ Hashtable 则不会。

11)fail-fast是什么?

快速失败(fail—fast)是java集合中的⼀种机制, 在⽤迭代器遍历⼀个集合对象时,如果遍历过程中对集合对象的内容进⾏了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使⽤⼀个 modCount 变量。集合在被遍历期间如果内容发⽣变化,就会改变modCount的值。每当迭代器使⽤hashNext()/next()遍历下⼀个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终⽌遍历。

Tip:这⾥异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发⽣变化时修改modCount值刚好⼜设置为了expectedmodCount值,则异常不会抛出。

因此,不能依赖于这个异常是否抛出⽽进⾏并发操作的编程,这个异常只建议⽤于检测并发修改的
bug。

4、TreeMap

1)使用默认的构造器,创建的TreeMap,是无序的(也没有排序)
2)按照传入的 K (String) 的大小进行排序
在这里插入图片描述

5、Hashtable

初始化容量为11
加载因子0.75
扩容2倍+1

1)put过程

put方法的主要逻辑如下:
1.先获取synchronized锁。
2.put方法不允许null值,如果发现是null,则直接抛出异常。
3.根据key的hashCode值 与 0x7FFFFFFF求与后得到新的hash值,然后计算其在table中的索引下标。
3.遍历对应位置的链表,如果发现已经存在相同的hash和key,则更新value,并返回旧值。
4.如果不存在相同的key的Entry节点,则调用addEntry方法增加节点。
5.addEntry方法中,先判断table.count是否超过阈值,超过则重新rehash,将原元素全部拷贝到新的table中,并重新计算索引下标。(rehash后,容量是以前的2倍+1的大小)
6.插入元素时直接插入在链表头部。
7、更新元素计数器。

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

 private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

2)get过程

1.先获取synchronized锁。
2.先对Hashtable的key进行hash拿到hashcode值,
3.用key的hashcode值进行异或,然后对数组的长度进行取余,这里效率没有HashMap的效率高
4.再去判断当前的这个Entry节点有没有存在数据值。如果存在,就去判断这个Entry节点的key是不是等于你要取的值对应的那个key,若果是直接返回,如果判断不存在,就说明没有你要的值,就直接返回NULL

public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

6、HashSet

1)底层数据结构

HashSet底层就是HashMap(数组+单项链表+红黑树

7、TreeSet

1)底层数据结构

1)当使用无参构造器,创建TreeSet时,仍然是无序的
2)希望添加的元素,按照字符串大小来排序,可以使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)
在这里插入图片描述在这里插入图片描述

8、LinkedHashSet

1)底层数据结构

1)HashSet的子类
2)底层是一个LinkedHashMap,底层维护类额一个 数组+双向链表
3)根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
4)LinkedHashSet不允许添加重复元素
5)添加第一次时,直接将 数组table 扩容到16,存放的结点类型是 LinkedHashMap$Enty
6)数组是 HashMap N o d e 【 】 存 放 的 元 素 / 数 据 是 L i n k e d H a s h M a p Node【】 存放的元素/数据是 LinkedHashMap Node/LinkedHashMapEnty类型

9、ConcurrentHashMap

1)底层数据结构

ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
在1.7中的数据结构:
在这里插入图片描述
是由 Segment 数组、HashEntry 组成,和 HashMap ⼀样,仍然是数组加链表。
Segment 是 ConcurrentHashMap 的⼀个内部类,主要的组成如下:

static final class Segment<K,V> extends ReentrantLock implements
Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 Entry 作⽤⼀样,真正存放数据的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
// 记得快速失败(fail—fast)么?
transient int modCount;
// ⼤⼩
transient int threshold;
// 负载因⼦
final float loadFactor;

HashEntry跟HashMap差不多的,但是不同点是,他使⽤volatile去修饰了他的数据Value还有下⼀个节点next。

2)1.8 做了什么优化?

其中抛弃了原有的 Segment 分段锁,⽽采⽤了 CAS + synchronized 来保证并发安全性。
跟HashMap很像,也把之前的HashEntry改成了Node,但是作⽤不变,把值和next采⽤了volatile去修饰,保证了可⻅性,并且也引⼊了红⿊树,在链表⼤于⼀定值的时候会转换(默认是8)

3)并发度高的原因?

原理上来说,ConcurrentHashMap 采⽤了分段锁技术,其中 Segment 继承于 ReentrantLock。

不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap⽀持CurrencyLevel (Segment 数组数量)的线程并发。

每当⼀个线程占⽤锁访问⼀个 Segment 时,不会影响到其他的 Segment。

就是说如果容量⼤⼩是16他的并发度就是16,可以同时允许16个线程操作16个Segment⽽且还是线程安全的。

4)线程安全怎么做的?

他先定位到Segment,然后再进⾏put操作。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
		HashEntry<K,V> node = tryLock() ? null :
			scanAndLockForPut(key, hash, value);
		V oldValue;
		try {
			HashEntry<K,V>[] tab = table;
			int index = (tab.length - 1) & hash;
			HashEntry<K,V> first = entryAt(tab, index);
			for (HashEntry<K,V> e = first;;) {
				if (e != null) {
					K k;
	// 遍历该 HashEntry,如果不为空则判断传⼊的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
					if ((k = e.key) == key ||
						(e.hash == hash && key.equals(k))) {
						oldValue = e.value;
						if (!onlyIfAbsent) {
							e.value = value;
							++modCount;
						}
						break;
					}
					e = e.next;
				}
				else {
				// 不为空则需要新建⼀个 HashEntry 并加⼊到 Segment 中,同时会先判断是否需要扩容。
					if (node != null)
						node.setNext(first);
					else
						node = new HashEntry<K,V>(hash, key, value,first);
					int c = count + 1;
					if (c > threshold && tab.length < MAXIMUM_CAPACITY)
						rehash(node);
					else
						setEntryAt(tab, index, node);
					++modCount;
					count = c;
					oldValue = null;
					break;
				}
			}
		} finally {
			//释放锁
			unlock();
		}
		return oldValue;
	}

⾸先第⼀步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利⽤
scanAndLockForPut() ⾃旋获取锁。

  1. 尝试⾃旋获取锁。
  2. 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。

get 逻辑⽐较简单,只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过⼀次 Hash 定位到具
体的元素上。
由于 HashEntry 中的 value 属性是⽤ volatile 关键词修饰的,保证了内存可⻅性,所以每次获取时都是
最新值。
ConcurrentHashMap 的 get ⽅法是⾮常⾼效的,因为整个过程都不需要加锁。

5)get过程?

根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
如果是红⿊树那就按照树的⽅式获取值。
就不满⾜那就按照链表的⽅式遍历获取值。

6)put过程?

ConcurrentHashMap在进⾏put操作的还是⽐较复杂的,⼤致可以分为以下步骤:

  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进⾏初始化。
  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写⼊数据,利⽤ CAS 尝试写⼊,失败则⾃旋保证成功。
  4. 如果当前位置的 hashcode== MOVED== -1 ,则需要进⾏扩容。
  5. 如果都不满⾜,则利⽤ synchronized 锁写⼊数据。
  6. 如果数量⼤于 TREEIFY_THRESHOLD 则要转换为红⿊树。

7)CAS是什么?自旋又是什么?

CAS 是乐观锁的⼀种实现⽅式,是⼀种轻量级锁,JUC 中很多⼯具类的实现就是基于 CAS 的。

CAS 操作的流程如下图所示,线程在读取数据时不进⾏加锁,在准备写回数据时,⽐较原值是否修改,
若未被其他线程修改则写回,若已被修改,则重新执⾏读取流程。

这是⼀种乐观策略,认为并发操作并不总会发⽣。
在这里插入图片描述
就⽐如我现在要修改数据库的⼀条数据,修改之前我先拿到他原来的值,然后在SQL⾥⾯还会加个判
断,原来的值和我⼿上拿到的他的原来的值是否⼀样,⼀样我们就可以去修改了,不⼀样就证明被别的
线程修改了你就return错误就好了。

自旋锁:与互斥量类似,它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。用在以下情况:锁持有的时间短,而且线程并不希望在重新调度上花太多的成本。“原地打转”。

8)CAS就⼀定能保证数据没被别的线程修改过么?

并不是的,⽐如很经典的ABA问题,CAS就⽆法判断了。

9)什么是ABA?如何解决ABA问题?

就是说来了⼀个线程把值改回了B,⼜来了⼀个线程把值⼜改回了A,对于这个时候判断的线程,就发现
他的值还是A,所以他就不知道这个值到底有没有被⼈改过,其实很多场景如果只追求最后结果正确,
这是没关系的。

但是实际过程中还是需要记录修改过程的,⽐如资⾦修改什么的,你每次修改的都应该有记录,⽅便回
溯。

解决:
⽤版本号去保证就好了,就⽐如说,我在修改前去查询他原来的值的时候再带⼀个版本号,每次判断就
连值和版本号⼀起判断,判断成功就给版本号加1。

其实有很多⽅式,⽐如时间戳也可以,查询的时候把时间戳⼀起查出来,对的上才修改并且更新值的时
候⼀起修改更新时间,这样也能保证,⽅法很多但是跟版本号都是异曲同⼯之妙,看场景⼤家想怎么设
计吧。

10)CAS性能很高,synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?

synchronized之前⼀直都是重量级的锁,但是后来java官⽅是对他进⾏过升级的,他现在采⽤的是锁升
级的⽅式去做的。

针对 synchronized 获取锁的⽅式,JVM 使⽤了锁升级的优化⽅式,就是先使⽤偏向锁优先同⼀线程然
后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂⾃旋,防⽌线程被系统挂起。
最后如果以上都失败就升级为重量级锁。

所以是⼀步步升级上去的,最初也是通过很多轻量级的⽅式锁定的。

11)如何在很短时间内将大量数据插入到ConcurrentHashMap,换句话说就是提高ConcurrentHashMap的插入效率?

将大批量数据保存到map中有两个地方的消耗将会是比较大的:第一个是扩容操作,第二个是锁资源的争夺。

第一个扩容的问题,主要还是要通过配置合理的容量大小和扩容因子,尽可能减少扩容事件的发生;

第二个锁资源的争夺,在put方法中会使用synchonized对头节点进行加锁,而锁本身也是分等级的,因此我们的主要思路就是尽可能的避免锁升级。所以,针对第二点,我们可以将数据通过ConcurrentHashMap的spread方法进行预处理,这样我们可以将存在hash冲突的数据放在一个组里面,每个组都使用单线程进行put操作,这样的话可以保证锁仅停留在偏向锁这个级别,不会升级,从而提升效率。

10、Collections工具类

1)只有无参构造器
2)常用方法
reverse()反转List中元素打的顺序
shuffle()对List集合元素进行随机排序
sort()根据元素的自然排序对指定List集合元素按升序排序
在这里插入图片描述

volatile的特性是?

保证了不同线程对这个变量进⾏操作时的可⻅性,即⼀个线程修改了某个变量的值,这新值对其他线程来说是⽴即可⻅的。(实现可⻅性)
禁⽌进⾏指令重排序。(实现有序性)
volatile 只能保证对单次读/写的原⼦性。i++ 这种操作不能保证原⼦性。

Collections.synchronizedMap()、ConcurrentHashMap、Hashtable之间的区别0

在SynchronizedMap内部维护了⼀个普通对象Map,还有排斥锁mutex
在这里插入图片描述

Collections.synchronizedMap(new HashMap<>(16));

我们在调⽤这个⽅法的时候就需要传⼊⼀个Map,它有两个构造器,如果你传⼊了mutex参数,则将对象排斥锁赋值为传⼊的对象。如果没有,则将对象排斥锁赋值为this,即调⽤synchronizedMap的对象,就是上⾯的Map。

创建出synchronizedMap之后,再操作map的时候,就会对⽅法都上锁

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值