Java集合再探
谈谈你对集合的理解?
集合首先分为两个集合和一个工具类,分别是Collection接口、Map接口以及Collections工具类
- 首先是Collection接口,也就是单例集合,它的子接口有List接口、Set接口、Queue接口
List接口首先是有序且允许重复,它的常见实现类
- 有线程不安全的,例如:
- ArrayList,底层是一个Object[]类型的elementData数组,适合查找、遍历,而插入\删除的效率比较低。它的无参构造方法首先是数组初始化为0,在第一次添加元素时,数组容量扩容为10;而有参构造方法,数组按指定容量初始化;当容量不足时,按数组现有容量的1.5倍扩容
- LinkedList,底层是一个双向链表,在每个节点里存入了上个节点和下个节点的地址,适合插入、删除,而查找\遍历的效率比较低。它由于采用链表结构,每次添加元素都在创建新的Node节点并进行分配空间,所以不存在扩容。
- 有线程安全的,例如:
- Vector,底层也是一个Object[]类型的elementData数组,因为底层加了synchronized同步锁来实现线程安全,因此效率较低;它的扩容方式,无参构造方法,数组的初始化容量为10;而有参构造方法,数组按指定容量初始化;当容量不足时,按照原有的容量的2倍或者按照指定容量值(capacityIncrement)进行扩容。
- Stack,底层是一个基于先进后出FILO实现的栈,底层加了synchronized同步锁来实现线程安全,它继承自Vector类。
- CopyOnWriteArrayList,底层是一个Object[]类型的array数组,底层通过ReentrantLock锁来实现线程安全,由于底层是被final修饰的一把ReentrantLock锁,所以在不同的写操作(set、add、addIfAbsent、remove等方法)时,会保持线程之间的同步性(互斥),而读操作由于没加锁,所以允许读写操作同时进行;
CopyOnWrite:"写入"操作时,先进行数组复制,然后在新数组中进行写入操作,然后替换;
Set接口首先是无序且不允许重复,也就是值唯一,它的常见实现类
- 有线程不安全的,例如:
- HashSet,【特点:元素不重复且无序,允许null值】底层是HashMap。如果多个线程尝试修改HashSet,则最终结果是不确定的。
- LinkedHashSet,【特点:元素有序,维护了元素的插入顺序】底层是LinkedHashMap。不支持按访问顺序对元素排序,只支持按插入顺序排序
- TreeSet,【特点:可排序且去重,可以自定义排序规则】底层是TreeMap。底层用到的数据结果是红黑树。当我们构造TreeSet时;若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。
- 有线程安全的,例如:
- CopyOnWriteArraySet,底层其实是CopyOnWriteArrayList,特点和它相同,读写分离,可能会产生脏读,在写上面加了ReentrantLock写锁,保持线程之间的同步性(互斥)
Queue接口,队列,先进先出FIFO
- 有线程不安全的,例如:
- LinkedList,基于双向链表实现的队列,底层是Node节点
- PriorityQueue,基于堆实现的队列,底层是Object类型的数组
- 有线程安全的,例如BlockingQueue阻塞队列,队列满,写入线程会阻塞,队列空,读取线程会阻塞。
- ArrayBlockingQueue,有界队列,读写操作使用同一把ReentrantLock锁进行控制,所以读写操作不能并发。
- LinkedBlockingQueue,无界队列,读takeLock写putLock使用各自不同的ReentrantLock锁,所以允许读写操作允许并发。
- Map接口,键值对集合,
- 有线程不安全的,例如:
- HashMap,【特点:无序,查找效率高,根据key查找value】底层是数组(哈希表)+链表(链地址法解决哈希冲突)+红黑树(自平衡二叉查找树,提高查找效率)
关键计算:计算key-value键值对在哈希表中的存储位置(桶),哈希运算,hash( )扰动函数【(h = key.hashCode()) ^ (h >>> 16)】计算新的哈希值,通过新哈希值,计算下标位置(桶位置)【(n - 1) & hash】
转换成红黑树的条件:链表的元素个数超过8,并且数组长度大于等于64
红黑树退化成链表条件:红黑树中的元素个数少于6
转换成红黑树的优点:
链表过长,会导致搜索效率降低,使用红黑树提高查找效率;
红黑树,是一颗自平衡的二叉查找树,树中所有节点均自动排序,并且自平衡,可以使用二分查找,提高查找效率。
影响性能的关键参数:
数组容量:默认为16,容量越大,内存占用越多,产生冲突的概率越小,必须为2的N次幂,每次按照2倍进行扩容,最大容量不超过2的30次幂;
加载因子:默认为0.75,加载因子越高,代表利用率越高,产生冲突的概率越高,加载因子越低,代表利用率越低,产生冲突的概率越低;
扩容条件:默认情况下,数组长度为16(自定义时,必须保证数组长度为2^n 次幂)达到扩容阈值threshold,按照2倍进行扩容,链表长度大于8,数组长度小于64,链表不会转换成红黑树,执行数组扩容
- <font style="background-color:#FBDE28;">LinkedHashMap</font>【HashMap的子类】键值对按照有序方式存储,在HashMap的基础上,多维护了一条双向链表,用于存储顺序
- <font style="background-color:#FBDE28;">TreeMap</font>:【自动按照key排序】底层使用红黑树存储
- 有线程安全的,例如:
- Hashtable【无序、key唯一、key和value不允许为null、线程安全】,哈希表 = 数组 + 单向链表,通过Synchronized实现线程安全。
- ConcurrentHashMap【无序】,数组+链表+红黑树,通过Synchronized同步锁+CAS无锁化编程模型
- Collections工具类
ThreadLocal
ThreadLocal利用Thread中的ThreadLocalMap来进行数据存储。
内部有一个map对象,键为当前的线程对象,值为存储的数据value
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;/*** @Author Stringzhua* @Date 2024/11/1 10:19* description:*/
public class Demo01 {public static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static ThreadLocal<Integer> threadLocalInt = new ThreadLocal<>();public static void main(String[] args) {show show = new show();Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {try {threadLocal.set("妲己");threadLocalInt.set(10000);show.show();show.choose();} catch (Exception e) {e.printStackTrace();} finally {threadLocal.remove();threadLocalInt.remove();}}});Thread t2 = new Thread(new Runnable() {@Overridepublic void run() {try {threadLocal.set("张飞");threadLocalInt.set(100);show.show();show.choose();} catch (Exception e) {e.printStackTrace();} finally {threadLocal.remove();threadLocalInt.remove();}}});t1.start();t2.start();}
}class show {public void show() {String role = Demo01.threadLocal.get();Integer score = Demo01.threadLocalInt.get();System.out.println(Thread.currentThread().getName() + "当前角色为" + role + "积分" + score);}public void choose() {String role = Demo01.threadLocal.get();Integer score = Demo01.threadLocalInt.get();System.out.println(Thread.currentThread().getName() + "选择的角色为" + role + "积分" + score);}
}
常用的方法
get()方法
set()方法
remove()方法
ThreadLocalMap底层:
在ThreadLocal里面的内部类里的ThreadLocalMap
初始化容量为16,是一个Entry[]类型的哈希表;
向下探查,开放地址法,而不是链地址法
当发生哈希冲突时,首先判断此处是否有值,若没有,直接存入。如果有值,则直接覆盖
ThreadLocal做key的原因?
首先是通过每个ThreadLocal对象的key,通过具体对的ThreadLocal对象的getMap()方法拿到ThreadLocalMap对象;再以当前的ThreadLocalMap对象为键,拿到Entry对象;然后从Entry对象中取到值。
如果是Thread对象做key的话,一个线程只能有一个Thread对象的key,那么就会产生混淆的问题。
ThreadLocalMap怎么查找数据?
以ThreadLocal对象作为key,通过访问Thread当前线程对象的内部ThreadLocalMap集合来获取到Value