## Java相关
### 1. 讲讲Java内存模型
Java内存模型分为五个区域,分别是线程私有的**程序计数器**、**本地方法栈**、**虚拟机栈**,线程共享的**堆**和**方法区**(非堆Non Heap)
程序计数器负责程序的流程控制,字节码解释器通过改变程序计数器的值来选取吓一跳执行的字节码指令,各线程都有自己的一个程序计数器以确保线程之间的流程控制互不影响,它的内存周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈的生命周期也和线程相同,与程序计数器一样,也是线程私有的,它是由一个个**栈帧**组成的,栈帧里是局部变量表,操作数栈,动态链接和方法出口信息,局部变量表中存放了编译器可知的各种数据类型(8大基本数据类新),对象引用(只是一个指针或句柄)。
本地方法区和Java虚拟机栈类似,但它存储的是Java本地的native修饰的方法。该区域在Hot Spot虚拟机中与虚拟机栈合二为一了。
堆是最大的一块内存区域,是线程共享的,主要负责对象实例的存放,它也是GC垃圾回收算法管理的主要区域。在JDK8之后的版本中,永久代被移除换为新的叫做元空间的区域。
![jvm.png](https://www.concoding.com/upload/2021/03/jvm-aebc773cc15d4f1384e0fc9c272f5f7d.png)
方法区与Java堆一样是线程共享的,它负责存储管理已被虚拟机加载的类信息,常量,静态变量,垃圾收集行为很少在这个区域里出现。
此外,还有运行时常量池和直接物理内存。
### 2. 两线程中的变量如何共享?(说说并发中的线程安全问题以及你知道的锁)
对于线程中的变量共享,可以使用锁机制实现。按不同维度可以将锁分为如下几种:
**可重入锁**
- 指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。为了避免死锁的发生,Java中的锁基本都是可重入锁。
**公平锁 / 非公平锁**
- 公平锁,多个线程按照申请锁的顺序获取锁资源。
- 非公平锁,不按等待顺序获取,可能按优先级。大部分锁都是非公平锁。
**独享锁 / 共享锁**
- 独享锁, 锁一次只能被一个线程占有。大部分锁都是独享锁。
- 共享锁,锁可以被多个线程同时长有,比如ReadWriteLock中的ReadLock
**悲观锁 / 乐观锁**
- 悲观锁就是假设每次操作数据,都会有线程来并发操作该数据,所以每次都会对代码块墙纸枷锁。
- 乐观锁就是假设每次都没人和我并发访问该数据,通常采用CAS算法不断尝试更新。
- 悲观所适合写多,乐观锁适合读多。
- CAS算法就是每次更新时先确定该变量是不是预期值,如果是预期值,那么修改成功,如果不是预期值,则不做操作并返回失败。
**粗粒度锁 / 细粒度锁**
- 粗粒度锁,锁住全部执行的代码块。
- 细粒度锁,尽可能少得锁代码块。
**锁升级机制**
- 偏向锁,每次只给一个线程该锁资源。
- 轻量级锁,偏向锁被一个其他线程获取,锁就升级为轻量级锁。
- 重量级锁,再有其他线程以自旋的方式尝试获取该轻量级锁,不会阻塞,自选一定次数(默认10次)还没拿到所资源,它就升级为重量级锁。
**自旋锁**
- 自旋锁不会调度到其他线程,它遇到要获取的锁被别的线程占有,不会立即阻塞,而是循环得去获取锁,这样可以减少线程切换,减少资源调度。
**volatile关键字**
使用该关键字可以保证可见性,禁止指令重排序,它保证被修饰变量对于每个线程来说都是可见的,但它并不是一定线程安全,因为对于i++这样的复合操作来说,它不是原子的,可能会引起并发问题。(禁止指令重排序使用的是内存屏障)。
**线程安全**
- 对于集合的线程安全来说,可以使用util中的concurrent集合类,List使用CopyOnWriteList集合类
### 3. 线程池是什么?
线程池提供了一种机制,它维护着一些可复用的线程,用户提交任务给线程池,线程池交给这些线程去执行,不必为每个任务单独创建线程,因为线程的创建是非常重量级的。线程池的状态分为RUNNING,SHUTDOWN,TYDYNG,TERMINATED,STOP。RUNNING可以执行队列任务,并可以接受新的任务,SHUTDOWN不接受新的任务,但可以执行阻塞的任务,STOP不接受任何新的任务,并且会停止当前进行的任务,TIDYING任务数为0,所有任务已中止,TERMINATED线程池彻底终止。
**线程池执行任务的顺序**
1. 提交任务给线程池,线程池判断线程是否达到核心线程数,若没达到,创建新线程执行任务;
2. 如果达到核心线程数,检查阻塞队列是否满,如果不满,将任务加入阻塞队列中等待执行;
3. 如果任务队列满了,检查线程数是否达到最大线程数,若没达到,创建新线程执行任务;
4. 拒绝任务。
### 4. ReentrantLock和synchronized的区别是什么?
- synchronized默认是阻塞的,线程B在获取不到锁资源时,会一直阻塞下去。
- ReentrantLock默认也是等待阻塞的,但可以指定为等待可中断式的锁,在获取不到锁资源一段时间后终端等待,去做别的事情。
- synchronized默认是非公平锁。
- ReentrantLock默认非公平,但是可以指定构造参数为true实现公平锁。
### 5. ThreadLocal是什么,怎么实现的?
**使用场景:**
当线程需要自己独立的实例,在线程间隔离,但是在方法和类见共享的时候,使用ThreadLocal解决该问题。
- 一种可能的实现方案是:由ThreadLocak维护线程与线程实例的映射关系,每一个ThreadLocal维护一个map,线程作为键,实例作为值,每个线程使用自己实例时从ThreadLocal中get即可。但是这种实现方案涉及到两个问题,一个是写的线程安全(增加线程和减少线程均需要修改ThreadLocal维护的map),另一个是当一个线程结束时,需要将该线程访问的ThreadLocal中的映射全部删除,否则会造成内存泄漏。基于上面两个原因,JDK不采用该实现方案。
![VarMap_1.png](https://www.concoding.com/upload/2021/03/VarMap_1-c6131cf0d4324325a4ffa2ad39519e11.png)
- 另一种实现方案是:由线程维护自己的map映射,每个线程只对自己的ThreadLocal做操作,不涉及多线程安全问题。该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。
![ThreadMap_2.png](https://www.concoding.com/upload/2021/03/ThreadMap_2-b6ecda7bcd73439b97b11384df4db787.png)
- JDK中的实现
该方案中,Map 由 ThreadLocal 类的静态内部类 ThreadLocalMap 提供。该类的实例维护某个 ThreadLocal 与具体实例的映射。与 HashMap 不同的是,ThreadLocalMap 的每个 Entry 都是一个对 **键** 的弱引用。另外,每个 Entry 都包含了一个对 **值** 的强引用。
使用弱引用的原因在于,当没有强引用指向 ThreadLocal 变量时,它可被回收,从而避免上文所述 ThreadLocal 不能被回收而造成的内存泄漏的问题。
但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。
### 6. BIO、NIO、AIO区别是什么?
- **BIO(Blocking-IO)**
同步阻塞IO模式,获取不到资源线程会阻塞到获取到资源。
- **NIO(Non-Blocking-IO)**
线程发起 IO 请求,立即返回;内核在做好 IO 操作的准备之后,通过调用注册的回调函数通知线程做 IO 操作,线程开始阻塞,直到操作完成。
- **AIO(Asynchronous-IO)**
线程发起 IO 请求,立即返回;内存做好 IO 操作的准备之后,做 IO 操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做 IO 操作完成或者失败。
### 7. HashMap的长度是多少,为什么?
长度为2的幂次,**取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)**,所以固定HashMap的长度为2的幂次,可以省略取余步骤,提高寻址效率。
### 8. ConcurrentHashMap 1.7 与 1.8 的区别:
**1.7**:底层维护着一个segment数组,每个segment继承自reentrantLock并且维护着一个hashEntry数组(链表结构的元素),它扮演者锁的角色,维护着整个hashMap的一部分数据,要修改某个segment数组里的元素,必须获取该锁,实现线程安全。
**1.8**:底层是一个Node数组+链表/红黑树,采用CAS和synchronized保证给线程安全,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发。
### 线程状态图
![Java 线程状态变迁.png](https://www.concoding.com/upload/2021/03/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81-ae67c299d6354b47b21973fc4fc3a052.png)
join:当一个主线程A需要等待另一个子线程B执行完再继续执行时,A线程中调用B.join()即等待B的结束。
> Join()方法,使调用此方法的线程wait()(在例子中是main线程),直到调用此方法的线程对象(在例子中是MyThread对象)所在的线程(在例子中是子线程)执行完毕后被唤醒。
sleep:释放CPU时间片,不释放锁资源。
wait:释放CPU时间片,释放锁资源,等待被唤醒。wait(long)可以自行唤醒。
yield(让):让出当前CPU时间片,让出锁资源,线程进入就绪状态等待调度。
Java常见问题