Java
HashMap底层实现原理
- 在 JDK 1.7 版本之前, HashMap 数据结构是数组和链表,HashMap通过哈希算法将元素的键(Key)映射到数组中的槽位(Bucket)。如果多个键映射到同一个槽位,它们会以链表的形式存储在同一个槽位上,因为链表的查询时间是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了。
图片
- 所以在 JDK 1.8 版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,查找时使用红黑树,时间复杂度O(log n),可以提高查询性能,但是在数量较少时,即数量小于6时,会将红黑树转换回链表。
图片
介绍一下ConcurrentHashMap;
- 在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素。简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
图片
- JDK 1.8 也引入了红黑树,优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。ConcurrentHashMap 主要通过 volatile + CAS 或者 synchronized 来实现的线程安全的,ConcurrentHashMap通过对头结点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。
图片
ThreadLocal的key是什么
为了有个宏观的认识,我们先来看下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,从而可以实现了线程隔离。
Reentranlock,公平锁/非公平锁的实现
公平锁和非公平锁的实现都是基于AbstractQueuedSynchronizer(AQS),这是Java并发框架中用于构建锁和同步器的基础框架。ReentrantLock内部通过继承AbstractQueuedSynchronizer(AQS)来实现锁的机制。AQS使用一个volatile int state字段来表示锁的状态,以及一个内部类Node来维护等待队列。
AQS的核心方法
- getState(): 获取state的值。
- setState(): 设置state的值。
- compareAndSetState(expected, update): 原子地更新state的值。
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;
}
synchronize锁升级过程
synchronized关键字在Java中用于控制多线程对共享资源的访问,确保同一时刻只有一个线程能够执行临界区代码。锁升级机制允许锁从一种成本较低的状态升级到成本较高的状态,具体包括以下几种锁状态:
- 无锁: 在没有线程访问同步块的情况下,对象头中的Mark Word中不包含锁信息。
- 偏向锁: 就是指一个线程多次重复访问同一段同步代码块时,会在对象头和栈帧中的锁记里面添加偏向的线程ID,以后在该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地比对一下对象头的markword里面是否存储着指向当前线程的偏向锁。减少CAS加锁带来的开销。
- 轻量级锁:如果一个线程拥有偏向锁,此时另一个线程尝试使用CAS将对象头的markword的锁指针指向自己。如果失败了,那么他会自旋尝试获取锁。此时为轻量级锁的状态。
- 重量级锁:当自旋次数达到阈值,或者来了第三个线程争夺锁时,轻量级锁就会升级为重量级锁。
锁升级过程是单向的,意味着一旦锁升级到更重的级别,它就不会降级回到更轻的级别。这种机制可以避免不必要的锁膨胀和收缩带来的开销,特别是在锁竞争较少的情况下,可以显著提高程序的执行效率。同时,在对象的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的实现
方法解析
图片
- CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数,barrierAction指定当所有线程到达屏障点之后,首先执行的操作,该操作由最后一个进入屏障点的线程执行。
- CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。
- getParties() 返回参与相互等待的线程数。
- await() 该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,直到所有线程都到达屏障点,当前线程才会被唤醒。
- await(long timeout, TimeUnit unit) 该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,在timeout指定的超时时间内,等待其他参与线程到达屏障点;如果超出指定的等待时间,则抛出TimeoutException异常,如果该时间小于等于零,则此方法根本不会等待。
- isBroken() 判断此屏障是否处于中断状态。如果因为构造或最后一次重置而导致中断或超时,从而使一个或多个参与者摆脱此屏障点,或者因为异常而导致某个屏障操作失败,则返回true;否则返回false。
- reset() 将屏障重置为其初始状态。
- getNumberWaiting() 返回当前在屏障处等待的参与者数目,此方法主要用于调试和断言。
源码解析
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恢复执行。
CountdownLatch和CyclicBarrier,哪个可以复用,为什么
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();
}
多线程在实际应用的场景
- 并发处理和并行计算:在Web服务器、数据库服务器或任何网络服务器中,多线程可以同时处理多个客户端的请求,提高服务器的吞吐量和响应能力。并且,在执行大量的独立任务时,如图像处理、数据清洗、报表生成等,可以将任务分解并分配给多个线程并行处理,加速处理过程。
- 异步操作和事件驱动:在图形用户界面中,多线程可以用来处理耗时的后台任务,如文件读写、网络请求等,以避免阻塞UI线程,确保界面的响应性。并且,使用多线程处理网络请求和响应,可以实现非阻塞I/O,提高网络应用的效率。
- 定时任务和后台服务:使用多线程实现定时任务,如定期备份、日志轮转、系统监控等。或者做如邮件发送、日志记录、数据分析等长时间运行的服务,可以独立于主程序线程执行。
- 分布式计算和云计算:在大数据处理中,多线程或多进程可以并行执行Map和Reduce操作,提高数据处理速度。在微服务架构中,每个服务可以独立运行在自己的线程或进程中,提高系统的并发能力和容错性。
写过SpringBoot starter吗
步骤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
Bean的生命周期
图片
- Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
- Bean实例化后对将Bean的引入和值注入到Bean的属性中
- 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
- 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
- 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
- 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
- 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
- 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
- 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
- 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
MySQL
MySQL索引什么时候回表
在MySQL中,回表是指在使用非聚集索引(也称为二级索引或辅助索引)进行查询时,需要额外访问主键索引(聚集索引)以获取完整的行数据的过程。这是因为非聚集索引中通常只存储了索引列的值和指向主键的引用,而不包含完整的行数据。
回表通常在以下情况发生:
- 非覆盖索引查询:当查询语句所请求的数据列不在非聚集索引中,而是需要从主键索引中获取时,就会发生回表。例如,如果查询SELECT * FROM table WHERE column1 = value;,且column1是非聚集索引,那么为了返回所有列,MySQL需要根据column1索引中的主键引用回到主键索引中获取完整的行数据。
- 索引列不完全匹配查询条件:如果查询条件涉及到多个列,而索引仅包含其中的一部分列,那么MySQL也需要回表来获取缺失列的数据。
- ORDER BY 或 LIMIT 子句:即使在一个非聚集索引上进行查询,如果查询中包含了ORDER BY子句或LIMIT子句,并且排序或限制的依据不是索引中的列,也可能导致回表。
MySQL执行update语句后,主键索引、辅助索引、联合索引,数据都是怎么变的
主键索引的变化
主键索引通常是聚集索引,在InnoDB中,数据行实际上是存储在主键索引的B+树结构中的叶子节点上的。这意味着数据行的物理位置是由主键值确定的。当你执行一个更新操作时:
- 如果更新的是非主键列,那么InnoDB会更新主键索引中对应行的非主键列数据,但主键值不变,所以行的物理位置也不会改变。
- 如果更新的是主键列,那么InnoDB需要移动数据行到新的物理位置,因为主键索引决定了数据行的位置。这会导致整个数据行被移动,并且所有的辅助索引中指向该行的指针也需要更新,以指向新的主键值。
辅助索引的变化
辅助索引(也称非聚集索引或二级索引)在InnoDB中包含两部分信息:索引列的值和主键值(或主键的一部分,取决于索引设计)。更新操作对辅助索引的影响如下:
- 更新索引列:如果更新的是辅助索引所包含的列,那么InnoDB会更新该索引中的值。如果更新后的值已经存在于索引中,那么可能会触发索引的重复检查,这在唯一索引中尤为关键。
- 更新主键列:如果更新操作改变了主键的值,那么所有相关的辅助索引中的主键值都需要更新,以指向新的主键值。
联合索引的变化
联合索引是包含多个列的索引。更新操作对联合索引的影响取决于更新的是哪些列:
- 更新索引内的列:如果更新的是联合索引中的任何一列,那么InnoDB会更新该索引条目中的相应值。
- 更新主键列:如果更新的是主键,那么联合索引中指向该行的主键值也需要更新。
MVCC的实现
MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。
- 「读提交」隔离级别是在「每个select语句执行前」都会重新生成一个 Read View;
- 「可重复读」隔离级别是执行第一条select时,生成一个 Read View,然后整个事务期间都在用这个 Read View。
Read View 有四个重要的字段:
图片
- m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。
- min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
- max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
- creator_trx_id :指的是创建该 Read View 的事务的事务 id。
对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
图片
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。
在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:
图片
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
- 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
- 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
- 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 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;
}
}