持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情
集合、线程池
1.ArrayList与LinkedList的区别
- 两者都是线程不安全的。
- 底层实现:
- ArrayList底层是Object[]数组实现。
- LinkedList底层使用双向链表。
- 插入和删除影响:
- ArrayList采用的是数组存储,如果插入的是最后一个元素,可以直接插入;如果插入的位置不是最后一个,那就需要移动元素的位置。如果数组容量已满,需要进行扩容(扩容为原来容量的2倍),即创建一个容量为原来2倍的数组,需要将原数组数据复制到新数组。作元素删除也一样需要对删除位置之后的元素往前移动。故时间复杂度会受元素位置移动的影响。
- LinkedList采用链表存储,采用头插尾插法都不会产生位置移动的影响[add(e)、addFirst(e)、addLast(e)、removeFirst()、removeLast(),时间复杂度近似O(1)]。如果要在指定位置插入元素add(i, e),remove(e),时间复杂度近似O(n),只需先移动到指定位置插入或删除即可。
- 随机访问:
- ArrayList是数组存储,所以根据数组下标查找,就能够随机访问。
- LinkedList不支持高效随机访问。
- 内存空间占用:
- ArrayList的末尾会预留一定容量的空间造成浪费。
- LinkedList每个元素会额外消耗一部分空间,用于存储链表上下节点指针。
2.HashMap底层数据结构
- JDK1.8之前
- 底层有动态数组+链表实现
- HashMap通过key的hashCode经过扰动函数(hash方法为了减少hash碰撞)处理后得到hash值,然后通过(n-1)&hash判断当前元素的存放位置(n指数组长度),如果当前位置存在元素,判断该元素与要存入的元素hash值和key值是否相同,如果相同直接覆盖,不同就使用拉链法解决冲突。
- JDK1.8之后
- 链表长度大于阈值(默认8)时,首先会调用treeifyBin()方法,然后根据HashMap数组长度判断,大等于64的情况下就转换为红黑树,以减少搜索时间。否则就只调用resize()做数组扩容。
3.HashMap扩容机制
- JDK1.7
- 第一次put会初始化数组,容量为不小于指定容量的2的幂数,然后根据负载因子确定阈值。
- 如果不是第一次扩容,则新容量等于旧容量的两倍,新阈值等于新容量*负载因子。
- JDK1.8
- new HashMap()默认空数组,即没有实例化数组。第一次调用put方法会开始第一次扩容,初始数组长度为16。
- 如果不是第一次扩容,则容量跟阈值都变为原来的2倍。
4.ConcurrentHashMap存储结构
- ConcurrentHashMap的HashEntry的value是volatile修饰的,对于读操作不加锁,也能保证读取到最新的值。
- JDK1.7中使用的是分段锁Segment,也就是每一个Segment上同时只能有一个线程操作,每一个Segment都类似HashMap的数组结构,可以扩容,冲突会转化为链表。但是Segment的个数初始化后就不能改变,默认是16个。
- JDK8中使用Synchronized锁和CAS机制实现,结构也有1.7的Segment数组+HashEntry+链表变成Node数组+链表/红黑树,Node类似一个HashEntry的结构,它的冲突达到一定的大小会转化为红黑树,在冲突小于一定数量又会转化为链表。
5.线程池大小如何设置
- CPU密集型(N+1):这种任务主要消耗CPU资源,可以将核心线程数设置为N(CPU核心数)+1,比CPU多出来一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来一个线程就可以充分利用CPU的空闲时间。
- I/O密集型(2N):这种任务应用处理,系统会用大部分时间来处理I/O交互,而线程正在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其他线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程(2N)。
6.线程池运行流程
- 线程池运行一个任务,首先判断当前线程数是否小于核心线程数,如果是则创建一个新的线程执行任务。
- 如果当前线程数大等与核心线程数,则将任务放入BlockingQueue阻塞队列。
- 如果阻塞队列已满,且当前线程数小于最大线程数,则创建新线程执行该任务。
- 如果阻塞队列已满,且当前线程数大等于最大线程数,则抛出异常(采用拒绝策略)
- 拒绝策略:
- 默认拒绝策略:抛出异常RejectedExecutionException
- 直接丢弃任务,不抛异常
- 丢弃队列最前面的任务,然后重新提交当前任务
- 使用当前线程直接运行该任务
- 线程池核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:线程池最大线程数
- keepAliveTime:空闲线程存活时间
- unit:空闲线程存活时间单位
- workQueue:阻塞队列
- threadFactory:线程工厂
- handler:拒绝策略
7.线程的生命周期
- 就绪状态(Runnable):一个线程对象创建后,其他线程调用它的start()方法,该线程就会进入就绪状态,这个线程就处于可运行池中,等待获得CPU使用权。
- 运行状态(Running):处于这个状态的线程占用CPU,执行程序代码,只有处于就绪状态的线程才有机会转到运行状态。
- 阻塞状态(Blocked):调用了sleep()、wait()线程就会进入阻塞状态,阻塞状态是指某些原因线程放弃CPU,暂时停止运行。处于该状态的线程不会被分配到CPU,直到线程重新进入就绪状态,才有机会转到运行状态。
- 死亡状态(Dead):当线程执行异常,或者正常执行结束退出run()方法时,就进入死亡状态,该线程生命周期结束。
8.ThreadLocal
- ThreadLocal是一个解决线程并发问题的类,用于创建线程的本地变量,一个对象的全局变量会被所有线程共享,所以这些变量不是线程安全的,可以使用同步技术。当有些场景不想使用同步技术的时候,可以使用ThreadLocal变量,为每一个线程单独创建一个变量副本,在整个线程的生命周期中可以随时操作访问。
- 每个Thread线程内部都用一个ThreadLocalMap,以线程作为key,泛型作为value,可以理解为就是线程级别的缓存。每一个线程都会获得一个单独的map。
- 提供了get、set、remove方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get方法总是返回由当前线程在调用set时设置的最新值。
- 应用场景: JDBC连接、Session管理、Spring事务管理、AOP