导图社区 题目记录
这是一篇关于题目记录的思维导图。包括多线程、数据库、Spring\加密算法等等内容,范围非常的宏观,概括全面。
编辑于2021-09-08 17:25:15
多线程
什么是线程安全
在多线程场景下,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
怎么保证线程安全
Java一般通过加锁来实现,通常使用synchronized和Lock
什么是锁,什么是死锁
锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足,如互斥锁、共享锁。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
synchronized的实现原理是什么?
synchronized是java关键字吗,是一种同步锁。他是通过monitor(监视器、管程)实现的线程同步。当synchorinized修饰方法时,方法的常量池中会有ACC_SYNCHORINIZED。当一个线程要执行这个方法,需要先获取监视器锁,然后再执行方法,执行完方法后释放监视器锁。当synchorinized修饰代码块时。编译代码的时候,编译器会在同步代码块前后放入monitorenter和monitorexit,当线程执行到monitorenter的时候获取监视器锁,当线程执行到monitorexit的时候,释放监视器锁。每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。
synchronized的锁优化是怎么回事?
起源
jdk1.6之前,synchronized直接通过monitor实现重量锁,而monitor时根据基层操作系统mutex互斥原语实现的,开销很大,jdk1.6之后通过一些列锁优化,减小了锁的开销,这些锁优化包括锁消除、锁粗化、锁膨胀。
锁消除
在JIT动态编译期间做的锁消除操作,JVM在动态编译期间通过逃逸分析,发现锁对象只被一个线程所使用,没有散布到其他线程中,JIT编译器就不会生锁的生成和释放的机器码,因此就消除了加锁的过程。
锁粗化
在JIT动态编译期间做的锁消除操作,通过扫描前后相邻代码,若发现相邻的同步代码块对同一个对象加锁,JIT编译器就会把这几个相邻的同步代码块合成一个同步代码块,减少不断地加锁释放锁的过程,减少加锁的开销。
锁膨胀
偏向锁
偏向锁"偏",就是"偏心"的"偏",它的意思是这个锁会偏向于第一个获得它的程序,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步
轻量级锁
对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。
自旋锁
当有个线程A去请求某个锁的时候,这个锁正在被其它线程占用,但是线程A并不会马上进入阻塞状态,而是循环请求锁
自适应自旋锁
自适应性自旋是自旋的升级、优化,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。例如线程如果自旋成功了,那么下次自旋的次数会增多,反之,如果对于某个锁,自旋很少成功,那么在以后获取这个锁的时候,自旋的次数会变少甚至忽略。
重量级锁
未抢到锁资源的线程,不再自旋,而是进入阻塞状态。
膨胀过程
1、当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2、当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3、当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4、当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5、偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6、轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7、自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
几种锁类型
同步锁(互斥锁)
不可冲入锁
同一时刻,一个同步锁只能被一个线程访问
独占锁(可重入锁)
可以被同一个对象多次获取锁对象。synchronized关键字、ReentrantLock
公平锁
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。Lock lock = new ReetrantLock(true)
非公平锁
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待 ReetrantLock默认为非公平锁,synchornized也是非公平锁。
共享锁(可共享的都是读)
能被多个线程同时获取、共享的锁。即多个线程都可以获取该锁,对该锁对象进行处理。典型的就是读锁
读写锁
维护了一对相关的锁,“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。“写入锁”用于写入操作,它是“独占锁”,只能被一个线程锁获取。
有了synchronized,还要volatile干什么?
synchronized是同步关键字,保证同一时间只能有一个线程执行代码,消耗资源比较大,volatile关键字是让本地内存修改后及时刷新到主内存当中,
知道JMM吗?
每个线程都有工作内存,并且只能操作工作内存,工作内存将数据同步到主内存上
Java并发包了解吗?
Lock
ReentrantLock
ReentrantReadWriteLock
原子变量
AtomicInteger
AtomicLong
AtomicBoolean
AtomicReference
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
并发容器
ConcurrentHashMap
ConcurrentHashMap,它内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下 一个 ConcurrentHashMap 被进一步细分为 16 个段,既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首 先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作
ConcurrentSkipListMap
CopyOnWriteArrayList
修改数据前拷贝一份,修改拷贝的数据,修改完成再将对象指针指向拷贝对象当期望的读数和遍历远远 大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。
CopyOnWriteArraySet
修改数据前拷贝一份,修改拷贝的数据,修改完成再将对象指针指向拷贝对象
工具类
CountDownLatch
线程池相关
见下方线程池详情
什么是fail-fast?
在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统,其实,这是一种理念,说白了就是在做系统设计的时候先考虑异常情况,一旦发生异常,直接停止并上报。我们通常说的Java中的fail-fast机制,默认指的是Java集合的一种错误检测机制。当多个线程对部分集合进行结构上的改变的操作时,有可能会产生fail-fast机制,这个时候就会抛出ConcurrentModificationException(后文用CME代替)。
什么是fail-safe?
为了避免触发fail-fast机制,导致异常,我们可以使用Java中提供的一些采用了fail-safe机制的集合类。这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。java.util.concurrent包下的容器都是fail-safe的,可以在多线程下并发使用,并发修改。同时也可以在foreach中进行add/remove 。
什么是CopyOnWrite?
是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
什么是AQS呢?
AQS(AbstractQueuedSynchronizer),即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。
什么是CAS呢?
内存位置(V)、预期原值(A)和新值(B)。要修改数据V时,先比较数据V与预期的A是否相等,若相等则将数据修改为B。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
乐观锁和悲观锁是什么?
乐观锁:认为一般情况下数据没有被修改,不会产生冲突,因此在修改数据提交时才会比较数据version是否和原本的一致,一致则修改,不一致告诉用户,让用户自己去处理。
悲观锁:当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。
线程池怎么用,有那些参数,代表什么意思
线程池参数
corePoolSize(核心线程数)
根据任务的处理时间和每秒产生的任务数量确定,如执行任务0.1s,平均每秒要执行100个任务,应该需要10个核心线程
maxPoolSize(最大线程数)
(最大任务数 - 任务队列长度)* 单个任务执行时间
queueCapacity(队列容量)
核心线程数/单个任务执行时间*2 ,就上方的情况10/0.1*2 = 200
keepAliveTime(最大空闲时间)
没有固定方案,根据实际情况设置找到一个最优值
rejectedExecutionHandler(拒绝策略)
拒绝策略
CallerRunsPolicy
由调用线程(提交任务的线程)处理该任务
AbortPolicy
丢弃任务并抛出RejectedExecutionException异常(默认策略)
DiscardPolicy
丢弃任务,但是不抛出异常
DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务
非核心线程数 = maxPoolSize - corePoolSize
创建策略
1、首先查看核心线程数是否已满,未满则创建核心线程;
2、核心线程数已满查看队列容量是否已满,未满则将要执行的任务放入队列;
3、队列容量已满,查看非核心线程数,若非核心线程数未满,则创建非核心线程;
4、非核心线程已满,则走拒绝策略。
JUC提供的线程池创建工具
newCachedThreadPool
缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量,可以进行自动线程回收,默认核心线程数0,最大线程数一直创建。
newFixedThreadPool
创建固定大小的线程池,方法传参指定核心线程数
newWorkStealingPool
工作窃取线程池
当A,B线程池尚未处理任务结束,而C已经处理完毕,则C线程会从A或者B中窃取任务执行,这就叫工作窃取假如A线程中的队列里面分配了5个任务,而B线程的队列中分配了1个任务,当B线程执行完任务后,它会主动的去A线程中窃取其他的任务进行执行WorkStealingPool 背后是使用 ForkJoinPool实现的
newScheduledThreadPool
创建固定大小的线程,可以延迟或定时的执行任务,方法传参指定核心线程数
newSingleThreadExecutor
线程池中只有一个线程
newSingleThreadScheduledExecutor
线程池中只有一个线程,可以延迟或定时的执行任务。
数据库
MVCC(多版本并发控制)
MVCC带来的好处是
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
隐藏列
DB_TRX_ID
最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
DB_ROLL_POINTER
回滚指针,指向这条记录的上一个版本(存储于rollback segment里)指向undolog的指针
DB_ROW_ID
隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
undo log
undo log主要存储的也是逻辑日志,比如我们要insert一条数据了,那undo log会记录的一条对应的delete日志。我们要update一条记录时,它会记录一条对应相反的update记录(修改前的记录)
当前读和快照读
当前读
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
Read View
事务进行快照读操作的时候生产的读视图(Read View)
通过ReadView实现Read committed和Repeatable read
读已提交:每次select时,都会生成新的 read-view;可重复读:每次select时,复用第一次的 read-view;
如果当前记录:事务id < 未提交事务的最小id,则可读;如果当前记录:最小id <= 事务id <= 最大id,判断事务id是否在未提交事务id数组中,若不在则可读;如果当前记录:事务id > 最大id,则不可读;
删除百万数据中字段怎么操作
1、删除该列索引
2、删除列
千万级别表添加字段
1.按照原始表(original_table)的表结构和DDL语句,新建一个不可见的临时表(tmp_table)2.在原表上加writelock,阻塞所有更新操作(insert、delete、update等)3.从原表获取数据插入insert into tmp_table select * from original_table4. 将两个表明互换,释放锁,删旧表
使用pt工具
其实pt工具的操作思路和上方一样,只不过他在创建临时表,拷贝数据,换名称这些过程中,把关于数据的操作全部执行在临时表上,记录了数据的修改记录。
事务
特性
原子性
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
一致性
一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
持久性
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
隔离级别
Read uncommitted
可以获取到其他事务未提交的数据 会造成脏读 不可重复读 和幻读
Read committed
仅能获取到其他事务已经提交的数据 会造成不可重复读 幻读
Repeatable read
事务开启时第一次获取的数据是什么样子,事务结束前再次获取都是这个样子 会造成幻读
Serializable
串行化 会解决脏读、不可重复度和幻读
不同隔离级别造成的问题
脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。事务A读取到事务B未提交的数据,此时数据B回滚了数据,A拿到的数据就是脏数据
不可重复度
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值事务A第一次获取到的数据为1,此时事务B修改数据为2,事务A再次查看数据,发现与第一次查询不一致了。
幻读
幻读是事务非独立执行时发生的一种现象。要把表中一列数据全部修改为1,修改完时有人插入了一行数据,该列值为2。再次查询时发现有一个值没改,好像出现了幻觉。幻读强调的是另一个事务insert数据
Spring
事务失效
非public方法
Spring事务管理中,有一个public判断,如果不是public,就不会生成事务的代理。
类内方法调用
类内方法调用,走的是this.method(),没有通过代理调用,因此就不会被代理,对应的事务也就失效了。((ClassName)AopContext.currentProxy()).method(param);
同步代码块失效
同步代码块范围小于事务,同步代码释放了,新线程进来了,事务还没提交,新线程还是查的老数据,就会出现好像同步代码块失效的情况。
事务传播机制
@Transactional(propagation=Propagation.REQUIRED)如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED)容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW)不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY)必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER)必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS)如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
三种注入方式
属性注入
不推荐
构造参数注入
构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。在生成对象的时候,就注入了构造函数里边的所有实例
set方法注入
Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后,调用该bean的setter方法,即实现了基于setter的依赖注入。更加灵活,可选依赖想注入哪个就注入哪个
Bean的生命周期
实例化 Instantiation
属性赋值 Populate
初始化 Initialization
销毁 Destruction
SpringColud
服务治理
服务调用
调用
负载均衡
Ribbon
RandomRule(随机)
实现方案:获取服务数量n,随机数获取0-n的index,list.get(index)
RoundRobinRule(轮询)
实现方案:获取服务数量n,记录请求次数i,进行取余操作(i%n)得到当前访问应当走第index个服务。list.get(index)
RetryRule(先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内进行重试,获取可用的服务。)
WeightedResponseTimeRule(对RoundRobinRule的扩展,响应速度越快的实例选择权重就越大,越容易被选择。)
BestAvailableRule(会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。)
AvailabilityFilteringRule(先过滤掉故障实例,再选择并发较小的实例。)
ZoneAvoidanceRule(默认规则,复合判断server所在区域的性能和server的可用性选择服务器。)
服务降级
服务降级
服务器繁忙,请稍后在世,不让客户端等待并立即返回一个友好的提示,fallback
1、程序运行异常2、超时3、服务熔断触发服务降级4、线程池/信号量打满会
服务熔断
达到最大访问量后,直接拒绝访问,然后调用服务降级的方法返回友好提示
服务限流
一秒钟只允许通过n个请求
服务网关
服务配置
服务总线
加密算法
对称加密
加密和解密的秘钥使用的是同一个
非对称加密
与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)
NIO
Netty
拆包、粘包
拆包和粘包是在socket编程中经常出现的情况,在socket通讯过程中:如果通讯的一端一次性连续发送多条数据包,tcp协议会将多个数据包打包成一个tcp报文发送出去,这就是所谓的粘包。而如果通讯的一端发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。
解决方案:1、定长数据包,提前和服务器确定好一个报文的大小,如果不足用空格补充2、特殊字符结尾3、使用已有的网络应用层协议 如http
面试问题 复习完成记录
浮动主题