1. 集合

# 集合

### **单列集合**

#### ArrayList

集合的话主要分为单列集合和双列集合两种。单列集合中主要是有ArrayList、linkList以及HashSet。ArrayList的主要特点就是**有序且可重复**的,之所以有序是因为它的底层其实就是一个Object数组结构,每次添加长度都是累加的,可以重复是因为存储的过程中没有对元素做过判断。这个数组它在创建的时候**初始长度是0**,在第一次添加数据的时候会初始化容量为10,因为它是一个数组结构,那么当它在存储数据存到了11的时候,数组会进行扩容,**扩容的机制**就是**原来的1.5倍**。ArrayList有一个要注意的点就是,当它在**迭代遍历的时候不能进行删除操作**,否则的话会报错,报并发修改异常,因为java不允许一个线程在遍历的时候另一个删除线程来修改它。那么解决的话可以换一个支持并发的类来执行操作。

#### **linkList**

与ArrayList对比的还有一个是linkList。那么linkList它的**底层结构**是一个**双向链表**的数据结构实现。它是**通过节点的指向**进行连接的,除了存储数据以外还存储了两个引用,一个指向前一个元素,一个指向后一个元素,所以它相比较ArrayList**更占用内存**。它的存储特点也是**有序且可重复**的,因为它存储的过程中也没有对元素进行判断。还有一个区别就是,在进行**顺序查找或者查询**的时候,ArrayList要比linkList要快;在进行**无规则插入**的时候,linkList效率可能要比ArrayList要快。它们都是**不同步**的,不保证线程安全。

### **双列集合**

#### HashSet

还有一个集合就是HashSet,它的底层原理比较特殊,它是**使用HashMap的key**来进行存储的,它的特点就是**无序且不可重复**,无序是因为hashset在保存数据的时候,顺序是通过计算key的hash值和当前数组的长度计算的,保存的是数据的**下标**位置。不可重复是因为在向HashSet中添加元素的时候,不仅要保证hashmap中的**key不可重复**,同时还要结合equles方法比较,相同的话会用新的值覆盖旧的值,并且返还旧的值。

#### **HashMap-JDK1.7**

那么说到HashSet的底层原理了,就不得不提一提双列集合里面,比较重要的一个集合HashMap。HashMap的存储结构是有很大的改变,首先是在1.8之前,它是**数组加链表**的结构,**初始长度的话**是0,**存储特点**和HashSet一样,也是无序且不可重复,不过它存储的数据是键值对。那么在插入的时候采用的方法是**头插法**,头插法就是原位置的数据后移一位,再将数据插入到该位置。当然如果要是确定需要存储元素的个数的话,最好直接初始化构建,因为它的**扩容是非常损耗性能**的。说到扩容,那么首先说一下它的**初始容量**是16,是因为2的幂次方在位运算的时候效率最高。当存储个数大于等于扩容阈值并且出现**Hash冲突**的时候会进行扩容,所谓的Hash冲突就是存储位置上已经有了元素。它的**扩容大小**是原来的2倍。与此同时,HashMap在1.8之前也是存在有问题的,一个是在并发的场景下会出现环链,所谓的环链就是两个线程同时进行扩容,导致**多线程死循环**,从而导致**数据丢失**的问题。还有一个就是如果在数据量过大的情况下,会导致链表过长,从而影响查询效率。

#### **HashMap-JDK1.8**

那么jdk1.8它的存储结构是:**数组+链表+红黑树**。红黑树是一种平衡的二叉查找树,基本操作是添加删除。解决了链表过长的问题,提高查询效率。同时又使用了**尾插法**代替头插法,插入的时候会将新元素插入到链表末尾,在扩容时会保持链表原本的顺序。那么,不直接就使用红黑树是因为在节点少的时候,链表和红黑树的差距不大,但红黑树是**以空间换时间**,所以**比较占用空间**。当链表长度大于等于8,并且数组长度大于等于64时,链表才需要转换成成红黑树。所以在节点6之前用链表,节点8之后用红黑树,7的话如果是在新增的时候是链表,删除的时候是红黑树。那么,树化值是8是因为**泊松分布**计算得到hash冲突8次的概率为千万分之一。在第一次执行put数据时,默认初始化容量为16,扩容阈值是12,每次扩容是原来的2倍。	

