HashMap和ConcurrentHashMap区别
HashMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
- HashMap的初始值还要考虑加载因子:哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
- HashMap的内部结构可以看作是数组(Node[] table)和链表的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组中的寻址(哈希值相同的键值对,则以链表形式存储)
ConcurrentHashMap
- 底层采用分段的数组+链表实现,线程安全
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
- 1.7分段锁,对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
- 1.8 优化的synchronized 关键字同步代码块 和 cas操作了维护并发
ConcurrentHashMap的数据结构
jdk1.7由一个Segment数组和多个HashEntry组成,分段加锁
jdk1.8用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作
高并发HashMap的环是如何产生的?
扩容调用transfer方法,首先获取新表的长度,之后遍历新表的每一个entry,然后每个ertry中的链表,以反转的形式,形成rehash之后的链表。
若当前线程此时获得ertry节点,但是被线程中断无法继续执行,此时线程二进入transfer函数,并把函数顺利执行,此时新表中的某个位置有了节点,之后线程一获得执行权继续执行,因为并发transfer,所以两者都是扩容的同一个链表,当线程一执行到e.next = new table[i] 的时候,由于线程二之前数据迁移的原因导致此时new table[i] 上就有ertry存在,所以线程一执行的时候,会将next节点,设置为自己,导致自己互相使用next引用对方,因此产生链表,导致死循环。
volatile作用
可见(修改推到主存)、防止指令重排(添加内存屏障,防止指令重排)
Atomic类如何保证原子性
atomic 内部的value 使用volatile保证内存可见性,使用CAS(compare and swap)保证原子性。
内存值 V 预估值 A 更新值 B,如果V和A相同时(compare),才会把B赋值给V(swap),否则什么都不做。
synchronized和Lock的区别
- Synchronized是关键字,内置语言实现,Lock是接口。
- Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
- Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。
- Lock可以使用读锁提高多线程读效率。
为什么要使用线程池?
为了减少创建和销毁线程的次数,让每个线程可以多次使用。可根据系统情况调整执行的线程数量(根据cpu运行时间和等待时间计算),防止消耗过多内存,所以我们可以使用线程池。
java中线程池的顶级接口是Executor,ExecutorService是Executor的子类,也是真正的线程池接口,它提供了提交任务和关闭线程池等方法。调用submit方法提交任务还可以返回一个Future(fei 曲儿)对象,利用该对象可以了解任务执行情况,获得任务的执行结果或取消任务。
由于线程池的配置比较复杂,JavaSE中定义了Executors类就是用来方便创建各种常用线程池的工具类。通过调用该工具类中的方法我们可以创建单线程池(newSingleThreadExecutor),固定数量的线程池(newFixedThreadPool),可缓存线程池(newCachedThreadPool),大小无限制的线程池(newScheduledThreadPool)
比较常用的是固定数量的线程池和可缓存的线程池。
固定数量的线程池是每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行。
可缓存线程池是当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
核心线程池ThreadPoolExecutor的参数
corePoolSize
核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。
核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
maxPoolSize
当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
keepAliveTime
当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
allowCoreThreadTimeout
是否允许核心线程空闲退出,默认值为false。
queueCapacity
任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。
ThreadPoolExecutor的工作流程
- 核心线程数未满,直接创建新的核心线程来执行任务,若有空闲线程,复用空闲线程
- 核心线程数已满,要执行的任务加入堵塞队列
- 核心线程数已满,堵塞队列已满,创建非核心线程来执行任务(非时序关联的流程,非核心线程若空闲,并且到达存活时间,会被回收),若有空闲线程,可复用
- 核心线程已满,堵塞队列已满,非核心线程已满,任务会被拒绝执行,调用RejectedExecutionHandler
如何控制线程池线程的优先级
Thread.currentThread.setPriority(1)
线程之间如何通信
- volatile
- wait、notify
- countdownlatch和栅栏
java基本类型各占几个字节
- byte 1字节容
- short 2字节
- int 4字节
- long 8字节
- float 4字节
- double 8字节
- char 2字节
- boolean 1字节
jdk1.8/jdk1.7都分别新增了哪些特性?
jdk1.7新特性
泛型实例
的创建可以通过类型推断
来简化,可以去掉
后面new部分的泛型类型,只用<>
就可以了。并发工具增强
: fork-join框架最大的增强,充分利用多核特性
,将大问题分解
成各个子问题,由多个cpu 可以同时 解决多个子问题
,最后合并结果
,继承RecursiveTask,实现compute方法,然后调用fork计算
,最后用join合并结果
。try-with-resources语句
是一种声明了一种或多种资源的try语句
。资源是指在程序用完了之后必须要关闭的对象。try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭
。任何实现了java.lang.AutoCloseable
接口的对象,和实现了java .io .Closeable
接口的对象,都可以当做资源使用
。Catch多个异常
:在Java 7中,catch代码块得到了升级,用以在单个catch块中处理多个异常
。如果你要捕获多个异常并且它们包含相似的代码,使用这一特性将会减少代码重复度。下面用一个例子来理解。
jdk1.8新特性
- jdk1.8对hashMap等map集合的优化
- Lambda表达式
- 函数式接口
- 方法引用和构造器调用
- Stream API
- 并行流和串行流
- Optional容器
- 接口中的默认方法和静态方法
- 新时间日期API
- 定义可重复的注解
- 扩展注解的支持
- jvm中的
方法区
变成了元数据区
(PermGen
变成了Metaspace
) 更好的类型推测机制
(不需要太多的强制类型转换了)- Java 8
将方法的参数名加入了字节码中
,这样在运行时通过反射就能获取到参数名
,只需要在编译时使用-parameters参数。
Exception和Error
Exception和Error都是继承于Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception是java程序运行中可预料的异常情况,咱们可以获取到这种异常,并且对这种异常进行业务外的处理。
Error是java程序运行中不可预料的异常情况,这种异常发生以后,会直接导致JVM不可处理或者不可恢复的情况。所以这种异常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。