1. JAVA基础
1.1 线程与进程之间的关系
一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.
所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成
1.2 并行和并发的区别
并发(concurrency)和并行(parallellism)是:
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”处理多个任务。
1.3 多线程实现方式
方式一:extends Thread
方式二: implements Runnable或 implements Callable
当我们调用start()启动线程时,底层虚拟机会自动调用run()执行我们的业务
1.4 线程的几种状态和阻塞原因
线程生命周期,主要有 五 种状态:
- 新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
- 就绪状态(Runnable): 当调用线程对象的start()方法,线程即为进入就绪状态.
处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行 - 运行状态(Running): 当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态 - 阻塞状态(Blocked): 处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
等待阻塞: 运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
同步阻塞: 线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
其他阻塞: 调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态 - 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期
1.5 集合
1.ArrayList:
ArrayList 初始化大小是 10
,排列有序可重复
,底层使用数组
,线程不安全
扩容点规则是,新增的时候发现容量不够用了,就去扩容
扩容大小规则是,扩容后的大小= 原始大小*1.5
。
ArrayList是一个集合,底层维护的是数组
结构,查询比较快,增删慢
在新增或者删除数据时,需要将已有的数据复制移动到新的储存空间,代价比较高,所以不适合插入或者删除。
2.LinkedList:
LinkedList 是一个双向链表
,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。
LinkedList是一个集合,底层维护的是链表
结构,查询比较慢,增删快
Vector(数组实现、线程同步)
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一
个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,
访问它比访问 ArrayList 慢。
3.HashMap:
线程不安全
HashMap 初始化大小是 16 ,扩容因子是0.75。(默认,可在初始化的时候自定义)
扩容机制:2倍
扩容,创建一个空数组重新Hash
JDK1.7:是数组+链表,头插。
JDK1.8:数组+链表+红黑树,尾插。长度达到8时变成红黑树,长度低于6时变回去
线程不安全原因:
- JDK1.7 中,由于多线程对HashMap进行
扩容
,调用了HashMap#transfer()
,具体原因:某个线程执行过程中,被挂起
,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变
,造成死循环、数据丢失。 - JDK1.8 中,由于多线程对HashMap进行
put操作
,调用了HashMap#putVal()
,具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标
是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起
,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入
,然后线程A获得时间片,由于之前已经进行了hash碰撞
的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了
,从而线程不安全。
HashMap线程不安全的体现:
- JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
- JDK1.8 HashMap线程不安全体现在:数据覆盖
4.Hashtable:
线程安全,但是代价大、效率低
。
HashTable和HashMap的实现原理几乎一样,差别无非是;HashTable不允许key和value为null;HashTable是线程安全 的。但是HashTable线程安全的策略实现代价太大了,简单粗暴。get/put所有相关操作都是synchronized的 ,这相当于个整个Hash表加了一把大锁,多线程访问时,只要以后一个线程访问或操作该对象,那其他线程只能阻塞,相当于所有的操作串行化,在竞争激烈的多线程场景性能就会表现的非常差。
5. ConcurrentHashMap:
线程安全
1.7:
数组+链表
segment分段锁:继承了reentranlock,尝试获取锁时存在并发竞争,会自旋,多次自旋最后导致阻塞
get方法比较高效,被volatile修饰,不需要加锁
1.8:
数组+链表+红黑树
CAS+Synchronized,cas失败自旋保证成功,再失败就syc保证
:CAS(比较并替换,在操作之前先把值取出来,然后进行操作,放进去的时候判断是否和取出来的值一致,一致则替换,不一致则自旋以保证成功,再失败就Synchronized保证成功操作)。
CAS锁升级:针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式
- 先使用偏向锁优先同一线程,然后再次获取锁
- 如果失败,就升级为 CAS 轻量级锁
- 如果失败就会短暂自旋,防止线程被系统挂起
- 最后如果以上都失败就升级为重量级锁
锁是一步步升级上去的,最初是通过很多轻量级的方式锁定的
2. Spring
2.1 Spring AOP的两种动态代理:Jdk和Cglib
原理区别:
java动态代理是利用 反射机制 生成一个实现代理接口的匿名类 ,在调用具体方法前调用 InvokeHandler 来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象 实现了接口,默认情况下会采用 JDK 的动态代理实现AOP ,可以强制使用CGLIB实现AOP
2、如果目标对象 没有实现接口,必须采用 CGLIB 库,spring会自动在JDK动态代理和CGLIB之间转换
如何强制使用CGLIB实现AOP?
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>
JDK动态代理和CGLIB字节码生成的区别?
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
因为是继承,所以该类或方法最好不要声明成final
3. 数据库和分布式事务
3.1 Redis
3.1.1 Redis常用数据类型
字符串String: 普通的 set 和 get,做简单的 KV 缓存,最大支持512M.
散列Hash类型: 类似 HashMap 的一种结构,存储结构化的数据,比如一个对象,最大支持2*32
双向链表 List类型: 有序列表
Set类型: 无序集合,会自动去重的那种
Sorted Set: 排序的 Set,去重但可以排序,适合热搜榜
3.1.2 双写一致性
Cache Aside Pattern
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
3.1.3 多级缓存
3.1.4 缓存预热
3.1.5 缓存击穿
问题描述: 缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
解决方案:
设置热点数据永远不过期。或者加上互斥锁就能搞定了
3.1.6 缓存穿透
问题描述: 缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都请求到数据库上,从而数据库接异常。
缓存穿透解决方案:
1 . 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
2 . 采用异步更新策略,无论Key是否取到值,都直接返回。Value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
3 . 提供一个能迅速判断请求是否有效的拦截机制比如,利用布隆过滤器,内部维护一系列合法有效的Key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
3.1.7 缓存雪崩
问题描述: 缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求都请求到数据库上,从而导致数据库连接异常。
缓存雪崩解决方案:
1 . 给缓存的失效时间,加上一个随机值,避免集体失效。
2 . 使用互斥锁,但是该方案吞吐量明显下降了。
3 . 双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B设置较长失效时间。
双缓存的实现过程: 从缓存A读数据库,有则直接返回; A没有数据,直接从B读数据,直接返回并且同时刷新A。并且利用异步更新线程,更新数据时候同时更新缓存A和缓存B。
3.2 MySQL
3.2.1 MySQL存储引擎区别
MyISAM
MySQL5.5之前的默认存储引擎,不支持事务和外键.对事务的完整性没有要求可以选用MYISAM
InnoDB
MySQL5.5之后的默认存储引擎,支持事务,外键.
- 自动增长列必须是索引,如果是组合索引,也必须是组合索引的第一列,主键自动生成索引
- 存储数据和索引有共享表空间存储和独占表空间存储两种方式
- 每一张表都有自己独立的表空间,表的结构在.frm文件中,表的数据和索引在.ibd的文件
3.2.2 数据库并发访问冲突问题
脏读
- 读取到其他事务未提交的数据。
不可重复读
- 重复读取同一数据时,与之前读取的数据不一致。
- 一个事务提交的数据,可以被另一个事务立即读取。
幻读
- 读取到已经被删除的数据。
- 读取不到新插入的数据。
3.2.3 MySQL隔离级别
参考补充
互联网项目请用:读已提交(Read Commited)这个隔离级别!
讲讲mysql有几个事务隔离级别?
“读未提交,读已提交,可重复读,串行化四个!默认是可重复读”
“为什么mysql选可重复读作为默认的隔离级别?”
(你面露苦色,不知如何回答!)
“你们项目中选了哪个隔离级别?为什么?”
你:“当然是默认的可重复读,至于原因。。呃。。。”
3.3 分布式事务
4. 微服务
4.1 SpringCloudAlibaba
4.2 SpringCloud
4.3 接口幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。(同一个订单,无论是调用了多少次,用户都只会扣款一次)
常见的两种实现方案: 1. 通过代码逻辑判断实现 2. 使用token机制实现
- 通过代码逻辑判断实现
- 设置订单状态,提交(0),付款成功(1),付款失败(2),
- 用户首次支付,携带订单id,完成业务操作,更新mysql订单状态.
- 用户重复发起支付,先查询订单状态,如果为1,则直接返回结果给用户,同时更新redis
- 使用token机制实现
- 生成全局唯一token,存在redis和session中
- 用户首次发起支付,先检验token再完成支付逻辑.支付成功后删除token并生成新token更新redis,
- 用户重复发起支付,页面再次提交携带session中的token是已删除的token后台验证会失败不让提交
5. 算法
FIFO
有界缓存淘汰策略:FIFO(先进先出)算法
Deque:双端队列(两头都可以操作),基于此队列记录key的顺序
package com.jt.cache;
import java.util.Deque;
import java.util.LinkedList;
/**
* 有界缓存淘汰策略:FIFO(先进先出)算法
* @author 小新
* @date 2021/09/06 18:19
*/
public class FifoCache implements Cache{
/**存储数据的Cache*/
private Cache cache;
/**Cache的最大容量*/
private int maxCap;
/**双端队列(两头都可以操作),基于此队列记录key的顺序*/
private Deque<Object> deque;
public FifoCache(Cache cache, int maxCap) {
this.cache = cache;
this.maxCap = maxCap;
this.deque=new LinkedList<>();
}
@Override
public void putObject(Object key, Object value) {
//1.记录key的顺序
deque.addLast(key);
//2.判定cache是否已满,满了则移除元素
//if(cache.size()==maxCap){} 方式1
if(deque.size()>maxCap){//方式2
//获取最先放入的元素key
Object eldestKey=deque.removeFirst();
//移除最先放进去的元素
cache.removeObject(eldestKey);
}
//3.添加新的元素
cache.putObject(key,value);
}
@Override
public Object getObject(Object key) {
return cache.getObject(key);
}
@Override
public Object removeObject(Object key) {
Object value=cache.removeObject(key);
deque.remove(key);
return value;
}
@Override
public void clear() {
cache.clear();
deque.clear();
}
@Override
public int size() {
return cache.size();
}
@Override
public String toString() {
return "FifoCache{" +
"cache=" + cache +
'}';
}
public static void main(String[] args) {
FifoCache cache = new FifoCache(new DefaultCache(), 3);
cache.putObject("A",100);
cache.putObject("B",200);
cache.putObject("C",300);
cache.putObject("D",400);
cache.putObject("E",500);
System.out.println(cache);
}
}
lru
LRU 算法的全程是 Least Rencently Used,顾名思义就是按照最近最久未使用的算法进行数据淘汰。
核心思想「如果该数据最近被访问,那么将来被访问的几率也更高」
是维护一个双向链表,被访问的数据会被移动到链表头部,被访问的数据之前的数据则相应往后移动一位.内存不足时淘汰链表尾部数据.
redis缓存淘汰策略–LRU
由于 LRU 算法需要用链表管理所有的数据,会造成大量额外的空间消耗。
除此之外,大量的节点被访问就会带来频繁的链表节点移动操作,从而降低了 Redis 性能。
所以 Redis 对该算法做了简化,Redis LRU 算法并不是真正的 LRU,Redis 通过对少量的 key 采样,并淘汰采样的数据中最久没被访问过的 key
Redis LRU 算法有一个重要的点在于可以更改样本数量来调整算法的精度,使其近似接近真实的 LRU 算法,同时又避免了内存的消耗,因为每次只需要采样少量样本,而不是全部数据。
配置如下:
maxmemory-samples 50
运行原理
数据结构 redisObject 中有一个 lru 字段, 用于记录每个数据最近一次被访问的时间戳。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
/* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time).
*/
unsigned lru:LRU_BITS;
int refcount;
void *ptr;
} robj;
Redis 在淘汰数据时,第一次随机选出 N 个数据放到候选集合,将 lru 字段值最小的数据淘汰。
当再次需要淘汰数据时,会重新挑选数据放入第一次创建的候选集合,不过有一个挑选标准:进入该集合的数据的 lru 的值必须小于候选集合中最小的 lru 值。
如果新数据进入候选集合的个数达到了 maxmemory-samples 设定的值,那就把候选集合中 lru 最小的数据淘汰。
这样就大大减少链表节点数量,同时不用每次访问数据都移动链表节点,大大提升了性能。
基于LRU算法(最近最少使用算法)对缓存进行数据淘汰设计
package com.cy.java.cache;
import java.util.LinkedHashMap;
import java.util.Map;
/** 缓存淘汰策略:LRU(最近最少使用算法)*/
public class LruCache implements Cache {
private Cache cache;
/**通过此属性记录要移除的数据对象*/
private Object eldestKey;
/**通过此map记录key的访问顺序*/
private Map<Object,Object> keyMap;
@SuppressWarnings("serial")
public LruCache(Cache cache,int maxCap) {
this.cache=cache;
//LinkedHashMap可以记录key的添加顺序或者访问顺序
this.keyMap=new LinkedHashMap<Object,Object>(maxCap, 0.75f, true)
{//accessOrder
//此方法每次执行keyMap的put操作时调用
@Override
protected boolean removeEldestEntry (java.util.Map.Entry<Object, Object> eldest) {
boolean isFull=size()>maxCap;
if(isFull)eldestKey=eldest.getKey();
return isFull;
}
};
}
@Override
public void putObject(Object key, Object value) {
//存储数据对象
cache.putObject(key, value);
//记录key的访问顺序,假如已经满了,就要从cache中移除数据
keyMap.put(key, key);//此时会执行keyMap对象的removeEldestEntry
if(eldestKey!=null) {
cache.removeObject(eldestKey);
eldestKey=null;
}
}
@Override
public Object getObject(Object key) {
keyMap.get(key);//记录key的访问顺序
return cache.getObject(key);
}
@Override
public Object removeObject(Object key) {
return cache.removeObject(key);
}
@Override
public void clear() {
cache.clear();
keyMap.clear();
}
@Override
public int size() {
return cache.size();
}
@Override
public String toString() {
return cache.toString();
}
public static void main(String[] args) {
SynchronizedCache cache=
new SynchronizedCache(
new LoggingCache(
new LruCache(new PerpetualCache(),3)));
cache.putObject("A", 100);
cache.putObject("B", 200);
cache.putObject("C", 300);
cache.getObject("A");
cache.getObject("C");
cache.putObject("D", 400);
cache.putObject("E", 500);
System.out.println(cache);
}
}
漏桶算法
漏桶算法是网络世界中流量整形或速率限制时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。也就是我们刚才所讲的情况。漏桶算法提供的机制实际上就是刚才的案例:突发流量会进入到一个漏桶,漏桶会按照我们定义的速率依次处理请求,如果水流过大也就是突发流量过大就会直接溢出,则多余的请求会被拒绝。所以漏桶算法能控制数据的传输速率。
令牌桶算法
令牌桶算法是网络流量整形和速率限制中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。Google开源项目Guava中的RateLimiter使用的就是令牌桶控制算法。令牌桶算法的机制如下:存在一个大小固定的令牌桶,会以恒定的速率源源不断产生令牌。如果令牌消耗速率小于生产令牌的速度,令牌就会一直产生直至装满整个令牌桶。