#### **ConcurrentHashMap** 

HashMap是线程不安全的,在多线程的时候,添加数据或者扩容的时候都有可能出现数据覆盖、丢失的现象,那么就不得不说到一个线程安全的集合**ConcurrentHashMap** ,首先在JDK1.7的时候,它是用**synchronized同步代码块**形式保证线程安全的,默认长度是16,默认分配16个哈希槽,第一次插入新元素时,会根据键的哈希值来计算出在数组中存入的位置,但它的**数组一旦创建就无法扩容**。那么在JDK1.8的时候,它的底层原理是:Node + CAS + sync 实现了**每个节点一把锁**,它的**加载机制**会变为在第一次添加元素的时候判断长度是否为0,如果是的话就初始化元素。



### **Conllection和==Conllections==的区别是什么?**

**Conllection**是一个单列集合的接口,**Collections**是一个操作集合的工作类,两者本质就不一样。

# ()list集合

首先List集合是有序集合,即存取有序,List集合的特点是存取顺序一致,存储元素可重复,都有索引。

1.**ArrayList**底层是**Object[]**数组结构,元素是有序且可重复的。而且查询快、增删慢。为什么有序呢?是因为根据下标来储存元素。为什么可重复呢?是因为存储对元素没有做判断。为什么查询快、增删慢?一般情况下根据下标来查询和增删,为了保证其有序性,在增删时元素需要移位,所以增删慢。

2.==ArrayList的扩容:==

​      用无参构造创建集合时其实是创建了一个**空数组**Object[] elementData,长度为0。当第一次调用**add()**方法添加元素时,数组容量会初始化为10。之后按1.5倍扩容。当添加第11个元素的时候,会创建一个长度为15(10 * 1.5倍)的数组,旧的数组就会使用`Arrays.copyOf()`方法被复制到新的数组中去。【在jdk1.6之前,是1.5倍+1扩容】

3.==LinkedList底层==
		是**链表**结构,**增删快查询慢**。LinkedList集合的每个元素都是以Node对象,都是首节点和尾节点。查询时会从两头开始查,所以查询慢。但是增删时,只需要将链表头结点和尾结点指向新插入的结点即可,所以增删速度较快。

查询规则:当数组长度除以元素下标>2时,会从头开始查找;反之从尾开始查询。



### **List集合是线程不安全的,你是怎么使用List集合的呢?**

使用Collections集合工具类,对集合进行同步处理:

 List list = Collections.synchronizedList(new ArrayList<>()); 

但是在多线程开发中,对其进行遍历,需要添加 synchronized 关键字,因为List的 add、index 等方法中都是带有synchronized 关键字,但是在 iterator 中没有synchronized 关键字。



# ()set集合

特点是**存储有序不可重复**。

1.**HashSet**底层是HashMap,放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT(当前的对象),它是一个静态的 Object 对象。

2.**TreeSet**的底层则是使用红黑树,可以使用自然排序(自定义类中实现Comparable接口,重写CompareTo方法)或比较器排序(在创建TreeSet对象创建一个Comparator的匿名内部类,并重写Compare方法),扩容时通过结点链接。



### ()HashMap    key可以存null

### **HashMap的底层实现原理吗?**

**简单来说HashMap是一种基于Map接口的一种键值对,Value>结构的实现。HashMap的Key和Value都允许为null,但最多只允许一条Key为null,HashMap是无序的,非同步的,也就是说它是线程不安全的。**

1.8以前**HashMap是数组+链表**结构

