java基础知识个人理解
为了方便自己快速的回忆java相关基础知识
1.hashmap
java7中Hashmap在并发下有死循环、put或get方法并发问题。
死循环是因为hashmap在多线程同时扩容的时候,每个线程各自初始化新的table,再把原先的节点使用头插法链到新的table上,最终会导致节点间出现循环依赖,这样在get方法的时候死循环。java8中使用尾插法,解决了该问题。
java8的hashmap为了解决get方法复杂度为O(N)的问题,在链表数量大于8时(要先判断hash桶是否大于64,如果不大于64则先进行扩容),会将该链表转化为红黑树,从而使复杂度降为O(log(N))。
java8的concurrenthashmap抛弃了java以前的锁分段的用法,使用CAS+Synchronized实现并发安全。
普通hashmap并发不安全的地方主要在三个地方
1.初始化
concurrenthashmap中有个属性sizeCtl,具体含义如下
- 负数代表正在进行初始化或扩容操作
- 1代表正在初始化
- N 表示有N-1个线程正在进行扩容操作
- 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
在一个线程已经初始化时,使用CAS把sizeCtl状态变更,其他线程感知到有线程在初始化,使用Thread.yield()将线程挂起来解决并发问题。
2.put方法
- 如果table[i]要插入节点位置为Null,使用CAS把节点添加到table[i]上
- table[i]不为Null并且hash值=-1,说明该节点为forward节点(已经扩容完毕的节点),说明当前有其他线程正在扩容,要去协助扩容。
- 前两种情况都不满足,将table[i]节点使用Synchronized锁住,判断需不需要转变红黑树,再执行相应插入操作。
3.get方法
get方法完全无锁,通过hash计算要查找的table[i],再获取table[i]的hash值,如果值为-1,表明是forward节点,则调用该节点的find方法,在新的nextTable去查询节点。如果大于0,则在当前table[i]下寻找节点。
2.多线程相关
1.线程池
在Executors类下,提前帮我们实现了四种线程池分别为
Executors.newFixedThreadPool(x)
Executors.newCachedThreadPool()
Executors.newScheduledThreadPool(5)
Executors.newSingleThreadExecutor()
返回值为ExecutorService
使用该方法创建出的newFixedThreadPool 和newCachedThreadPool使用的无界队列,也就是LinkedBlockingQueue,会发生OOM,所以不建议使用
2.ThreadPoolExecutor
上述四种线程池其实也是调用的该类的构造函数生成的,只不过Executors帮我们封装了相应参数。
完整构造参数如下
- corePoolSize 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
- maximumPoolSize 线程池中线程的上限 超过该数量会使用拒绝策略
- keepAliveTime 非核心线程的最大空闲时间,如果非核心线程空闲的时间超过这个时间,该线程会被回收。
- workQueue 任务队列 当任务队列为BlockingQueue时,如果核心线程都不可用,则会在队列放满时,再新增线程运行任务。当任务队列为SynchronousQueue时,总会新开线程执行任务。
- RejectedExecutionHandler 当运行线程数大于maximumPoolSize时的拒绝策略 默认为抛出异常,提供了四种也可以自定义 1.抛出异常 2.直接忽略 3.丢弃最老的线程 4.由主线程执行
- ThreadFactory threadFactory, 新线程的产生方式,也就是给线程起个名称什么的。
3.ExecutorService
ExecutorService为Executor直接的扩展接口(Executor只有excute方法)
- ExecutorService.submit用于提交一个新线程到线程池中 ExecutorService.shutdown
- 用于停止线程池中的线程(会让线程执行完) ExecutorService.shutdownNow
- 用于停止线程池中的线程(返回未执行的线程列表,会发生线程中断异常InterruptedException)
- ExecutorService.invokeAll 接受一个线程列表,当所有线程运行完成返回Futrue集合
- ExecutorService.invokeAny 接受一个线程列表,当任一线程运行完成该线程的结果,取消其他正在进行的线程
4.线程状态
- 新建状态(New):当线程对象对创建后,即进入了新建状态
- 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,获取cpu 的使用权,并不是说执行了t.start()此线程立即就会执行
- 运行状态(Running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
- 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权
- 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
5.线程调用方法
- sleep():sleep为Thread提供的方法,执行该方法后,当前线程让出CPU(依然保留着资源,因此不能在同步代码中执行sleep方法)等待休眠时间结束后,重新竞争线程执行。
- wait():wait为Object的方法,调用wati方法会释放掉线程的所有资源(包括锁),并且只能被Notify方法唤醒,唤醒后,先获取对象锁。
- yield():与sleep方法相同,但区别是,只允许同优先级的线程获取CPU时间片。
6.JUC并发包
- Semaphore 基本类同于最原始的信号量,虚拟一个抽象资源池用于线程申请资源和释放资源。
- CountDownLatch 基本操作为countDown/await,主要用于保证线程顺序 或者 执行依赖的场景,比如说线程A要等待线程B(也可以是一批线程)运行完以后再执行自己的工作。并且CountDownLatch初始申请的数量是不会重置的。
- CyclicBarrier 只提供await操作,更加强调的是等待一批线程就位以后,再执行某项操作,比如说用于消息批量发送场景,等待积累了N条消息后,再进行发送。并且初始化数量可重置,能够重复使用。
- BlockingQueue 阻塞队列,该名称原因因为其相比于其他队列,额外提供了阻塞插入获取操作 take(如果队列为空,等待队列有值再返回) 、poll(超时阻塞)、 put (如果队列已满,等待队列有空闲再插入)操作、offer(超时阻塞),这两个操作就是阻塞执行的。在消费者-生产者模式下,就不用开发自己再处理循坏等待相应的场景了。
3.垃圾回收
java8默认的垃圾收集器为 parScavenage + parOld
3.1 标记清除(mark-sweap)
正如其名称一样,在扫描对象时,对于可回收的对象置上标记,在回收阶段,把标记过的对象全部回收掉。但是会产生内存碎片,影响后续的对象分配,所以一般只会用于老年代中(老年代中的对象只要很少一部分会回收掉,所以产生的碎片不多)
3.2 复制算法
为了解决标记清楚算法中的内存碎片问题,复制算法出现了。复制算法额外使用了一倍的空间用于存放存活的对象,而原先的空间就可以全被回收了,不足就是会额外消耗空间,所以一般常用于年轻代(年轻代中每一次可回收的对象占比比较大,且大对象一般不会放在年轻代中)
3.3 G1
1.概述,g1整体上使用了复制算法,解决了CMS中的内存碎片问题,并把所有的堆内容分为了region,通过计算不同region的回收效率,根据指定的目标选择合适的region进行回收
2.region,region分为四种
- E=Eden
- S=Survivor 当E的对象移动到S时,如果空间不够,则全部转移到O中,并将E整体回收
- O=Old
- H=humongous 用于存放大对象(超过region 50%以上)
3.Remembered Set
对象之间的引用会不可避免的出现跨代引用的情况,对于old->young这种例子,在回收时young,必须要扫描old,开销太大,所以在每个region上定义了RS(为了维护RS,在改变引用前添加写屏障,检查是否存在跨代引用,如果存在,把引用的对象添加到RS中),RS存放的时有那些old region引用了本region的对象(谁引用了我),这样在进行扫描时,只需要把RS中的对象作为root即可。
4.young gc
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发,整个回收阶段都要STW。
- 阶段1:根扫描 静态和本地对象被扫描
- 阶段2:更新RS 处理dirty card队列更新RS
- 阶段3:处理RS 检测从年轻代指向年老代的对象
- 阶段4:对象拷贝 拷贝存活的对象到survivor/old区
- 阶段5:处理引用队列 软引用,弱引用,虚引用处理
5.full gc
由于full gc会先触发一次young gc 所以 初始标记与young gc的初始标记是可以复用的
- 初始标记(STW 耗时很短) 在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
- 根区域扫描(与用户线程并行执行) G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
- 并发标记(Concurrent Marking) G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
- 最终标记(STW) 最终确定标记(耗时长),清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理(将RS log中的记录合并到RS中,RS log为并发期间对象引用的变化记录)。
- 清除垃圾(STW) 在这个最后阶段,执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
由于存在与用户线程并发情况,所以引入了SATB(Snapshot-At-The-Beginning)机制,确保垃圾回收的正确性。在GC开始时,创建一个堆的对象引用快照,并使用了三色标记算法,来标记对象的引用状态。在并发标记阶段,如果对象的引用状态发生改变,则将这些对象置灰,表明对象不可回收,因此G1回收会产生浮动垃圾(float garbge),当浮动垃圾过多时,G1会退化使用serial回收堆空间。
6.三色标记算法
- 黑色:根对象,或者该对象与它的子对象都被扫描
- 灰色:对象本身被扫描,但还没扫描完该对象中的子对象
- 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
G1总结:
- G1为跨越新生代和老年代的垃圾回收算法
- 通过的region的方式进行垃圾回收,可以在吞吐量和停顿时间进行一定范围的选择
- G1的STW时间主要消耗在对象转移和最终标记阶段,其中对象转移是大头,并且堆空间越大,存活对象越多,则对象转移耗时也就越长。
3.4 安全点和安全区域
无论是那种GC收集器,在寻找所有root根节点的时候都是需要暂停用户任务的。但是简单粗暴的直接暂停当前所有任务的运行显然是不合理的,在hotSpot中存在安全点(SafePoint),当线程发现当前需要中断时,就运行到下一个安全点中暂停当前任务。对于休眠的线程来讲,因为无法运行到下一个安全点,所以又新增了安全区域的概念,表明在安全区域中,对象的引用不会发生改变。
4.即时编译器JIT
1.热点代码
对于热点代码,JVM会启用编译器将热点代码编译为本地代码。
2.方法内联,减少方法栈的数量
3.栈上分配
将部分变量和对象直接分配到方法栈中,减少额外的GC
5.异常
根据大类,java中异常可分为Error和Exception
其中Exception又可分为以下几种
- 非检查异常
又可称为RuntimeException,在代码中不需要强制处理,例如最常见的NPE空指针异常。 - 检查异常
在代码中必须要被处理的异常(要么try catch 要么往上抛),比如说IOExcepiton
6.缓存
- 缓存穿透
接口所查询的数据为空,导致缓存无法生效(因为缓存里面没有数据),查询直接越过了缓存。 - 缓存击穿
在缓存过期的那一时刻内,有大量请求进来,造成数据库压力过大,一般只会发生在热点数据中。
解决方法:增加保底的缓存层,也就是一共有两层缓存A和B,当A的key失效时,请求会再去请求B,B收到请求后只允许有一个请求去查库,其他请求直接返回默认数据,当查库的请求查询到请求结果后,再去设置A的缓存。 - 缓存雪崩
有很多key在同一时间过期,在这一时间内,大量请求都打到了数据库上。
解决方法:不让key都在同一时间过期,过期时间增加随机值。