社招的面试难度与校招面试还是有一定的差别,校招因为没有实际的工作项目,会更考察基础内容多一些。
社招因为都是有工作经验,所以会基于项目中涉及到的知识点深入问,考察你对用过的技术栈的深度,而且也会从实际场景操作和源码方面进行提问,难度可谓是上了一个档次!
但是互联网中大厂的社招,算法还是逃不了,还是有手撕的环节,但是一般面试官也不会刁难你,真想要你的话,一般都是出个简单的算法题,走个流程,如果这都不会,那就真没办法了,所以社招跳槽中大厂,算法还是要熟练起来
接下来让我们一起来看看吧,大家看看难度如何?
考察的知识点,我给大家罗列了一下:
为了有个宏观的认识,我们先来看下ThreadLocal
的内存结构图
从内存结构图,我们可以看到:
Thread
类中,有个ThreadLocal.ThreadLocalMap
的成员变量。ThreadLocalMap
内部维护了Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型对象值。关键源码分析
对照着几段关键源码来看,更容易理解一点哈~我们回到Thread
类源码,可以看到成员变量ThreadLocalMap
的初始值是为null
public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap是Thread的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap的关键源码如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//Entry数组
private Entry[] table;
// ThreadLocalMap的构造器,ThreadLocal作为key
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
ThreadLocal类中的关键set()方法:
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程t
ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
if (map != null) //如果获取的ThreadLocalMap对象不为空
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //创建一个新的ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
}
void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal
}
ThreadLocal类中的关键get()方法
public T get() {
Thread t = Thread.currentThread();//获取当前线程t
ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
if (map != null) { //如果获取的ThreadLocalMap对象不为空
//由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //初始化threadLocals成员变量的值
}
private T setInitialValue() {
T value = initialValue(); //初始化value的值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
if (map != null)
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //实例化threadLocals成员变量
return value;
}
综上所述:
Thread
线程类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,即每个线程都有一个属于自己的ThreadLocalMap
。ThreadLocalMap
内部维护着Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型值。Thread
,在往ThreadLocal
里设置值的时候,都是往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而可以实现了线程隔离。公平锁和非公平锁的实现都是基于AbstractQueuedSynchronizer(AQS),这是Java并发框架中用于构建锁和同步器的基础框架。ReentrantLock内部通过继承AbstractQueuedSynchronizer(AQS)来实现锁的机制。AQS使用一个volatile int state字段来表示锁的状态,以及一个内部类Node来维护等待队列。
AQS的核心方法
ReentrantLock的实现
在ReentrantLock的构造函数中,你可以选择创建公平锁或非公平锁。默认情况下,不带参数的构造函数创建非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
NonfairSync和FairSync分别扩展了AbstractQueuedSynchronizer,并重写了tryAcquire()方法。
非公平锁(默认)
非公平锁试图在获取锁时立即进行CAS操作,即使已经有其他线程在等待。
这意味着它可能会让新来的线程“插队”,这在理论上是不公平的,但在实践中往往能提供更好的性能,因为减少了线程的等待时间。
在非公平锁中,tryAcquire()方法首先尝试获取锁,而不考虑是否有线程在等待队列中。
protected final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 直接尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁
公平锁则会保证锁的分配顺序,即后来的线程必须等待前面的线程释放锁才能获取。
这样虽然理论上更公平,但可能增加线程等待的时间,从而影响性能。 在公平锁中,tryAcquire()方法会检查等待队列中是否有线程在等待,如果有,则不会尝试获取锁,除非队列为空或当前线程已经是队列的头部。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 检查队列中是否有等待线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// 可重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
synchronized关键字在Java中用于控制多线程对共享资源的访问,确保同一时刻只有一个线程能够执行临界区代码。锁升级机制允许锁从一种成本较低的状态升级到成本较高的状态,具体包括以下几种锁状态:
锁升级过程是单向的,意味着一旦锁升级到更重的级别,它就不会降级回到更轻的级别。这种机制可以避免不必要的锁膨胀和收缩带来的开销,特别是在锁竞争较少的情况下,可以显著提高程序的执行效率。同时,在对象的Mark Word中记录了锁的当前状态。
Mark Word是对象头的一部分,它包含对象的元数据信息,如哈希码、GC分代年龄、锁状态标志、线程ID等。当锁升级时,Mark Word中的信息也会相应地改变,以反映新的锁状态。
在Java中,要实现多个任务同时达到某个临界点后由主线程执行某些操作,可以使用CountDownLatch或者CyclicBarrier这两个工具类,它们都位于java.util.concurrent包下。
下面分别介绍这两种方式的使用方法:
使用CountDownLatch
CountDownLatch是一个同步辅助类,它允许一个或多个线程等待直到其他线程完成了操作。它通过一个计数器来实现这一功能,每当一个线程完成了操作,计数器减一;当计数器到达零时,所有因为计数器未到达零而在await()方法上等待的线程都将被释放。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int threadCount = 5;
final CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
// 执行一些操作...
System.out.println(Thread.currentThread().getName() + " is ready.");
latch.countDown();
}).start();
}
// 等待所有线程完成它们的操作
latch.await();
System.out.println("All threads have reached the critical point. Main thread continues...");
// 主线程继续执行...
}
}
使用CyclicBarrier
CyclicBarrier与CountDownLatch类似,也允许一组线程互相等待直到到达某个公共屏障点(barrier)。但是CyclicBarrier支持在屏障点执行一个回调操作,并且在每次所有参与线程都到达屏障点后,它可以被重用。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
final int threadCount = 5;
final CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("All threads have reached the critical point. Main thread executes callback...");
// 主线程在这里执行回调操作
});
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
// 执行一些操作...
System.out.println(Thread.currentThread().getName() + " is ready.");
try {
barrier.await(); // 等待所有线程到达屏障点
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
在这两个例子中,多个线程各自执行一些操作,当它们都到达临界点时,主线程才继续执行后续的操作。根据具体的应用场景,你可以选择使用CountDownLatch或CyclicBarrier。如果只需要一次性触发事件,可以选择CountDownLatch;如果需要多次循环等待所有线程到达,CyclicBarrier可能更加合适。
方法解析
源码解析
CyclicBarrier(int parties, Runnable barrierAction)和await()方法是CyclicBarrier的核心,本篇重点分析这两个方法的背后实现原理。 首先,看一下CyclicBarrier内声明的一些属性信息:
//用于保护屏障入口的锁
private final ReentrantLock lock = new ReentrantLock();
//线程等待条件
private final Condition trip = lock.newCondition();
//记录参与等待的线程数
private final int parties;
//当所有线程到达屏障点之后,首先执行的命令
private final Runnable barrierCommand;
private Generation generation = new Generation();
//实际中仍在等待的线程数,每当有一个线程到达屏障点,count值就会减一;当一次新的运算开始后,count的值被重置为parties
private int count;
其中,Generation是CyclicBarrier的一个静态内部类,它只有一个boolean类型的属性,具体代码如下:
private static class Generation {
boolean broken = false;
}
当使用构造方法创建CyclicBarrier实例的时候,就是给上面这些属性赋值,
//创建一个CyclicBarrier实例,parties指定参与相互等待的线程数,
//barrierAction指定当所有线程到达屏障点之后,首先执行的操作,该操作由最后一个进入屏障点的线程执行。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
//创建一个CyclicBarrier实例,parties指定参与相互等待的线程数
public CyclicBarrier(int parties) {
this(parties, null);
}
当调用await()方法时,当前线程已经到达屏障点,当前线程阻塞进入休眠状态,
//该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态
//直到所有线程都到达屏障点,当前线程才会被唤醒
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen;
}
}
//该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态
//在timeout指定的超时时间内,等待其他参与线程到达屏障点
//如果超出指定的等待时间,则抛出TimeoutException异常,如果该时间小于等于零,则此方法根本不会等待
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
//使用独占资源锁控制多线程并发进入这段代码
final ReentrantLock lock = this.lock;
//独占锁控制线程并发访问
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
//如果线程中断,则唤醒所有等待线程
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//每调用一次await()方法,计数器就减一
int index = --count;
//当计数器值等于0的时
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//如果在创建CyclicBarrier实例时设置了barrierAction,则先执行barrierAction
if (command != null)
command.run();
ranAction = true;
//当所有参与的线程都到达屏障点,为唤醒所有处于休眠状态的线程做准备工作
//需要注意的是,唤醒所有阻塞线程不是在这里
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
//让当前执行的线程阻塞,处于休眠状态
trip.await();
else if (nanos > 0L)
//让当前执行的线程阻塞,在超时时间内处于休眠状态
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//释放独占锁
lock.unlock();
}
}
private void nextGeneration() {
//为唤醒所有处于休眠状态的线程做准备工作
trip.signalAll();
//重置count值为parties
count = parties;
//重置中断状态为false
generation = new Generation();
}
private void breakBarrier() {
//重置中断状态为true
generation.broken = true;
//重置count值为parties
count = parties;
//为唤醒所有处于休眠状态的线程做准备工作
trip.signalAll();
}
到这里CyclicBarrier的实现原理基本已经都清楚了,下面来深入源码分析一下线程阻塞代码trip.await()和线程唤醒trip.signalAll()的实现。
//await()是AQS内部类ConditionObject中的方法
public final void await() throws InterruptedException {
//如果线程中断抛异常
if (Thread.interrupted())
throw new InterruptedException();
//新建Node节点,并将新节点加入到Condition等待队列中
//Condition等待队列是AQS内部类ConditionObject实现的,ConditionObject有两个属性,分别是firstWaiter和lastWaiter,都是Node类型
//firstWaiter和lastWaiter分别用于代表Condition等待队列的头结点和尾节点
Node node = addConditionWaiter();
//释放独占锁,让其它线程可以获取到dowait()方法中的独占锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//检测此节点是否在资源等待队列(AQS同步队列)中,
//如果不在,说明此线程还没有竞争资源锁的权利,此线程继续阻塞,直到检测到此节点在资源等待队列上(AQS同步队列)中
//这里出现了两个等待队列,分别是Condition等待队列和AQS资源锁等待队列(或者说是同步队列)
//Condition等待队列是等待被唤醒的线程队列,AQS资源锁等待队列是等待获取资源锁的队列
while (!isOnSyncQueue(node)) {
//阻塞当前线程,当前线程进入休眠状态,可以看到这里使用LockSupport.park阻塞当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//addConditionWaiter()是AQS内部类ConditionObject中的方法
private Node addConditionWaiter() {
Node t = lastWaiter;
// 将condition等待队列中,节点状态不是CONDITION的节点,从condition等待队列中移除
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//以下操作是用此线程构造一个节点,并将之加入到condition等待队列尾部
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//signalAll是AQS内部类ConditionObject中的方法
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//Condition等待队列的头结点
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
//将Condition等待队列中的Node节点按之前顺序都转移到了AQS同步队列中
transferForSignal(first);
first = next;
} while (first != null);
}
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//这里将Condition等待队列中的Node节点插入到AQS同步队列的尾部
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
//ReentrantLock#unlock()方法
public void unlock() {
//Sync是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer,它是ReentrantLock中公平锁和非公平锁的基础实现
sync.release(1);
}
public final boolean release(int arg) {
//释放锁
if (tryRelease(arg)) {
//AQS同步队列头结点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒节点中的线程
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒阻塞线程
LockSupport.unpark(s.thread);
}
原理总结
CyclicBarrier简单使用样例
public class CyclicBarrierDemo {
@Test
public void test() {
final CyclicBarrier barrier = new CyclicBarrier(2, myThread);
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
barrier.await();
System.out.println(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}, "thread1").start();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName());
barrier.await();
System.out.println(Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}, "thread2").start();
}
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("myThread");
}
}, "thread3");
}
结果输出:
thread1
thread2
myThread
thread2
thread1
用上面的示例总结一下CyclicBarrier的await方法实现。
假设线程thread1和线程thread2都执行到CyclicBarrier的await(),都进入dowait(boolean timed, long nanos),thread1先获取到独占锁,执行到 --count 的时,index等于1,所以进入下面的for循环,接着执行trip.await(),进入await()方法,执行Node node = addConditionWaiter() 将当前线程构造成Node节点并加入到 Condition 等待队列中,然后释放获取到的独占锁,当前线程进入阻塞状态。
此时,线程thread2可以获取独占锁,继续执行--count,index等于0,所以先执行command.run(),输出myThread,然后执行nextGeneration(),nextGeneration()中 trip.signalAll() 只是将Condition等待队列中的Node节点按之前顺序都转移到了AQS同步队列中,这里也就是将thread1对应的Node节点转移到了AQS同步队列中,thread2执行完nextGeneration(),返回return 0之前,细看代码还需要执行lock.unlock(),这里会执行到ReentrantLock的unlock()方法,最终执行到AQS的unparkSuccessor(Node node)方法,从AQS同步队列中的头结点开始释放节点,唤醒节点对应的线程,即thread1恢复执行。
如果有三个线程thread1、thread2和thread3,假设线程执行顺序是thread1、thread2、thread3,那么thread1、thread2对应的Node节点会被加入到Condition等待队列中。
当thread3执行的时候,会将thread1、thread2对应的Node节点按thread1、thread2顺序转移到AQS同步队列中,thread3执行lock.unlock()的时候,会先唤醒thread1,thread1恢复继续执行,thread1执行到lock.unlock()的时候会唤醒thread2恢复执行。
CyclicBarrier可以复用。
CyclicBarrier设计成可复用是因为它内部维护了一个“generation”计数器,这使得即使所有线程通过了一次屏障,CyclicBarrier也可以准备下一次的屏障。
如果在某个时刻线程因为异常或其他原因没有成功通过屏障,CyclicBarrier可以通过调用reset()方法重置状态,这会清除所有等待线程的状态,并允许新的线程组再次使用同一个CyclicBarrier实例。
源码部分
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
步骤1: 创建Maven项目
首先,需要创建一个新的Maven项目。在pom.xml中添加Spring Boot的starter parent和一些必要的依赖。例如:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
步骤2: 添加自动配置
在src/main/resources/META-INF/spring.factories中添加自动配置的元数据。例如:
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.example.starter.MyAutoConfiguration
然后,创建MyAutoConfiguration类,该类需要@Configuration和@EnableConfigurationProperties注解。@EnableConfigurationProperties用于启用你定义的配置属性类。
@Configuration
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {
@Autowired
private MyProperties properties;
@Bean
public MyService myService() {
return new MyServiceImpl(properties);
}
}
步骤3: 创建配置属性类
创建一个配置属性类,使用@ConfigurationProperties注解来绑定配置文件中的属性。
@ConfigurationProperties(prefix = "my")
public class MyProperties {
private String name;
// getters and setters
}
步骤4: 创建服务和控制器
创建一个服务类和服务实现类,以及一个控制器来展示你的starter的功能。
@Service
public interface MyService {
String getName();
}
@Service
public class MyServiceImpl implements MyService {
private final MyProperties properties;
public MyServiceImpl(MyProperties properties) {
this.properties = properties;
}
@Override
public String getName() {
return properties.getName();
}
}
@RestController
public class MyController {
private final MyService myService;
public MyController(MyService myService) {
this.myService = myService;
}
@GetMapping("/name")
public String getName() {
return myService.getName();
}
}
步骤5: 发布Starter
将你的starter发布到Maven仓库,无论是私有的还是公共的,如Nexus或Maven Central。
步骤6: 使用Starter
在你的主应用的pom.xml中添加你的starter依赖,然后在application.yml或application.properties中配置你的属性。
my:
name: Hello World
在MySQL中,回表是指在使用非聚集索引(也称为二级索引或辅助索引)进行查询时,需要额外访问主键索引(聚集索引)以获取完整的行数据的过程。这是因为非聚集索引中通常只存储了索引列的值和指向主键的引用,而不包含完整的行数据。
回表通常在以下情况发生:
主键索引的变化
主键索引通常是聚集索引,在InnoDB中,数据行实际上是存储在主键索引的B+树结构中的叶子节点上的。这意味着数据行的物理位置是由主键值确定的。当你执行一个更新操作时:
辅助索引的变化
辅助索引(也称非聚集索引或二级索引)在InnoDB中包含两部分信息:索引列的值和主键值(或主键的一部分,取决于索引设计)。更新操作对辅助索引的影响如下:
联合索引的变化
联合索引是包含多个列的索引。更新操作对联合索引的影响取决于更新的是哪些列:
MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
Read View 有四个重要的字段:
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if (k == 0 || head == null || head.next == null) {
return head;
}
int n = 1;
ListNode iter = head;
while (iter.next != null) {
iter = iter.next;
n++;
}
int add = n - k % n;
if (add == n) {
return head;
}
iter.next = head;
while (add-- > 0) {
iter = iter.next;
}
ListNode ret = iter.next;
iter.next = null;
return ret;
}
}