1.8以后是**数组+链表+红黑树**结构,**元素是无序且不可重复**。为什么无序?因为元素存储位置是根据hash值和数值长度计算出来的,hash不确定,所以无序。为什么不可重复?因为如果下标相等时会判断hash值是否相等、==是否相等、equals是否相等。如果都相等会用新的值替换旧的值。【键相同值覆盖】

创建一个空的HashMap时初始容量是0,当第一次调用put()方法时,初始容量会设置为16。16是个经验值。

### **==Put()添加方法==**

​     假设添加元素a,会根据**key的hash值和数组的长度**计算出存储下标,如果**当前位置没有元素**会直接存在该位置。当添加b元素时,会先根据key的hash和数组长度计算b元素的存储下标,如果**b和a元素的下标相等**,会判断hash值是否相等,如果不相等,则会把划出一个节点来存放b元素,与a元素形成一个链表。如果hash值相等,就会发生哈希冲突,此时会判断**==、equals()**方法判断key的内容是否相等,如果相等则会覆盖a的值。当**链表的长度>阈值(8)的时候且数组长度>64**的时候会由链表转为**红黑树**。当不断添加元素的时候会触发hashMap的扩容机制。如果**链表的长度>8,但数组的长度<64时,会先扩容,再树化**。

![image-20221105180913364](C:\Users\chaijt\AppData\Roaming\Typora\typora-user-images\image-20221105180913364.png)







### **==HashMap的扩容机制是怎么样的?它什么时候会转化为红黑树?==**

Hash表中数组的分手手动初始化,和自动初始化,自动初初始会在第一次插入元素时开辟空间,**默认长度为16**,**扩容因子为0.75**,每次扩容量为**自身的2倍**长度,扩容之后存入数组的新索引位置就会改变。手动初始化的话,可以在创建对象时自定义初始数组长度,但HashMap不一定会自主设置的数值初始化数组,而按2的n次方创建。

**HashMap1.7版本**的的扩容时机是**先判断是否达到阈值**,达到先扩容,再添加元素,如果当前下标位置是null,不会扩容。使用的是**头插法**(新元素 + 旧元素),多线程情况下会出现**丢值**(put方法)或者**环链**问题(resize)。(两者必须满足)

而**HashMap1.8**的扩容时机是**先添加元素**是否达到阈值,达到直接扩容,且使用的是**尾插法**,即新元素挂在旧元素下面。(满足一个即可)

​        初始化后,当存入新的键值对时,会先判断数组长度是否大于64,再判断链表元素是否大于等于8时,如果两者都成立,链表会自动转换成红黑树,如果数组小于64,会从第9个开始先扩容,直到数组大于等于64时,链表长度再增加,就会转为红黑树。

细节:

​        在**添加第一个元素的时候是直接添加进数组**的,而**不会进入到红黑树转化的判断**的,所以里面的binCount并没有创建。添加第二元素并发生了哈希冲突时,才进入红黑树转化的判断,同时初始化binCount=0,它判断的是binCount>=7,也就是0至7,有8个元素时,再加上没有进行判断的1个元素,即第9个元素时,才会转化为红黑树。

### ==为什么一定要用红黑树(RB-Tree)?为什么不用平衡二叉树(AVL)?==

