面试突击:Java基础
平时工作很多概念性的东西用不到,长时间不用吧,容易忘,但面试又确实是要问,只能复习面试的时候整理下来,也是再学习一遍。
真希望能找到一份,能把学习到的诸多理论知识变成实践的工作,而不是业务复杂,但技术不复杂的工作,嗨,加油吧。
关键命令
jstack
查死锁
jmap
查内存
jstat
查性能
锁
简单的说Java的锁分为两大种:Lock和synchronized
Lock是Java的对象,synchronized是Java自带的关键字,在1.6以前synchronized相较于Lock有着比较大的性能差异,在1.6的时候给synchronized引入了自适应自旋、轻量级锁、偏向锁等优化,使得现在synchronized和Lock并没有什么明显的性能差异了,使用哪个完全看使用场景。
如果只是想对某一块代码或者说资源加锁,就可以简单的使用synchronized,如果说需要灵活一些,比如在这里传参、设置超时时间、甚至加锁解锁在不同的方法里,那就得用Lock
死锁
死锁简单的说就是俩线程都需要对方的资源,又抢不过来,就只能互相等待,结果谁都执行不了
死锁的4要素:
- 互斥
- 占有且等待
- 不可抢占
- 循环等待
所以如何避免死锁就是在可能导致死锁的地方,比如同时申请两个锁的时候,总是以相同顺序去申请
CAS
一般加锁都是悲观锁,但是悲观锁一般来说效率比较低,所以CAS就是来解决这些问题的,它是一种乐观锁,在java.util.concurrent包里面基本都是用的CAS,包括atomic包也基本都是用CAS实现的
CAS,compare and swap(set),顾名思义,比较并替换
每个线程CAS的时候要带俩参数:要改变的值原本是什么、要把它变成什么
这样在CAS里面就会进行比较,如果当前内存中的值,和你要改变的值相同,说明你是对的,或者说你是第一个到的,那就把内存的值改成你要变的值。而几乎同时再进来的后面的线程,你带来的原本的值已经不对了,自然执行不成功,就只能把现在的值返给你,这就确保了在多线程下,对同一个资源操作,虽然没加锁,仍能保证不会出问题。
CAS会带来ABA问题,就是A改了这个值,B又来给改回去了,这时候再来的线程会以为这个值没被A和B改过,还是原来的值,要解决这个问题就需要再加一个版本号作为参数
分布式锁
synchronized是JVM提供的,所以只能单机,Lock在不尽兴扩展的话也只作用于单机,而我们现在分布式的理念又比较流行,所以如果说有需求要确保一个分布式锁的话,这两个就用不了了,就需要分布式锁。
分布式锁一般锁基于数据库、redis或者其他手段来做,这里就说一下数据库或者redis
基于数据库就是相当于基于数据库的乐观锁,给一个字段是锁名,一个字段是锁的状态,一个字段是版本号,各微服务去取锁的状态和版本号就行了,由于数据库自己有事务隔离,自然就保证了取的值的状态。但是我不是很推荐这个方法,本身现在分布式架构下,压力最大最难做拓展的就是数据库,锁这个频繁操作的事这又给数据库加负担,个人不喜欢。
基于Redis的话就很简单了,用setnx(锁名,1)的方法就行了,返回0代表占锁失败,返回1代表占锁成功,拿到锁的线程执行完了给锁名删了就行了。要拓展,防死锁的话value就不是1了,需要是当前时间+过期时间,取到以后和当前时间对比,小于当前时间就获取锁,大于当前时间就没获取到锁
volatile
volatile简单的说就是保证了可见性,你加了volatile的值,你看到是1,那么就一定是1,但是他不能保证原子性,多线程同时操作还是可能会有线程问题的。解决线程问题只能加锁。
它的原理是:
- 实际上为了解决效率问题,每个线程并不是直接从主内存中读值,而是先读到每个线程自己的工作内存中,那这样多个线程就有可能各自工作内存里面的同一个值不一样。
- 加了volatile以后,线程在改变这个值的时候,会强制刷新主内存中的值,并强制让其他线程的工作内存中的值过期,其他内存在用的时候得再从主内存中取
volatile还可以保证执行的顺序,代码在被jvm编译器优化后其实不一定能保证执行顺序就是你的代码顺序,用volatile就可以保证
volatile还有个问题是缓存行,如果两个加了volatile的值在同一个缓存行中,那么同时刷新主内存的时候会导致出问题,需要加关键字来做跨行优化
IO
IO分为三种:BIO,NIO,AIO
IO嘛,就是IN和OUT,说白了也就是数据交换吧,比如从磁盘读写文件,服务期间通讯,这些就是IO的主要用途
BIO
早期的IO就是BIO,现在用在文件读写上也是很好用的,但是如果用在服务器间通讯的话就会有个很大的问题:因为BIO是同步阻塞的,操作得在一个线程内完成,所以要是用在网络上的话,那么客户端发起一个请求,服务端就得开启一个线程去接,各自处理业务的时候大家都得互相等着,那客户端多的话对一个服务端来说可能就遭不住了。
NIO
而NIO为了解决这个问题,采用了轮询模式,用一个线程不停的循环,这个线程也就是selector。它不停的循环,去看注册在自己身上的各个channel有没有连接,有连接的时候,才会去开启新的线程去处理,多路请求进来只有一个selector去处理,这就是多路复用。而每次请求呢,也不是像BIO那样用字节流或者字符流,而是用一个缓冲区,也就是buffer,一次传输这么多数据,那边接受完了,这边再装这么多,装完为止,NIO同样是同步的,只是同步非阻塞,如果并发的太多服务端处理不过来,还是需要排队等待的
AIO
AIO是异步非阻塞的,在NIO的基础上,客户端传完数据以后不需要等待服务端处理了,客户端直接结束连接,等服务端处理完了以后再通知客户端来读取数据。
垃圾回收
什么时候会触发垃圾回收
新生代的eden区满了,会触发年轻代gc,minor gc,这个gc非常频繁
新生代没被回收的,熬过了几次minor gc后会进入老年代(没被回收的会进入到年轻代到surviver0,然后survivor1年龄加一岁。够一个阈值了就进入老年代,剩下的并进surviver0,然后0和1交换)
老年代满了会触发full gc
可以调大gc和非gc的比来避免超过了GCTimeRatio触发OOM
调大新生代老年代的比来减慢full gc的次数
调多新生代回收次数进入老年代的阈值来减少full gc次数
不过归根结底还是得找到为啥full gc回收不掉,用jmap看一下问题出在哪
回收的什么
从gc root开始伸展,经过一次标记后仍然没有复活的对象
哪些可以是GC ROOT
1、系统类加载器加载的类;
2、活跃线程持有的对象;调用栈(包括JVM栈、本地方法栈)持有的对象;
3、常量引用的对象;
4、静态属性实体引用的对象。
做了什么事
G1用的是分代回收的方法
- 在minor gc的时候,使用多线程复制算法,使用两块内存空间,每次只用一块,触发gc后,将还活着的对象复制到另一块上,避免出现内存碎片,stw一次,触发即stw
- full gc用的是多线程标记-整理算法
G1在full gc上总共stw三次
- 初始标记,stw
- 并发标记,顾名思义,和程序一起并发执行,不stw
- 最终标记,stw
- 整理回收,stw
CMS
- 初始标记(initial mark) 有 STW
- 并发标记(concurrent mark) 没有 STW
- 重新标记(remark) 有 STW
- 并发清除(concurrent sweep) 没有 STW
初始标记(STW initial mark)
并发标记(Concurrent marking)
并发预清理(Concurrent precleaning)
重新标记(STW remark)
并发清理(Concurrent sweeping)
并发重置(Concurrent reset)
比起cms,优点在于更能充分利用CPU的多核性能更好、分代回收算法思想先进、标记-整理而不是标记-清除使得没有碎片
浮动垃圾
是指由于在并发标记期间,有些原本不可达的对象变得可达了,所以为了避免回收掉还有用的数据,所以重新标记去掉那些可达对象的标记。但是由于在此期间,程序是并行的,所以肯定会产生新的垃圾,然后并没有对这部分进行标记,所以导致了浮动垃圾。不过其实不要紧,下一次回收就回收掉了。
真要解决可以价格配置,在full gc前加一次minor gc
Java 11 ZGC
- 初始标记,STW
- 分区并发标记,STW,非常小
- 整理移动(读屏障)
- 筛选回收,SWT,如果时间长于10ms,会恢复之STW状态
- **(STW)**Pause Mark Start,开始标记,这个阶段只会标记(Mark0)由root引用的object,组成Root Set
- Concurrent Mark,并发标记,从Root Set出发,并发遍历Root Set object的引用链并标记(Mark1)
- **(STW)**Pause Mark End,检查是否已经并发标记完成,如果不是,需要进行多一次Concurrent Mark
- Concurrent Process Non-Strong References,并发处理弱引用
- Concurrent Reset Relocation Set
- Concurrent Destroy Detached Pages
- Concurrent Select Relocation Set,并发选择Relocation Set;
- Concurrent Prepare Relocation Set,并发预处理Relocation Set
- **(STW)**Pause Relocate Start,开始转移对象,依然是遍历root引用
- Concurrent Relocate,并发转移,将需要回收的Page里的对象转移到Relocation Set,然后回收Page给系统重新利用
多线程
线程的状态
新建状态、就绪状态、运行状态、阻塞状态、等待状态、死亡状态
wait、sleep、join、yield的区别
- wait/notify/notifyAll位于Object中,sleep、join、yield位于Thread中
- wait阻塞当前线程,释放对象锁,其他线程可来争抢对象锁;sleep阻塞当前线程,持有对象锁,其他线程只能等待sleep结束后释放对象锁才可继续
- yield并不阻塞当前线程,只是释放当前占有的CPU资源,使当前线程从运行状态变成就绪状态,但并不可靠,可能马上又进入到运行状态,暂时不知道什么情况下应该使用yield
- join是父线程等待子线程结束后执行,内部使用了wait,但是并没有notify,是jvm在子线程销毁的时候会notify all所有在该子线程join的父线程
如何实现线程
继承Thread或者实现Runnable,这俩比Runnable好一些因为不能多继承,多实现用起来更灵活,然而我选择线程池
start和run的区别
start开新线程,run不开;start内部调用了run
Runnable和Callable的区别
Runnable没返回值,Callable有返回值
CyclicBarrier和CountDownLatch
都可以用来等其他线程,CountDownLatch不能重新计数,这里感觉用join更多一点
ThreadLocal
ThreadLocal实际上就是绑定了线程的一个map,是Thread.ThreadLocalMap的一条引用。就相当于定义了一个只有当前线程能访问的全局变量,可以一定程度上解决线程安全问题来达到安全的使用全局变量的目的,比如一般的mybatis的分页插件就是用了这个ThreadLocal来达到不用把分页参数一层层往下传的目的,紧邻的第一条sql执行完以后就清除掉了
线程池
- CachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- FixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- ScheduledThreadPool :创建一个定长线程池,支持定时及周期性任务执行。
- SingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ThreadPoolExecutor
- corePoolSize: 线程池核心线程数最大值
- maximumPoolSize: 线程池最大线程数大小
- keepAliveTime: 线程池中非核心线程空闲的存活时间大小
- unit: 线程空闲存活时间单位
- workQueue: 存放任务的阻塞队列
- threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
- handler: 线城池的饱和策略事件,主要有四种类型。
现在感觉从设计思路来说,1.7引入的fork/join好一些,每个线程都有自己的队列,谁先干完了从没干完的那可以窃取
阻塞队列
- 无届队列:顾名思义,没大小限制,并发太大处理不过来可能导致OOM
- 有届队列:FIFO的队列或者优先级队列
- 同步移交队列:放入这个队列必须有其他的线程等待接收,不推荐使用这个
拒绝策略
- 丢弃任务,抛出异常(默认)
- 丢弃任务,不抛出异常
- 丢弃队列最前面的任务,重新提交被拒绝的任务
- 由调用线程处理该任务
内存模型
线程私有区
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享区
- 堆
- mate区(元空间)
- 常量池