#面经学习001
1,violatile的语义,什么场景下会使用到**
valatile:
volatile 是一种关键字,用于告诉编译器和处理器,该变量的值可能在任何时候被修改,因此每次读取该变量的值都必须从内存中读取,而不是从寄存器或缓存中读取。这样可以保证多线程之间对该变量的访问是正确的。
指令重排序是一种优化技术,处理器可以将指令重排以提高处理器的性能。然而,这种优化技术可能会导致代码的行为与期望的不一致。
在单线程环境下,指令重排序是一个很好的优化技术。但是在多线程环境下,指令重排序可能会导致程序的行为出现问题,因为它可能会让线程读取到错误的变量值或执行错误的指令。
因此,当使用 volatile 关键字时,编译器和处理器会禁止指令重排序,以确保多线程环境下程序的正确性。
语义:1,内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。2,可以禁止指令重排序
volatile关键字保证了操作的可见性,但是不能保证对变量的操作是原子性。
volatitle 写-读的内存语义:
volatile写的内存语义:当写一个volatitle变量时,JMM会把该线程对应的 本地内存中的共享变量值刷新到主内存中。
volatile读的内存语义:当读一个volatitle变量时,JMM会把该线程对应的本地内存置为无效。
线程需要从主内存中重新读取共享变量。
使用场景:
1,状态标记量
volatile boolean flag= false;
// 线程1
context = loadContext();
flag= true;
// 线程2
while(!flag) {
sleep();
}
doSomethingWithConfig(context);
2,双重检查
所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {// 1
if (instance == null) {// 2
instance = new Singleton();// 3
}
}
}
return instance;
}
}
3,独立观察
4,“volatile bean” 模式
5,开销较低的读-写锁策略
2,指令重排序的意义
- 指令的概念:指令是指计算机执行某种操作的命令
- 指令重排序:只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
- 指令重排序分类:1,编译器重排序,JVM 中完成 2,指令级并行重排序3,处理器重排序:CPU 中完成
- Synchronized 把多线程执行环境改变为单线程执行环境,无需关心指令重排序(单线程执行结果不会改变)。
3,AOP的实现原理及使用场景有哪些
1,使用场景:
- 权限管理,控制用户的功能权限,方案详述:在@ControllerAdvice里边,处理全局请求,控制权限。权限管理的其他方案:(除了AOP之外的方案),在过滤器或者拦截器中处理,使用Shiro中间件
- 异常处理,在@ControllerAdvice里边,处理全局异常
- 日志处理,按产品的需求,有的接口需要记录操作日志
- 数据同步,增删改数据时,同时要处理MySQL和ES,将相关类作为切面,若数据库提交,则写到ES;若回滚,则不写到ES
- 事务控制,
2,通知方式:前置,后置,环绕,返回,异常。失败后,返回不会执行(即使失败,后置也会执行)
3,AOP配置:如果引入的是spring-aop包,则需要使用@EnableAspectJAutoProxy开启aop功能;如果引入的是spring-boot-starter-aop,则AOP就直接可以用了,无需加注解之类的开启它。 另外,无论是使用spring aop还是 aspectj都需要aspectjweaver.jar spring-aop.jar这两个jar包。
4,实现方式:Spring AOP的动态代理主要有两种方式实现,JDK动态代理和cglib动态代理。
5,JDK代理与CGLIB代理
SpringBoot 1.5.x:默认使用JDK代理,即:spring.aop.proxy-target-class=false 若设置为true,则使用CGLIB动态代理。
SpringBoot 2.x:默认使用CGLIB代理,即:spring.aop.proxy-target-class=true。
为什么SpringBoot 2.x:默认使用CGLIB代理?
我们应该使用@EnableTransactionManagement(proxyTargetClass = true)来防止人们不使用接口时出现讨厌的代理问题。
JDK动态代理和CGLIB动态代理都是实现AOP的方式,但它们之间有一些区别。
JDK动态代理:
- 基于接口的代理,需要目标类实现接口;
- 通过反射来实现代理;
- 只能代理接口中定义的方法;
- 原理: Proxy.newProxyInstance()。
CGLIB动态代理:
- 基于继承的代理,可以代理目标类的所有方法;
- 通过生成代理子类来实现代理;
- 没有接口的限制;
- 原理: Enhancer.create()。
综上所述,JDK动态代理适用于代理接口中定义的方法,而CGLIB动态代理适用于代理类中所有的方法。在实际应用中,如果目标对象是实现了接口的类,则使用JDK动态代理;如果目标对象是没有实现接口的类,则使用CGLIB动态代理。
6,Java类的初始化顺序及原因
Java类的初始化顺序如下:
- 父类的静态变量和静态代码块按照声明顺序依次执行。
- 子类的静态变量和静态代码块按照声明顺序依次执行。
- 父类的实例变量和代码块按照声明顺序依次执行,再执行父类的构造方法。
- 子类的实例变量和代码块按照声明顺序依次执行,再执行子类的构造方法。
这个顺序的原因是因为静态代码块和静态变量只会在类加载时执行一次,而实例变量和实例代码块会在每次创建新的实例时被执行,因此在初始化时需要先执行静态的,再执行实例的。另外,子类的初始化必须在父类之后进行,因为子类可能会依赖于父类的一些变量或方法。
7,synchronized锁class对象和普通对象的区别
synchronized锁定的是对象的锁,可以分为两种情况:
- 锁定的是class对象
当使用synchronized锁定的是class对象时,相当于锁定了所有该类的实例化对象共同的锁,即类的静态锁。
例如:
public class MyClass {
public static synchronized void synchronizedMethod() {
// do something
}
}
在上述代码中,使用了synchronized修饰的方法锁定的是MyClass类的静态锁,即锁定的是所有MyClass的实例化对象共同的锁。
- 锁定的是普通对象
当使用synchronized锁定的是普通对象时,相当于锁定了该对象的锁,即对象的实例锁。
例如:
public class MyClass {
public synchronized void synchronizedMethod() {
// do something
}
}
在上述代码中,使用了synchronized修饰的方法锁定的是MyClass类的实例锁,即锁定的是当前对象的锁。不同的实例化对象之间的锁是相互独立的,不会互相影响。
总体来说,锁定class对象与锁定普通对象的区别在于锁定的范围不同,前者是类的静态锁,后者是实例锁。
8,lock和synchronized的底层实现
synchronized底层原理:synchronized是基于JVM中的Monitor锁实现的,Java1.5之前的synchronized锁性能较低,但是从Java1.6开始,对synchronized锁进行了大量的优化,引入可锁粗话、锁消除、偏向锁、轻量级锁、适应性自旋等技术来提升synchronized的性能。
当synchronized修饰方法时,当前方法会比普通方法在常量池中多一个ACC_SYNCHRONIZED标识符,synchronized修饰方法的核心
JVM在执行程序时,会根据这个ACC_SYNCHRONIZED标识符完成方法的同步。如果调用了被synchronized修饰的方法,则调用的指令会检查方法是否设置了ACC_SYNCHRONIZED标识符。
如果方法设置了ACC_SYNCHRONIZED标识符,则当前线程先获取monitor对象。同一时刻,只会有一个线程获取monitor对象成功,进入方法体执行方法逻辑。在当前线程释放monitor对象前,其它线程无法获取同一个monitor对象,从而保证了同一时刻只有一个线程进入被synchronized修饰的方法中执行方法体的逻辑。
Lock源码分析
lock锁是在JDK层面实现的一种比内置锁更灵活的锁,它能弥补synchronized内置锁的不足,他们都通过Java提供的接口来完成加锁和解锁操作。
下面分别单独介绍一下Lock中的方法
(1)void lock();
阻塞模式抢占锁的方法。如果当前线程抢占锁成功,则继续向下执行程序的业务逻辑,否则,当前线程会阻塞,直到其它抢占到锁的线程释放锁后再继续抢占锁。
(2)void lockInterruptibly() throws InterruptedException;
可中断模式抢占锁的方法。当前线程在调用lockInterruptibly()方法抢占锁的过程中,能够响应中断信号,从而能够中断当前线程。
(3)boolean tryLock();
非阻塞模式下抢占锁的方法。当前线程调用tryLock()方法抢占锁时,线程不会阻塞,而会立即返回抢占锁的结果。
(4)boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
在tryLock()的基础上,加上限制抢占锁的时间限制。
(5)void unlock();
释放锁。
(6)Condition newCondition();
创建与当前线程绑定的Condition条件,主要用于线程间以“等待 - 通知”的方式进行通信。
所以,Lock锁支持响应中断、超时和以非阻塞的方式获取锁,全面弥补了JVM中synchronized内置锁的不足。
9,公平锁和非公平锁的区别
公平锁原理:
每个线程抢占锁的时候,都会检索锁维护的等待队列,如果等待队列为空,或者当前线程是等待队列的第一个线程,则当前线程获取到锁,否则,当前线程加入到等待队列的尾部,然后等待队列中的线程会按先进先出的规则按顺序尝试获取资源。
非公平锁原理:
非公平锁的核心就是抢占锁的所有线程是不公平的,在多线程并发环境中,每个线程在抢占锁的过程中都会先直接尝试抢占锁,如果抢占成功,就继续执行程序的业务逻辑,如果抢占失败,就会进入等待队列中排队。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
公平锁和非公平锁的区别:
非公平锁在队列的处理上比公平锁多了一个插队的过程,,如果插队时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。、
锁优化
加锁使得原本能够并行执行的操作变得串行化,串行操作会降低程序的性能,CPU对于线程的上下文切换也会降低系统的性能。下面总结一下锁优化的相关方法。
1、缩小锁的范围
将一些不会引起线程安全问题的代码,移出同步代码块,尤其是耗时的IO操作,或者可能引起阻塞的方法,这样能提高程序执行的速度。
2、减小锁的粒度
减小锁的粒度就是缩小锁定的对象,比如将一个大对象拆分成多个小对象,对这些小对象进行加锁,能够提高程序的并行度,提高程序执行的速度。
3、锁分离
锁分离最典型的技术就是读写锁,ReadWriteLock分为写锁和读锁,其中读读不互斥,读写互斥,写写互斥,这样既保证了线程安全,又提高了性能。
ReadWriteLock 使用方式:
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock(); readLock.lock(); finally中执行 readLock.unlock();
private final Lock writeLock = readWriteLock.writeLock(); writeLock.lock();finally中执行 writeLock.unlock();
4、锁分段
进一步缩小锁的粒度,对一个独立对象的锁进行分解的现象叫做锁分段。锁分段最典型的例子就是ConcurrentHashMap。
ConcurrentHashMap将数据按照不同的数据段进行存储,每个数据段分配一把锁,当某个数据段占有某个数据段的锁访问数据时,其它数据段的锁也能被其它线程抢占到,提高程序的并行度,提高程序性能。
5、锁粗化
如果同一个线程不停的请求、同步、释放同一把锁,则会降低程序的执行性能,此时可以扩大锁的范围,即进行锁粗化处理。
10,AQS(AbstractQueuedSynchronizer)的实现原理
AQS
抽象队列同步器
AQS是一个用来构建锁和其他同步组件的基础框架,使用AQS可以简单且高效地构造出应用广泛的同步器,例如我们后续会讲到的ReentrantLock、Semaphore、ReentrantReadWriteLock和FutureTask等等。
AQS实现的核心思想
如果被请求的共享资源(即锁)空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。(为了方便理解以下的共享资源都用锁替代)
如果被请求的锁被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS中的锁是使用一个int成员变量来表示同步状态(即锁被占用与否用一个int变量来表示),通过内置的FIFO队列来完成获取锁线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。状态信息通过procted类型的getState,setState,compareAndSetState进行操作。
// 同步状态,使用volatile来保证其可见性
// 在 ReentrantLock 中 state = 0表示无锁状态
// 在 Semaphore 和 CountDownLatch 中 state 表示锁的个数
private volatile int state;
// 获取同步状态
protected final int getState() {
return state;
}
// 设置同步状态
protected final void setState(int newState) {
state = newState;
}
// 原子性地修改同步状态
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
CLH队列
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求锁的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。当锁被某个线程占有,其他请求该锁的线程会被阻塞,从而进入同步队列。
Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue,即等待队列,不是必须的,它是一个单向链表,只有当使用Condition时,才会存在此单向链表,并且可能会有多个Condition queue。
Node节点的状态有如下四种
- CANCELLED = 1
表示当前节点从同步队列中取消,即当前线程被取消 - SIGNAL = -1
表示后继节点的线程处于等待状态,如果当前节点释放锁会通知后继节点,使得后继节点的线程能够运行 - CONDITION = -2
表示当前节点在等待condition,也就是在condition queue中 - PROPAGATE = -3
表示下一次共享式同步状态获取将会无条件传播下去
AQS定义两种锁共享方式
- Exclusive(独占):只能有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 - Share(共享):多个线程可同时执行,如Semaphore、CountDownLatch等。
AQS的设计模式
AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。
自定义同步器时需要重写下面几个AQS提供的模板方法:
// 该线程是否正在独占锁。只有用到condition才需要去实现它。
isHeldExclusively();
// 独占方式。尝试获取锁,成功则返回true,失败则返回false
tryAcquire(int);
// 独占方式。尝试释放锁,成功则返回true,失败则返回false。
tryRelease(int);
// 共享方式。尝试获取锁。负数表示失败;0表示成功,但没有剩余可用锁;正数表示成功,且有剩余锁。
tryAcquireShared(int);
// 共享方式。尝试释放锁,成功则返回true,失败则返回false。
tryReleaseShared(int);
上面的方法均使用protect修饰,且默认实现都会抛出UnsupportedOperationException异常。这些方法的实现必须是内部线程安全的。AQS类中的其他方法都是final ,所以无法被其他类覆盖。
AQS实现原理
AQS核心方法
AQS中方法众多,总体可分为两大类,独占式和共享式地获取和释放锁。
独占式锁获取(acquire方法)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该方法中一共出现了四个方法
tryAcquire:尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法由自定义同步组件自己去实现,该方法必须要保证线程安全的获取锁。
addWaiter:入队,即将当前线程加入到CLH同步队列尾部。
acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止,并且返回当前线程在等待过程中有没有中断过。
selfInterrupt:自我中断
独占式锁执行流程
1. tryAcquire(自定义同步组件)
首先调用方法tryAcquire方法(该方法需要自定义同步组件自己实现,ReentrantLock中的FairSync和NonFairSync等都有不同的实现)尝试获取锁,如果成功返回true,整个acquire方法全部返回,获取锁成功不执行阻塞,否则获取锁失败,进行往下执行阻塞操作;
2. addWaiter(加入到CLH同步队列尾部)
tryAcquire返回false,获取锁失败,则继续执行方法acquireQueued,在此之前会先执行方法addWaiter,将当前线程加入到CLH同步队列尾部;
3. acquireQueued(自旋获取锁,抢占锁的操作)
入队操作结束后执行方法acquireQueued,将添加到队列中的Node作为参数传入acquireQueued方法,这里面会做抢占锁的操作。该方法是一个自旋的过程,即每个线程进入同步队列中,都会自旋地观察自己是否满足条件且获取到锁,如果满足条件就可以从自旋过程中退出,否则继续自旋下去。
4. shouldParkAfterFailedAcquire
自旋获取锁,如果获取锁失败,会执行方法shouldParkAfterFailedAcquire判断是否需要进行线程挂起。从上面的分析可以看出,只有队列的第二个节点可以有机会争用锁,如果成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点,if (p == head)条件不成立,首先进行shouldParkAfterFailedAcquire(p, node)操作shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,是说明此节点已经将状态设置,如果锁释放,则应当通知它,所以它可以安全的阻塞了,返回true。
5. parkAndCheckInterrupt
如果shouldParkAfterFailedAcquire返回false,则继续下次自旋。
如果shouldParkAfterFailedAcquire返回了true,则会执行:parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。
6. cancelAcquire
最后执行finally块中的cancelAcquire方法(只有在自旋过程中发生异常时才执行,因为此时failed为true),该方法的作用就是取消当前线程对资源的获取,即设置该结点的状态为CANCELLED。
7. selfInterrupt
最后如果acquireQueued方法返回false,acquire方法直接结束。否则返回true表示当前线程被中断过,需要恢复它的中断标记,所以调用方法selfInterrupt进行自我中断。
独占式锁释放(release方法)
该方法中包含两个方法
tryRelease:尝试释放锁,成功则返回true,失败则返回false。同样也是由自定义组件自己实现
unparkSuccessor:当释放锁成功之后,唤醒当前节点的后继结点。
在方法unparkSuccessor(Node)中,就意味着真正要释放锁了,它传入的是head节点(head节点是占用锁的节点),当前线程被释放之后,需要唤醒下一个节点的线程。
流程:
独占式获取响应中断(acquireInterruptibly方法)
前面说到的acquire方法对中断不响应。因此为了响应中断,AQS提供了acquireInterruptibly方法,该方法在等待获取锁时,如果当前线程被中断了,会立刻响应中断抛出异常InterruptedException。
独占式超时获取(tryAcquireNanos方法)
AQS还提供了一个增强版的获取锁的方法,tryAcquireNanos方法不仅可以响应中断,还有超时控制,即当前线程没有在指定时间内获取锁则返回失败。原理和acquire方法大致相同,只是加入了超时控制特性。
共享式
同一时刻可以有多个线程获取锁
共享式锁获取(acquireShared方法)
AQS提供acquireShared方法共享式获取锁,共享式与独享式实现原理大致相同。
不同点在于:
- 节点入队时为共享节点;
- 自定义同步器实现tryAcquireShared方法,必须以共享方式实现,比如:Semaphore在实现这个方法时,用到int remaining = available - acquires;来控制锁的剩余量。
public final void acquireShared(int arg) {
// 尝试获取锁,小于0表示获取锁失败
if (tryAcquireShared(arg) < 0)
// 自旋获取锁
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
// 入队,此时节点为共享式节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的前驱节点
final Node p = node.predecessor();
// 如果p是头节点
if (p == head) {
// 尝试获取锁
int r = tryAcquireShared(arg);
// 如果r大于等于0,表示获取锁成功
if (r >= 0) {
// 此方法对CountDownLatch唤醒所有的await线程很重要
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 如果执行这个方法,那么propagate一定等于1
private void setHeadAndPropagate(Node node, int propagate) {
// 获取头结点
Node h = head;
// 因为当前节点被唤醒,设置当前节点为头结点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取当前节点的下一个节点
Node s = node.next;
// 如果下一个节点为null或者节点为shared节点,则继续循环唤醒await的线程
if (s == null || s.isShared())
doReleaseShared();
}
}
共享式锁释放(releaseShared方法)
原理和release方法大致相同,不再赘述。
11,Java线程池七个参数详解
ThreadPoolExecutor
corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler
- corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。 - maximumPoolSize 线程池最大线程数量
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。 - keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定 - unit 空闲线程存活时间单位
keepAliveTime的计量单位 - workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。 - threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等 - handler 拒绝策略
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
12,分布式事务处理
微服务分布式服务问题
在微服务中单体应用被拆成了微服务,每个服务都是一个分别使用单独的一个数据源。三个服务之间通过RPC实现业务调用。每一个服务内部的数据一致性仍由本地事务来保证。而整个业务层面的全局数据一致性要如何保障呢?这就是微服务架构下面临的,典型的分布式事务需求:我们需要一个分布式事务的解决方案保障业务全局的数据一致性。
什么是分布是事务
分布式事务就是为了保证不同数据库的数据一致性。
什么是分布式系统:部署在不同节点上的系统通过网络交互来完成协同工作的系统。
分布式事务应用在哪些场景
电商系统中的下单扣库存 电商系统中,订单系统和库存系统是两个系统,一次下单的操作由两个系统协同完成…等
分布式事务解决方案
1.XA两段提交(低效率)-分布式事务解决方案
总结:两阶段提交就是 1.就是当协调者向参与者发送请求之后,当参与者收到请求之后执行对应的操作但是不进行提交事务,而是向协调者发送完成或者失败的消息。2.第二阶段就是协调者拿到不同的参与者的反馈后进行判断如果有一个失败则继续发送消息给参与者都进行回滚,如果都成功的话就发送消息都提交,他的缺点就是性能各个节点会占用资源,存在单点故障问题(一旦事务的协调者挂掉的话就无法完成事务),各个参与者的如果有一个存在局部网络问题的话会导致各个节点之间数据不一致的问题
2.TCC三段提交(2段,高效率[不推荐(补偿代码)])
TCC总结:TCC的缺点就是开发的代码量会比较大,需要写三个接口。比起XA的优点是它不是长时间的去占有锁,当try结束后就会释放锁可以让其他事务使用,而XA则是一直占用直到事务执行结束之后才会释放。TCC两阶段提交与XA两阶段提交的区别
3.本地消息(MQ+Table)
总结:添加一个本地消息表和一个mq消息队列,如图所示的步骤中当5修改状态的时候故障的时候就会导致消息表这里一直收不到状态码的消息这时候就会导致不停的轮询发送消息导致库存不停的在变,如何解决这个问题呢我们可以使用乐观锁去解决。
4.事务消息(RocketMQ[alibaba])
总体流程总结:首先消息发送放发送一个half消息告诉MQ服务器我有一个事务要准备操作了,当MQ服务器给消息发送方回复一个消息的成功的消息的时候,然后发送方开始执行本地事务,发送发会判断自己的事务执行成功或者失败,然后发送一个消息给消息队列告诉消息队列提交或者回滚,如果是提交的话这时候的half消息就变成了全消息。然后就会去接收消息方的本地执行事务操作,但是如果是回滚的话,消息队列就会将half消息删掉,当发送方执行结束后发送给消息队列提交或者回滚的消息的时候因为网络或者其他问题导致发送失败的话,消息队列会有一个回查本地事务的机制。
5.Seata(alibaba)
我们的微服务分布式中主要使用的是seate中的AT模式
seate又称简单可扩展自治事务框架。
解决分布式事务问题,有两个设计初衷
1.对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
2.高性能:减少分布式事务解决方案所带来的性能消耗
Seata中有两种分布式事务实现方案,AT及MT(TCC)
AT模式主要关注多 DB 访问的数据一致性,当然也包括多服务下的多 DB 数据访问一致性问题 2PC-改进 TCC
模式主要关注业务拆分,在按照业务横向扩展资源时,解决微服务间调用的一致性问题
Seata 的设计思路是将一个分布式事务可以理解成一个全局事务,下面挂了若干个分支事务,而一个分支事务是一个满足 ACID 的本地事务,因此我们可以操作分布式事务像操作本地事务一样。
AT模式(Automatic (Branch) Transaction Mode)
Transaction Coordinator (TC):事务协调器,维护全局事务的运行状态,负责协调并决定全局事务的提交或回滚。
Transaction Manager(TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM):资源管理器,负责本地事务的注册,本地事务状态的汇报(投票),并且负责本地事务的提交和回滚。
XID:一个全局事务的唯一标识
其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。
一个分布式事务在Seata中的执行流程:
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
XID 在微服务调用链路的上下文中传播。
RM 向 TC 注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC
TM 根据 TC 中所有的分支事务的执行情况,发起全局提交或回滚决议。
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
Seata 中有三大模块,分别是 TM、RM 和 TC。 其中 TM 和 RM 是作为Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。
总结:
流程就是TM需要发送请求的时候首先向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID,然后这个XID会在微服务调用的整个链路中赋给每一个微服务,每个微服务的RM会去TC(seate)中注册分支事务,然后会执行分支事务并且提交这里是关键执行完事务之后就会直接提交,然后将结果汇报给TC,TM根据TC的中所有分支的执行情况发起全局的提交或者回滚。最终使全部的分支都完成提交或者回滚的操作。
MT模式(Manual (Branch) Transaction Mode)
Seata还支持MT模式
MT模式本质上是一种TCC方案,业务逻辑需要被拆分为 Prepare/Commit/Rollback 3 部分,形成一个 MT 分支,加入全局事务。
MT 模式一方面是 AT 模式的补充。另外,更重要的价值在于,通过 MT 模式可以把众多非事务性资源纳入全局事务的管理中。
就相当与将steate和微服务都注册到Nacos中就可以使得steate直接访问Nacos中的微服务
13,Redis分布式锁过期怎么解决
场景:
一般情况下我们加锁时,都会指定过期时间参数,当任务执行时间超过了锁过期时间,下一个任务进来时就会获取到锁,造成异常。
针对过期时间常见有两种处理方法:
自动续期:锁快到期时,通过定时任务自动续期
加锁不设置过期时间:任务不执行完,锁就不会过期
解决方案
第一种方案:当设置了过期时间后,如果还执行自动续期操作,那么这个锁的实际过期时间就与我们在加锁时设置的过期时间不符合,产生了逻辑上的冲突!所以博主认为自动续期操作对已经设置了过期时间的锁不适用。
第二种方案:加锁不设置过期时间的话,理论上好像是可以解决这个问题,任务不执行完,锁就不会释放。但是实际针对一些极端异常场景下,如果任务执行过程中,服务器宕机、网络断连等都可能造成锁释放不了,比如加锁成功了,执行中发生了宕机,程序直接没了,但是锁还在,另一个任务就一直获取不到锁。
综合来看:博主认为如果加锁代码需要添加过期时间,其实不需要进行自动续期操作。当我们需要确保当前任务没执行完,下一个任务一定不能获取到锁时,可以不设置过期时间。
那怎么避免第二种方案中,异常场景下,锁一直未释放的问题嘞?
答案是在加锁成功时,如果没有指定过期时间,则给一个默认过期时间比如三十秒,通过定时任务给我们的锁进行自动续期,这样就既可以解决锁一直未释放的问题,又能保证下一任务获取不到当前任务的锁。
14,HTTP和RPC区别
OSI 网络七层模型
第一层:应用层。定义了用于在网络中进行通信和传输数据的接口。
第二层:表示层。定义不同的系统中数据的传输格式,编码和解码规范等。
第三层:会话层。管理用户的会话,控制用户间逻辑连接的建立和中断。
第四层:传输层。管理着网络中的端到端的数据传输。
第五层:网络层。定义网络设备间如何传输数据。
第六层:链路层。将上面的网络层的数据包封装成数据帧,便于物理层传输。
第七层:物理层。这一层主要就是传输这些二进制数据。
实际应用过程中,五层协议结构里面是没有表示层和会话层的。应该说它们和应用层合并了。
我们应该将重点放在应用层和传输层这两个层面。因为 HTTP 是应用层协议,而 TCP 是传输层协议。
RPC 服务之RPC 架构
我们可以很清楚地看到,一个完整的 RPC 架构里面包含了四个核心的组件。
分别是:
Client
Server
Client Stub
Server Stub(这个Stub大家可以理解为存根)
客户端(Client),服务的调用方。
服务端(Server),真正的服务提供者。
客户端存根,存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
服务端存根,接收客户端发送过来的消息,将消息解包,并调用本地的方法。
HTTP 服务
其实在很久以前,我对于企业开发的模式一直定性为 HTTP 接口开发,也就是我们常说的 RESTful 风格的服务接口。
的确,对于在接口不多、系统与系统交互较少的情况下,解决信息孤岛初期常使用的一种通信手段;优点就是简单、直接、开发方便。
利用现成的 HTTP 协议进行传输。我们记得之前本科实习在公司做后台开发的时候,主要就是进行接口的开发,还要写一大份接口文档,严格地标明输入输出是什么?说清楚每一个接口的请求方法,以及请求参数需要注意的事项等。
比如下面这个例子:
POST http://www.httpexample.com/restful/buyer/info/shar
接口可能返回一个 JSON 字符串或者是 XML 文档。然后客户端再去处理这个返回的信息,从而可以比较快速地进行开发。
但是对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC 框架的好处就显示出来了,首先就是长链接,不必每次通信都要像 HTTP 一样去 3 次握手什么的,减少了网络开销。
其次就是 RPC 框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。
总结
HTTP和RPC都是协议,用于在客户端和服务器之间传输数据。但是它们有以下区别:
-
HTTP是基于请求/响应模式的通信协议,而RPC是一种远程过程调用协议,它允许客户端像本地函数一样调用远程服务。
-
HTTP主要用于Web应用程序,而RPC可用于任何应用程序。
-
HTTP使用RESTful API,通常通过HTTP GET、POST、PUT、DELETE请求来进行通信,而RPC使用RPC API,通常使用像gRPC或Thrift这样的协议来进行通信。
-
HTTP通常基于文本格式(如JSON或XML)来传输数据,而RPC通常使用二进制格式(如Protocol Buffers或MessagePack)进行数据传输。
-
因为RPC协议提供了更高效的数据传输方式和更快的响应时间,所以它通常用于大型分布式应用程序。HTTP协议则更适用于Web应用程序,特别是基于浏览器的应用程序。
15,服务发现-从原理到实现
1概念
服务发现之所以重要,是因为它解决了微服务架构最关键的问题:如何精准的定位需要调用的服务ip以及端口。无论使用哪种方式来提供服务发现功能,大致上都包含以下三点:
Register, 服务启动时候进行注册
Query, 查询已注册服务信息
Healthy Check,确认服务状态是否健康
整个过程很简单。大致就是在服务启动的时候,先去进行注册,并且定时反馈本身功能是否正常。由服务发现机制统一负责维护一份正确或者可用的服务清单。因此,服务本身需要能随时接受查下,反馈调用方服务所要的信息。
微服务架构模式下,服务实例动态配置,因此服务消费者需要动态了解到服务提供者的变化,所以必须使用服务发现机制。
服务发现的关键部分是注册中心。注册中心提供注册和查询功能。目前业界开源的有Netflix Eureka、Etcd、Consul或Apache Zookeeper,大家可以根据自己的需求进行选择。
服务发现主要有两种发现模式:客户端发现和服务端发现。客户端发现模式要求客户端负责查询注册中心,获取服务提供者的列表信息,使用负载均衡算法选择一个合适的服务提供者,发送请求。服务端发现模式,客户端每次都请求注册中心,由注册中心内部选择一个合适的服务提供者,并将请求转发至该服务提供者,需要注意的是 当一个请求过来的时候,注册中心内部获取服务提供者列表和使用负载均衡算法。
这个世界没有完美的架构和模式,不同的场景都有适合的解决方案。我们在调研决策的时候,一定要根据实际情况去权衡对比,选择最适合当前阶段的方案,然后通过渐进迭代的方式不断完善优化方案