①红黑树不追求"完全平衡",即不像AVL那样要求节点的 |balFact| <= 1,它只要求部分达到平衡,但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。
就插入节点导致树失衡的情况,AVL和RB-Tree都是最多两次树旋转来实现复衡rebalance(协议),旋转的量级是O(1)
删除节点导致失衡,AVL需要维护从被删除节点到根节点root这条路径上所有节点的平衡,旋转的量级为O(logN),而RB-Tree最多只需要旋转3次实现复衡,只需O(1),所以说RB-Tree删除节点的rebalance的效率更高,开销更小!
AVL的结构相较于RB-Tree更为平衡,插入和删除引起失衡,RB-Tree复衡效率更高;当然,由于AVL高度平衡,因此AVL的Search效率更高啦。
针对插入和删除节点导致失衡后的rebalance操作,红黑树能够提供一个比较"便宜"的解决方案,降低开销,是对search,insert ,以及delete效率的折衷,总体来说,RB-Tree的统计性能高于AVL.
故引入RB-Tree是功能、性能、空间开销的折中结果。
② AVL更平衡,结构上更加直观,时间效能针对读取而言更高;维护稍慢,空间开销较大。
③ 红黑树,读取略逊于AVL,维护强于AVL,空间开销与AVL类似,内容极多时略优于AVL,维护优于AVL。
基本上主要的几种平衡树看来,红黑树有着良好的稳定性和完整的功能,性能表现也很不错,综合实力强



问题:

### **A. 为什么不直接使用红黑树?**

当链表的长度较短的情况下,链表的查询效率比红黑树高一些。红黑树占用的存储空间大于链表。

我记得我翻略HashMap源码时,看过其中有一段相关描述是: “因为树节点的大小是链表节点大小的两倍,所以只有在容器中包含足够的节点保证使用才用它”,显然尽管转为树使得查找的速度更快,但是在节点数比较小的时候,此时对于红黑树来说内存上的劣势会超过查找等操作的优势,自然使用链表更加好,但是在节点数比较多的时候,综合考虑,红黑树比链表要好。

### **B. 为什么转==红黑树的阈值==是==8==而不是第9第10个呢?**

源码中有对这个进行计算,正常的随机哈希码下,哈希冲突多到超过8个的概率不到千万分之一,几乎可以忽略不计了,再往后调整并没有很大意义。

### C.6与8之间的第7个冲突时,会是什么状态?

分情况看。8退6,是红黑树转链表,6进8,是链表转红黑树,中间的7是防止频繁变动做的一个预留位,如果是8退6,中间的7就是红黑树;如果是6进8,中间的7就是链表。

### **C. 极端情况下,HashMap最多能存储多少个元素不扩容?**

27个。添加第28个的时候扩容

### **D. HashMap的==数组长度==为什么==始终是2的幂次方==?**

因为2的n次幂在位运算下效率快。在HashMap的底层对于数组的操作其实是(n-1)&hash,当数组的长度为2的n次时,减1转为二进制后,他被任何数字&上都不会超过这个数字,比如数组长度为8,减1后为7,那么它的数组长度就是0-7,共8个,即元素可以在这个数组上全部排满,而如果是奇数,或者不是2的n次的偶数,一定会有一个二进制为0,也就是无论另一个数是什么,都不会被存入数组,会浪费掉的位置。

### **E. 多线程下的HashMap线程安全吗?为什么?**

不安全。并发情况下会出现丢失数据的情况。也会导致链表过程,环链等问题。

多线程下,在添加元素时容易出现数据覆盖情况而丢失数据,也可能在扩容时,迁移数据出现数据覆盖情况而丢失数据。

### **F.为什么==初始容量是16==?**

hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。

而作为默认容量,太大和太小都不合适。太小了就可能会频繁的发生扩容,影响效率;太大了又浪费空间,不划算,所以16就作为一个比较合适的经验值被采用了。

### **E.为什么HashMap的初始容量是2的n次幂?**

因为在计算下标时,2的n次幂位运算更快。如果想要元素分布均匀,可以将容量设置为质数。

### F.什么是==哈希冲突?==

哈希冲突就是两个元素在通过哈希函数后,得到的角标是相同的,在同一个哈希槽中。哈希冲突的四种解决思路分别是:重哈希法,开放地址法,建立公共溢出,链地址法。

### G.为什么==1.7是头插法==,==1.8是尾插法==?

1.7版本使用头插法是因为头插法是操作速度最快的,找到数组位置就直接找到插入位置了,但这样插入方法在并发场景下会因为多个线程同时扩容出现循环列表,也就是Hashmap的死锁问题。

