java基础知识
jvm
jvm内存结构
- 参考: link
- 参考: link
- 线程私有内存:
- 虚拟机栈
- 程序计数器
- 本地方法区
- 线程共享内存:
- 堆内存
- 方法区(元空间)
- 直接内存
- 虚拟机栈
- 一个线程拥有一个独立的栈
- 创建一个方法就创建了一个栈帧
- 方法的调用也是通过入栈和出栈来完成的
- 栈帧中保存了方法的局部变量,操作数,动态链接
- 程序计数器
- 唯一不会出现内存溢出的区域
- 保留了当前线程即将执行的指令地址
- 本地方法区
- 本地方法区和虚拟机栈相似,不同的是这块区域是用来执行native方法的
- 堆内存
- 存放着字符串常量池
- 通过参数-Xmx,-Xms来设置最大堆内存,最小堆内存
- 方法区,保存了类加载信息,静态变量,运行时常量池
- 直接内存,元空间通过nio可以分配直接内存
- jdk1.8用把方法区移到了直接内存中
- 线程私有内存:
GC
垃圾收集算法
-
堆的分区
- 新生代划分了eden,幸存区,非幸存区
- 各个区域的占比可以通过jvm参数配置,默认是:8:1:1
- 这样就相当于在标记-整理算法下,只造成了10%的内存空间浪费
-
标记-清除算法
- 扫描标记内存中需要清除的区域,然后清除
- 使用场景:新生代
- 优点:只需要清除被标记需要收集的内存,高效
- 缺点:造成内存碎片
-
标记-复制算法
- 划分一块内存空闲区,存放需要保留的对象,然后一次性清除剩下的区域
- 使用场景:新生代
- 优点:存活对象少的情况下,可以高效完成复制
- 缺点:浪费一半的内存空间
-
标记-整理算法
- 朝着一个方向不断移动需要保留的内存,同时清除标记内存
- 使用场景:老年代
- 优点:不会产生内存碎片,假定需要清除的内存很少
- 缺点:移动大量内存,开销大
垃圾收集器
参考: link
- Serial
- 算法:标记复制
- 特点:单线程收集,会造成用户线程暂停工作
- 使用场景:新生代
- ParNew
- 算法:标记复制
- 特点:多线程收集,同样也会造成用户线程暂停工作
- 使用场景:新生代
- ParallelScavenge
- 算法:标记复制
- 特点:多线程收集,用户线程不会暂停工作,注重jvm的吞吐量
- 使用场景:新生代
- CMS
- 算法:标记清除
- 特点:多线程收集,用户线程不会暂停工作,注重垃圾收集时候的停顿时间
- 使用场景:老年代
- SerialOld
- 算法:标记整理
- 特点:单线程收集,会造成用户线程暂停工作
- 使用场景:老年代
- ParallelOld
- 算法:标记整理
- 特点:多线程收集,同样也会造成用户线程暂停工作
- 使用场景:老年代
- G1
- 新生代和老年代中都会使用的收集器
jvm优化
- 通过visualvm在本机查看内存使用是否是锯齿形状
- 如何设置参数
- Xmx 堆内存最大值
- Xms 堆内存初始值
jvm内存溢出如何排查
- 确定是堆内存溢出还是栈内存溢出
- 如果是堆内存溢出的话,先增大堆内存大小观察,输出堆内存快照包方便问题排查
- 如果是栈内存溢出的话,根据日志定位到栈内存溢出的方法,查看是否是局部变量过多导致的
threadlocal
内部实现原理
参考: link
- 通过每个线程内部创建一个map来保证线程安全
- map的生命周期和线程生命周期保持一致
- map的内部是通过线性探测来解决hash冲突
- 线性探测
- 优点:实现简单
- 缺点:
- 算法不断寻找下一个元素是否满足条件,实际上是穷举法,性能不高
- 如果删除一个元素,需要重新hash计算并排列所有元素的位置
- 线性探测
为什么会内存泄漏
- key为弱引用,垃圾回收的时候key被回收,key为null
- value引用还被持有没有被释放,造成内存泄漏
- value在下次调用threadLocal.get方法的时候也会被值为null,从而被垃圾收集器收集
- key为弱引用的目的:避免value被长期使用,保持key-value最新
虚引用,弱引用,软引用,强引用的区别
- 强引用,通过new创建的对象引用
- 软引用,垃圾回收时,发现内存不足,软引用指向的对象就会被回收
- 弱引用,垃圾回收时,不管内存是否充足,都会回收
- 虚引用,只是为了收到垃圾回收信号
hashmap
实现原理
- 通过数组加链表的结构来存储数据
- put操作计算key的hashcode,用hashcode和数组长度进行位与运算
- 定位到相应的数组元素,如果为null就直接插入,否则插入链表尾部
- 判断链表长度是否大于8,大于8需要把链表升级为红黑树
扩容为什么是2的n次方
key计算hashcode后需要和数组长度进行位与运算,可以保证数组元素均匀分布
为什么线程不安全
当两个线程同时设置数组元素,恰好key发生哈希碰撞,就会出现value相互覆盖
为什么concurrentHashmap线程安全
- 当数组元素为null,通过cas的方式设置key-value,通过cas的方式保证线程安全
- 当数组元素已经有值,通过synchronized关键字锁住当前桶位
- 相当于只有一个线程能操作当前桶位的链表或者红黑树
synchronized
实现原理
参考: link
锁升级过程
- 对象头在无锁的情况下,对象头保存了:
- hashcode
- 垃圾回收分代信息
- 锁的标志位
- 偏向锁
- 在锁没有被占用的时候,获取锁的方式是,通过cas的设置对象头的几个字节保存当前线程id
- 下次再次获取偏向锁的时候就直接比较线程id是否相等
- 轻量级锁
- 线程检测到锁被占用,会根据以往自旋获取锁的成功率来判断是否进入自旋
- 自旋的时间和自旋的次数是由jvm自动优化的
- 自旋可以避免线程挂起和唤醒的开销
- 因为从线程挂起需要由用户态切换到操作系统的内核态
- 重量级锁
- 线程挂起,等待被唤醒
synchronized和lock的区别
- 线程持有lock锁和等待lock锁的时间太长,是可以被中断的
- lock锁可以通过condition来灵活控制线程的状态,比如:在生产者和消费者模型中,生产者可以让消费者处于等待或唤醒状态
- lock锁实现公平锁,实现原理是:lock锁内部通过有序链表存储了等待获取锁的线程,只有最先请求锁的线程可以得到锁
CAS
- 三个关键要素:内存值,预期值,新值
- 只有内存值和预期值相等的情况下才会修改成功,否则一直重试,直到成功为止
- 使用场景:在资源竞争少的场景使用,假设不存在并发的场景
- cas会产生ABA问题,不会对执行结果有什么影响,可以忽略
- 如果一定避免ABA问题,那就直接使用lock锁或者synchronized锁
- cas广泛使用在各种并发编程中,比如:concurrentHashmap的put一个key的时候
volatile关键字
- 线程安全指的是:并发场景下保证当前操作的原子性,可见性,有序性
- volatile可以保证变量操作的可见性,有序性,但是不能保证原子性
- volatile是通过内存屏障来实现的,内存屏障指令前后的指令不能交换顺序,保证了有序性
- 写入遇到内存屏障是需要把CPU高速缓存中的数据写入主内存
- 读取遇到内存屏障需要直接从主内存中读
- 当两个线程同时对一个volatile修饰,初始值为0变量加1时,有可能变量的最终结果是1,而不是2,所以不能保证对这个变量操作的原子性
- 使用场景:
- 变量操作不依赖当前值,比如:布尔类型的标识位
- 反例:访问量统计
lock
实现原理
- 当多个线程竞争lock锁时,lock锁通过cas修改同步状态的方式保证线程安全
- lock锁内部通过双向链表形成一个存储线程引用的队列
- 公平性:lock锁的公平性就是利用了链表的特性,获取公平锁的线程会判断是否链表中存在排队线程
- 可重入性:lock锁通过state变量记录了同一个线程获取锁的次数
- 可中断:lock.lockInterruptibly()
- lock锁可以通过定义condition进行线程之间的通信
- condition通过调用线程的wait方法和notify方法控制线程的状态
- lock还可以满足锁获取超时自动中断的场景,防止想要获取锁的线程长时间等待
读写锁
- 使用场景:读多写少
- 实现方式:内部使用整型标识位记录当前锁是否已经写线程获取
生产者和消费者代码实现
class Buffer {
private final Lock lock;
private final Condition notFull;
private final Condition notEmpty;
private int maxSize;
private List<Date> storage;
Buffer(int size){
//使用锁lock,并且创建两个condition,相当于两个阻塞队列
lock=new ReentrantLock();
notFull=lock.newCondition();
notEmpty=lock.newCondition();
maxSize=size;
storage=new LinkedList<>();
}
public void put() {
lock.lock();
try {
while (storage.size() ==maxSize ){//如果队列满了
System.out.print(Thread.currentThread().getName()+": wait \n");;
notFull.await();//阻塞生产线程
}
storage.add(new Date());
System.out.print(Thread.currentThread().getName()+": put:"+storage.size()+ "\n");
Thread.sleep(5000);
notEmpty.signalAll();//唤醒消费线程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void take() {
lock.lock();
try {
while (storage.size() ==0 ){//如果队列满了
System.out.print(Thread.currentThread().getName()+": wait \n");;
notEmpty.await();//阻塞消费线程
}
System.out.print(Thread.currentThread().getName()+": take:"+storage.size()+ "\n");
Date d=((LinkedList<Date>)storage).poll();
Thread.sleep(5000);
notFull.signalAll();//唤醒生产线程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
class Producer implements Runnable{
private Buffer buffer;
Producer(Buffer b){
buffer=b;
}
@Override
public void run() {
while(true){
buffer.put();
}
}
}
class Consumer implements Runnable{
private Buffer buffer;
Consumer(Buffer b){
buffer=b;
}
@Override
public void run() {
while(true){
buffer.take();
}
}
}
public class Main {
public static void main(String[] arg){
Buffer buffer=new Buffer(10);
Producer producer=new Producer(buffer);
Consumer consumer=new Consumer(buffer);
for(int i=0;i<3;i++){
new Thread(producer,"producer-"+i).start();
}
for(int i=0;i<3;i++){
new Thread(consumer,"consumer-"+i).start();
}
}
}
线程池
线程的状态变化
- new,ready,running,waitting,blocked,terminated
- waitting和blocked状态的发生都会导致线程操作由用户态切换到内核态
- waitting是主动等待
- blocked是被动等待,比如没有获取到锁的时候处于等待状态
线程池有哪些参数
- 队列未满时,核心线程数
- 队列满时,最大线程数
- 大于核心线程的线程空闲回收时间
- 空闲回收时间单位
- 任务队列
- 任务拒绝策略
- 直接丢弃
- 丢弃然后抛出异常
- 让正在运行的线程处理当前任务
- 丢弃队列尾部的任务,插入当前任务
如何设置线程池的核心数
- CPU密集型任务
- 线程数 = CPU核心数 + 1
- IO密集型任务
- 线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
其他
string的创建和内存分配原理
参考: link
- 字符串常量池的内存分配是堆内存
- string.intern方法返回首次出现这个字符串对象的引用
数据库
事务
事务特性
- 原子性
- 隔离性
- 持久性
- 一致性
事务隔离级别
- 读未提交
- 脏读
- 读已提交
- 不可重复读
- 可重复读
- 幻读
- 串行执行
索引
mysql数据库引擎
参考: link
索引的实现原理
- 聚族索引(主键索引)
- innodb引擎一张表对应一个聚族索引
- 索引的数据结构是一棵B+树
- 每个叶子结点对应一个数据页
- 数据页的一行保存了一条完整的记录
- 数据页之间通过双向链表连接,方便数据的区间查询
- 非聚族索引(辅助索引)
- 开发人员平时建的索引就是辅助索引
- 辅助索引先查询到记录的主键id,想要获取完整的记录,还需要根据主键id查询主键索引
联合索引失效场景
- 联合索引其实相当于多层级排序,第一层级排序结束后,继续接下来的排序
- 联合索引失效,是因为没有遵循最左匹配原则
常见的sql优化
- 避免全表扫描的情况
- 避免 where is null
- 避免<>或!=
- 避免or
- 避免使用like
- 慎用in和not in,使用between和exists代替
- 避免对字段进行表达式而导致索引失效
- 使用数据量小的表作为驱动表
- 使用联合索引,避免回表操作
- 通过explain查看索引使用情况和扫描的行数
B树和B+树的区别
- B树每个节点都存储了key和data
- B+树只有叶子结点存储了key和data,非叶子结点存储的是key
- B+叶子结点连接成有序链表,方便区间查询
- B+所有key都会包含到叶子结点,B树的key是分散在所有节点中
数据结构和算法
归并排序
public class MergeSortTest {
public static void main(String[] args) {
//测试数据
int A[] = { 1, 6, 4, 5, 2, 9, 7, 23, 56, 43, 99 };
// 排序前
System.out.println("排序前:");
for (int a : A) {
System.out.print(a + " ");
}
System.out.println();
// 排序
mergeSort(A);
// 排序后
System.out.println("排序后:");
for (int a : A) {
System.out.print(a + " ");
}
System.out.println();
}
// 排序入口
public static void mergeSort(int[] A) {
sort(A, 0, A.length - 1);
}
//递归
public static void sort(int[] A, int start, int end) {
if (start >= end)
return;
// 找出中间索引
int mid = (start + end) / 2;
// 对左边数组进行递归
sort(A, start, mid);
// 对右边数组进行递归
sort(A, mid + 1, end);
// 合并
merge(A, start, mid, end);
}
// 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
public static void merge(int[] A, int start, int mid, int end) {
int[] temp = new int[A.length];// 临时数组
int k = 0;
int i = start;
int j = mid + 1;
while (i <= mid && j <= end) {
// 从两个数组中取出较小的放入临时数组
if (A[i] <= A[j]) {
temp[k++] = A[i++];
} else {
temp[k++] = A[j++];
}
}
// 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
while (i <= mid) {
temp[k++] = A[i++];
}
while (j <= end) {
temp[k++] = A[j++];
}
// 将临时数组中的内容拷贝回原数组中 (left-right范围的内容)
for (int m = 0; m < k; m++) {
A[m + start] = temp[m];
}
}
}
手写二分查找
int binarysearch(int array[], int low, int high, int target) {
if (low > high) return -1;
int mid = low + (high - low) / 2;
if (array[mid] > target)
return binarysearch(array, low, mid - 1, target);
if (array[mid] < target)
return binarysearch(array, mid + 1, high, target);
return mid;
}
树的遍历
// 前序遍历
public void preOrderTraverse1(TreeNode root) {
if (root != null) {
System.out.print(root.val + "->");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
LRU缓存
public class LRUCache<K, V> {
private Map<K, V> map;
private final int cacheSize;
public LRUCache(int initialCapacity) {
map = new LinkedHashMap<K, V>(initialCapacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
};
this.cacheSize = initialCapacity;
}
}
设计模式
单例模式
public class Singleton {
private volatile static Singleton singleton;
public static Singleton getSingleton() {
if (singleton == null) {
// 这里方法是static的,所以synchronized不能锁住this对象
// 只能锁住class对象
synchronized (Singleton.class) {
if (singleton == null) {
/**
* 这就是为什么要家volatile关键字原因
* 1 防止对象没有初始化完成,其他线程使用未初始化的对象
* 2 相当于是禁止了指令重排
*
*/
singleton = new Singleton();
}
}
}
return singleton;
}
}
模版方法模式
策略模式
代理模式
责任链模式
spring
自动装配机制
springCloud的各个组件
springboot实现原理
spring事务
参考: link
- 事务传播机制其实就是处理调用方法和被调用方法之间的事务关系
- 如果当前方法和子方法同时被@Transaction修饰,子方法是否需要创建新的事务,如果不创建,是否合并到当前事务
- 如果只有子方法被@Transaction修饰,那么子方法是否创建新的事务,还是抛出异常
- 只要能够确定当前子方法是否会创建新的事务,一般就不会错误使用事务传播机制
feign超时时间设置
参考: link
redis
如何删除数据量很大的key
- 如果直接删除很大key,比如zset,可能会导致redis计算资源被占用,影响正常业务
- 可以批量删除
如何模糊搜索key
- 设计key的时候需要做好层次划分
- 对于频繁访问的key可以存储在map或zset中
zset跳表
- 空间换时间,时间复杂度logN
- 方便zset进行区间查询
红黑树特性
为什么选择跳表而不是红黑树
缓存雪崩,缓存穿透,缓存击穿原因和解决办法
- 缓存雪崩,原因:缓存大规模失效,解决办法:设置缓存随机失效时间
- 缓存穿透,原因:缓存和数据库都没有需要访问的数据,解决办法:设置key的value设置为null并添加过期时间(或者使用布隆过滤器)
- 缓存击穿,原因:热点数据失效,解决办法:热点数据永不过期
redis如何实现分布式锁
- 切面
- 自定义注解
- redis setnx
- redis单线程保证了命令的原子性
- 超时时间设置太大,只要正常释放锁就没有问题
- 超时时间设置太小,会导致想要锁住的代码逻辑没有执行完毕,锁就释放了导致锁失效
mybatis
缓存
- 装饰器模式
- 打印缓存日志
- 缓存线程安全
- 缓存定时清理
- 缓存事务封装
- 二级缓存脏读
- 更新操纵只能确保单机缓存最新
- 根据具体业务,设置缓存过期的前提下可以使用缓存
- 一级缓存在本次session关闭时会清除
插件
- 通过责任链模式添加插件
- 责任链中每个处理器又是一个动态生成的代理对象
- 框架自带的分页是内存分页不能满足需求,可能造成内存溢出
- 需要使用第三方的分页插件来实现分页
消息中间件
kafka
消息丢失的原因
- 生产者
- 原因:
- 网络波动
- 发送消息异常
- 办法:
- 消息发送确认,失败的话重复发送
- 确认主节点发接收消息成功,ack=1
- 确认所有节点接收消息成功,ack=all
- 消息发送确认,失败的话重复发送
- 原因:
- kafka
- 原因:kafka内部保存消息异常
- 办法:运维问题
- 消费者
- 原因:消费者处理消息异常
- 办法:先处理再commit
- 可能导致消息重复消费,但是可以通过业务逻辑进行判断
Git
git空间
- 工作区
- git stash命令存储代码版本的空间
- 暂存区
- git commit命令存储代码版本的空间
- 版本库
- git push命令存储代码版本的空间
rebase
- git rebase可以把多次提交合并为一条提交,保持提交记录清晰
- git rebase合并提交记录最好是代码还没有执行push操作
- 执行push也可以继续rebase,然后强制push到远程版本库
- 强制push之后,需要其他合作开发人员执行git pull --rebase才能保证本地代码和远程代码一致,不冲突
- 不使用这个命令会造成代码冲突,造成不必要的麻烦
项目经验
技术点
雪花算法
参考: link
- 如何生成workId
- 通过redis incr获取原子自增id
- 保存ip地址和workId的键值对
- 解决时钟回拨问题
- 阻塞10毫秒,尝试获取当前时间,如果大于上次生成id成功的时间,则恢复正常
- 从10位workid,预留3位,如果碰到时钟回拨就+1处理
接口幂等实现
- 动态代理
- 切面
- 自定义注解
- 对接口代码无侵入
- redis
- key为接口名 + 用户id
oauth2协议
参考: link
- 项目用到的是密码模式
字节码编码
功能模块设计
通过redis zset集合对直播间排序
- 技术方案:直播间的关键字段存在了mysql,详细信息存hbase,为了提升接口响应时间,用redis zset集合对直播间排序
- 技术细节:
- zset有序集合存储所以房间id和访问人数(或访问时间)的对应关系
- 多个set无序集合存储不同分类直播间房间id,比如:体育,游戏,新闻
- 通过zset和set集合的交集得到所以体育直播间的有序集合
- 通过zset集合可以对直播间分页获取
- 其中涉及的zset命令:zinterstore(交集),zunionstore(并集),zrangebyscore limit(分页)
- 为了保持redis和mysql的数据一致性,定时任务每晚同步mysql的数据到redis
列表多字段排序
- 层级排序
- 第一层级,活跃项设置为100,非活跃项设置为200
- 第二层级,活跃项中个人和公司分别用101和102区分
- 多字段排序
- 自定义权重排序
- 时间排序
抢优惠券秒杀系统设计
- 前端设置5秒内的随机等待时间,减少服务器压力
- incr记录优惠券被抢张数
- zset记录前100个抢到优惠券的用户,根据抢到时间排序
- 不管是否抢到都把记录加入消息队列,方便后期逻辑处理
- 使用定时任务读取zset发放优惠券,发放成功id放到set集合
- 读取zset和set交集,发放优惠券是两个步骤,需要加分布式锁
一个完整的项目开发流程
- 项目开发流程的每个关键节点都需要关注,从而把控项目进度
- 关键节点
- 需求评审
- 理解需求背后的价值
- 评估需求时间成本,避免做无用功
- 需求敲定
- 技术评审
- 开发
- 工作计划
- 待办事项优先级
- 自测
- 提测
- 预发布
- 上线
- 制定上线流程
- 需求评审
-
为什么重写equals方法,就要重写hashCode方法
先看看object默认的equals方法public boolean equals(Object obj) { return (this == obj); }
默认的equals直接比较的是内存地址,我们想要的效果是类的某些属性相等,这样就需要重写equals方法
在hashmap中对象作为key,equals返回ture,hashcode方法也需要返回相同的值,这样才可以找到map正确的位置 -
final, finally, finalize的区别。
final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
finally是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集 时的其他资源回收,例如关闭文件等 -
mybatis原理
-
feign超时时间设置