1.8版本加入了红黑树来优化哈希桶中的遍历效率,相比头插法而言,尾插法在操作额外的遍历消耗(指遍历哈希桶)已经小很多,也可以避免之前的循环列表问题,同时如果已经变成红黑树了,也不能再用头插法了,而是按红黑树自己的规则排列了。

### H.如果是头插法,怎么才能获取之前的旧元素呢?

因为1.7版本的头插法,是新元素在上面,旧元素挂新元素后面,所以新元素始终是在数组上的,可以通过在对象上重写toString方法,加上对象的HashCode值,这样只要打印出来相同的HashCode说明发生了哈希冲突,这时候只需要遍历即可,要取哪个就指定那个HashCode,相同就取出,而上一个老元素就是第二个获取的元素。

### I:什么是HashMap双链循环/==死锁==?

双链循环是JDK1.7及更早的版本之前才有的问题。在多线程扩容的情况下,一个线程执行到一半,还未扩容,而另一个线程却抢走先行扩容了,这时候可能出现第一个线程的元素与第二个线程中的元素相互引用的情况,相互引用就会造成死锁。

比如一个数线长度为4,有两个数,一个为2,一个为10,那么这两个数都会在索引2上形成哈希桶结构,此时进行扩容,本来在新数组中是2指向10的,结果但之前那个前程正好断在10指向新数组的中间,这就会导至10又重新指向2,最终导while判断中的e永远不会等于null,造成死循环。

JDK1.8版本避免了双链循环,但不是完全避免,看过一些测试文章,红黑树之间也可能出现死循环,只是比较1.7版本,几率降低。

### ==判断链表==是否有环?

方法1:
自定义双向链表的时候,继承List,重写Size方法,每add方法新增一个Node节点,就Size++, 保证调用 XXX.Size() ;的时候能够返回自定义链表的长度。

定义一个checkCycle方法,判断是否有环就很简单了,只需要不断的getNextNode ; 同时定义一个 height ++ ; 只要height自增大于 XXX.Size() ; 那就肯定是有环,不然不可能遍历深度height 会超过 总大小 Size 。但是第一种方法对Java的List接口强依赖,如果换个C,换个PY,编码量就更大了,很明显不符合算法短小精悍的特征。



方法2:

经典的快慢指针法
定义一个慢指针,步长为1,定义一个快指针,步长为2,这样慢指针走一步,快指针走三步,当快指针追上了慢指针,证明循环了。



### J:==1.8版本==是否==完全避免死循环==问题?

不能。1.8版本中引进了红黑树,可以降低死循环出现的机率,但不能完全避免,红黑树之间也可能出现死循环。



### K:==环链==问题==如何发生的==

使用头插法转移对象时,同样的处理逻辑在不同线程中执行时,当一个线程的已经结束,此时是一根单链条,从头到尾结尾,越早进来的就在靠近尾节点处。

若此时另一个线程的首元素即新数组的尾节点,还刚开始执行。此时将这个尾节点的next再指向于这整个单链条的首节点,这时候就出现了环链现象。

JDK1.8版本避免了双链循环,但不是完全避免,看过一些测试文章,红黑树之间也可能出现死循环,只是比较1.7版本,几率降低。



### ==L:hashmap和hashtable的区别==

HashMap是线程不安全的(多线程环境下会出问题);

Hashtable是线程安全的(但效率低下);

Hashtable底层和哈希表一样,扩容因子是0.75,扩容倍率是2倍。

Hashtable一次只能执行一个线程(全表加锁),采取悲观锁(增善改的方法上都加了synchronized)保证了线程安全。



### M:==**fail-safe** 机制==与 ==**fail-fast** 机制==分别有什么==作用==

fail-safe 和 fail-fast,是多线程并发操作集合时的一种失败处理机制。

Fail-fast:表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了, 会立刻抛出 ConcurrentModificationException 异常,从而导致遍历失败,像这种情况。定义一个 Map 集合,使用 Iterator 迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生 fail-fast。java.util 包下的集合类都是快速失败机制的,常见的的使用 fail-fast 方式遍历的容器有 HashMap 和 ArrayList 等。

Fail-safe:表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出 ConcurrentModificationException。

原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的, 而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到

比如这种情况,定义了一个 CopyOnWriteArrayList,在对这个集合遍历过程中, 对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。

java.util.concurrent 包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。

常 见 的 的 使 用 fail-safe 方 式 遍 历 的 容 器 有 ConcerrentHashMap 和CopyOnWriteArrayList 等。



### ==hashmap线程不安全有什么解决办法?==

使用线程安全的集合**ConcurrentHashMap**,首先在JDK1.7的时候,它是用**synchronized**同步代码块形式保证线程安全的,默认长度是16,默认分配16个哈希槽,第一次插入新元素时,会根据键的哈希值来计算出在数组中存入的位置,但它的*数组一旦创建就无法扩容。那么在JDK1.8的时候,它的底层原理是:Node + CAS + sync(同步) 实现了每个节点一把锁,它的*加载机制会变为在第一次添加元素的时候判断长度是否为0,如果是的话就初始化元素



### hashmap中==自定义类作为key==  需要做什么处理?

hashMap的key是一个对象时,这个对象类中需要重写hashcode和Eq

防止这两个对象的地址值不一样但是属性一模一样





##  () **TreeMap**

TreeMap实现了SortedMap接口,保证了有序性。默认的排序是根据key值进行升序排序,也可以重写comparator方法来根据value进行排序具体取决于使用的构造方法。**不允许有null值null键**。TreeMap是线程不安全的。

TreeMap 中key 可以自动对 String 类型或8大基本类型的包装类型进行排序,但是,TreeMap 无法直接对自定义类型进行排序。当我们想对 TreeMap 中 key 中的自定义类型排序时,必须要指定排序规则。排序规则有两种:

1.**比较器排序**:创建对象时带参,匿名(new Comparator),重写 compare()方法。

2.**自然排序**:实现Comparable接口,重写CompareTo方法







## ConCurrentHashMap   key不能存null

构造参数:capacity元素个数  factor加载因子  clevel并发度

在JDK1.7的时候,底层是**Segment + 数组(HashEntry) + 链表**ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 **Node 数组+链表+红黑树**的数据结构来实现,并发控制使用 synchronized 和 CAS (比较与交换)来操作。

扩容:将数组迁移到新数组中,会重新计算元素的hash值,而且是最后一个节点开始迁移。迁移完会做个标记(forwordingNode),让其他线程知晓。



###### HashTable --线程安全(已经过时了)

初始容量是11,加载因子是0.75。扩容大小为2n+1。(n为数组大小),底层是 synchronized 悲观锁, 一锁锁全部



###### ==说一说ConcurrentHashMap,JDK7版本跟JDK8版本有什么不同?==

ConcurrentHashMap1.7版本:

**创建对象**

1、默认创建一个**长度16**,**加载因子为0.75的大数组**,但这个大数组一但创建无法扩容,所以加载因子是给小数组用的。

2、还会创建一个**长度为2的小数组**,把地址值赋值给0索引处。其他索引位置的元素均为null。

 **插入元素**

第一次插入新元素时,会根据键的哈希值来计算出在大数组中应存入的位置。

如果为null,则按照模板创建小数组,大数组只用来存放地址值。创建完毕,会进行二次哈希,计算出在小数组中应存入的位置直接存入。

如果不为null,就会根据记录的地址值找到小数组。二次哈希,计算出在小数组中应存入的位置。

如果需要扩容,则先将小数组扩容2倍。

如果不需要扩容,则判断小数组的这个位置有没有元素。

如果没有元素,则直接存。

如果有元素,就会调用equals方法,比较属性值

如果equals为true,相同则不存;

如果equals为false,新元素替换老元素,老元素挂在新元素下面,形成哈希桶结构(链表)。

**线程安全**

1、用synchronized同步代码块形式保证线程安全,锁住大数组的一个地址值连同它的小数组。

2、最多同时访问16个线程,因为大数组只有16个哈希槽。

 

###### **ConcurrentHashMap1.8版本:**

区别1.7:

· 底层结构改变:

哈希表(数组会扩容)——【数组】+【链表】+【红黑树】

1.7是旧元素挂新元素下面(旧挂新——头插法);1.8是新元素挂在旧元素下面(新挂旧——尾插法)!

线程安全机制改变:结合CAS机制+synchronized同步代码块形式保证线程安全。

如果该索引为null,则利用cas算法,将本结点添加到数组中。(第一次添加用CAS算法)

如果该索引不为null,则利用volatile关键字获得当前位置最新的结点地址,新元素挂在旧元素下面。

有元素后,再对该元素进行操作时,会给头结点(第一个元素)做为锁对象,加上synchronized同步代码块(锁对象的方式),保证线程安全。

·加载机制改变:懒加载——第一次添加元素时初始化数组。(添加元素时,判断数组是否为空,或者长度为0,如果是,就初始化数组)





###### ConcurrentHashMap 的==size()方法==是线程安全的吗?为什么

ConcurrentHashMap 的 size()方法是非线程安全的。

也就是说,当有线程调用 put 方法在添加元素的时候,其他线程在调用 size()方法获取的元素个数和实际存储元素个数是不一致的。原因是 size()方法是一个非同步方法,put()方法和 size()方法并没有实现同步锁。put()方法的实现逻辑是:在 hash 表上添加或者修改某个元素,然后再对总的元素个数进行累加。其中,线程的安全性仅仅局限在 hash 表数组粒度的锁同步,避免同一个节点出现数据竞争带来线程安全问题。数组元素个数的累加方式用到了两个方案:当线程竞争不激烈的时候,直接用 cas 的方式对一个 long 类型的变量做原子递增。当线程竞争比较激烈的时候,使用一个 CounterCell 数组,用分而治之的思想减少多线程竞争,从而实现元素个数的原子累加。size()方法的逻辑就是遍历 CounterCell 数组中的每个 value 值进行累加,再加上 baseCount,汇总得到一个结果。所以很明显,size()方法得到的数据和真实数据必然是不一致的。因此从 size()方法本身来看,它的整个计算过程是线程安全的,因为这里用到了CAS 的方式解决了并发更新问题。但是站在 ConcurrentHashMap 全局角度来看,put()方法和 size()方法之间的数据是不一致的,因此也就不是线程安全的。之所以不像 HashTable 那样,直接在方法级别加同步锁。在我看来有两个考虑点。直接在 size()方法加锁,就会造成数据写入的并发冲突,对性能造成影响,当然有些朋友会说可以加读写锁,但是同样会造成 put 方法锁的范围扩大,性能影响极大! ConcurrentHashMap 并发集合中,对于 size()数量的一致性需求并不大,并发集合更多的是去保证数据存储的安全性。



## **ThreadLocal**

1.实现【资源对象】的线程隔离,每个线程都有自己的【资源】,避免争用引起线程安全。

2.实现了线程类资源的共享。

`ThreadLocal t1 = new ThreadLocal();`

`t1.get():从当前线程取值`

`t1.set():给当前线程存值`

`t1.remove():删除值`

原理:每个线程内都有一个ThreadLocalMap类型的成员变量,用来储存资源对象,以ThreadLocal本身作为key,资源对象作为value,放入当前的ThreadLocalMap集合中。



### 为什么说ThreadLocal的==key是弱引用==?

根据ThreadLocalMap对key和value的构造:

在set方法中,会将key和value以entry对象进行存储,key就是我们的Threadlocal引用,value就是我们要设置的值。

由Entry这个类可以发现,它继承了WeakReference类,并且在构造方法中,将key设置成了弱引用,而value则是强引用。

为什么要这样做?

要知道,ThreadlocalMap是和线程绑定在一起的,如果这样线程没有被销毁,而我们又已经不会再某个threadlocal引用,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。

为了避免这种情况,只要将key设置为弱引用,那么当发生GC的时候,就会自动将弱引用给清理掉,也就是说:假如某个用户A执行方法时产生了一份threadlocalA,然后在很长一段时间都用不到threadlocalA时,作为弱引用,它会在下次垃圾回收时被清理掉。

而且ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。







### **你了解迭代器吗?说说==迭代器==。**

从两方面来说,从底层原理上来说是大同小异的,底层通过hasnext()方法判断当前当前指向是否有元素,而指针一开始就会指向第一个元素。如果有,就会再执行next()方法,取出当前游标,并向后移一位;如果hasnext()方法返回为null,就说明不存在下一个元素,就会迭代结束。针对不同集合,会有一些细节不同,(比如HashMap就要考虑到红黑树里的取出顺序)。再底层则是采用C语言的指针原理。

在应用层面上,迭代器就是用来遍历的。所有的单列集合都可以使用迭代器,因为他们都继承了Iterable接口,这个接口里的Iterator方法可以返回一个Iterator对象,这个Iterator对象就是迭代器对象,底层针对不同类型的集合都写了不同的实现类,所以集合可以直接使用迭代器进行遍历查询。这种只需要提供一种方法(iterator方法)访问一个容器对象中各个元素,而又不暴露该对象的内部细节的方式就是迭代器设计模式。

但要注意的是,如果用的是直接使用增强for和用迭代器方式(iterator)是不一样的,后者可以直接修改、删除原数据,但增强for是引用了一个第三方变量进行遍历,只适合单纯遍历。	



###### **但是iterator是自动取下一个,如果需要倒着遍历怎么办?**

List集合可以使用迭代器倒着遍历,ListIterator有previous()方法和hasprevious()方法,可以自动指向并取出上一个元素。Set集合不能用迭代器倒着遍历,但可以根据它的大小顺序倒着取出。



###### 使用==foreach、iterator、for==在有什么区别?效率上哪个更高?

**区别上:**

普通for循环一般用来处理比较简单的有序的,可预知大小的集合或数组.

foreach可用于遍历任何集合或数组,而且操作简单易懂,唯一的不好就是需要了解集合内部类型,它的底层有函数式编程注解 @FunctionalInterface,也就是说它可以进行Lamda形式简写。

iterator是最强大的,他可以随时修改或者删除集合内部的元素,并且是在不需要知道元素和集合的类 型的情况下进行的,当你需要对不同的容器实现同样的遍历方式时,迭代器是最好的选择!

至于增强for和iterator其实是一样的,增强for编译后的.class文件中,就是iterator,所以二者除了写法是用第三方参数来表示,效率上没有任何区别。

**效率上:**

这个需要多方考虑,比如普通for循环用在数组是遍历最快的,它是直接获取数据,但普通for不能用在不知道长度的集合中,这就需要用iterator或者foreach,相对来说,iterator效率会高于foreach,因为foreach在访问过程中产生一个额外的Enumerator对象,这个对象会进行版本检查处理,所以它是线安全的。

对于ArrayList来说,它是通过一个数组实现的,可以随机存取;但是LinkedList是通过链表实现的,当要遍历依个取出时,for循环时要取的每个元素都必须从头开始遍历,而iterator遍历则从头开始,边遍历边取,取完只需要一次遍历,所以for循环需要的时间远远超过for循环。 
对于数组来说,for和foreach循环效率差不多,但是对于链表来说,for循环效率明显比foreach低。

你可能感兴趣的:(python,开发语言)