前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >上难度了!社招三年了,我要跳槽了!

上难度了!社招三年了,我要跳槽了!

作者头像
小林coding
发布2024-07-05 15:08:13
4700
发布2024-07-05 15:08:13
举报
文章被收录于专栏:小林coding

社招的面试难度与校招面试还是有一定的差别,校招因为没有实际的工作项目,会更考察基础内容多一些。

社招因为都是有工作经验,所以会基于项目中涉及到的知识点深入问,考察你对用过的技术栈的深度,而且也会从实际场景操作和源码方面进行提问,难度可谓是上了一个档次!

但是互联网中大厂的社招,算法还是逃不了,还是有手撕的环节,但是一般面试官也不会刁难你,真想要你的话,一般都是出个简单的算法题,走个流程,如果这都不会,那就真没办法了,所以社招跳槽中大厂,算法还是要熟练起来

接下来让我们一起来看看吧,大家看看难度如何?

考察的知识点,我给大家罗列了一下:

  • Java:Hashmap、JUC、Spring
  • MySQL:索引、MVCC
  • 项目:分库分表、热点问题
  • 算法:旋转链表

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代表一个完整的对象,keyThreadLocal本身valueThreadLocal的泛型对象值。

关键源码分析

对照着几段关键源码来看,更容易理解一点哈~我们回到Thread类源码,可以看到成员变量ThreadLocalMap的初始值是为null

代码语言:javascript
复制
public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap的关键源码如下:

代码语言:javascript
复制
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()方法:

代码语言:javascript
复制
 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()方法

代码语言:javascript
复制
    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代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型值。
  • 并发多线程场景下,每个线程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的构造函数中,你可以选择创建公平锁或非公平锁。默认情况下,不带参数的构造函数创建非公平锁。

代码语言:javascript
复制
public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

NonfairSync和FairSync分别扩展了AbstractQueuedSynchronizer,并重写了tryAcquire()方法。

非公平锁(默认)

非公平锁试图在获取锁时立即进行CAS操作,即使已经有其他线程在等待。

这意味着它可能会让新来的线程“插队”,这在理论上是不公平的,但在实践中往往能提供更好的性能,因为减少了线程的等待时间。

在非公平锁中,tryAcquire()方法首先尝试获取锁,而不考虑是否有线程在等待队列中。

代码语言:javascript
复制
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()方法会检查等待队列中是否有线程在等待,如果有,则不会尝试获取锁,除非队列为空或当前线程已经是队列的头部。

代码语言:javascript
复制
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()方法上等待的线程都将被释放。

代码语言:javascript
复制
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支持在屏障点执行一个回调操作,并且在每次所有参与线程都到达屏障点后,它可以被重用。

代码语言:javascript
复制
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内声明的一些属性信息:

代码语言:javascript
复制
//用于保护屏障入口的锁
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类型的属性,具体代码如下:

代码语言:javascript
复制
private static class Generation {
    boolean broken = false;
}

当使用构造方法创建CyclicBarrier实例的时候,就是给上面这些属性赋值,

代码语言:javascript
复制
//创建一个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()方法时,当前线程已经到达屏障点,当前线程阻塞进入休眠状态,

代码语言:javascript
复制
//该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态
//直到所有线程都到达屏障点,当前线程才会被唤醒
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()的实现。

代码语言:javascript
复制
//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简单使用样例

代码语言:javascript
复制
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");
}

结果输出:

代码语言:javascript
复制
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实例。

源码部分

代码语言:javascript
复制
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和一些必要的依赖。例如:

代码语言:javascript
复制
<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中添加自动配置的元数据。例如:

代码语言:javascript
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.example.starter.MyAutoConfiguration

然后,创建MyAutoConfiguration类,该类需要@Configuration和@EnableConfigurationProperties注解。@EnableConfigurationProperties用于启用你定义的配置属性类。

代码语言:javascript
复制
@Configuration
@EnableConfigurationProperties(MyProperties.class)
public class MyAutoConfiguration {

    @Autowired
    private MyProperties properties;

    @Bean
    public MyService myService() {
        return new MyServiceImpl(properties);
    }
}

步骤3: 创建配置属性类

创建一个配置属性类,使用@ConfigurationProperties注解来绑定配置文件中的属性。

代码语言:javascript
复制
@ConfigurationProperties(prefix = "my")
public class MyProperties {
    private String name;
    // getters and setters
}

步骤4: 创建服务和控制器

创建一个服务类和服务实现类,以及一个控制器来展示你的starter的功能。

代码语言:javascript
复制
@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中配置你的属性。

代码语言:javascript
复制
my:
    name: Hello World

Bean的生命周期

  1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
  2. Bean实例化后对将Bean的引入和值注入到Bean的属性中
  3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
  7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
  8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
  9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  10. 如果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(多版本并发控制)。

项目

  • 分库分表是怎么做的,热点问题怎么解决

算法

旋转链表

代码语言:javascript
复制
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;
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-07-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小林coding 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java
    • HashMap底层实现原理
      • 介绍一下ConcurrentHashMap;
        • ThreadLocal的key是什么
          • Reentranlock,公平锁/非公平锁的实现
            • synchronize锁升级过程
              • 多个任务,同时达到临界点,主线程执行,怎么实现
                • CyclicBarrier的实现
                  • CountdownLatch和CyclicBarrier,哪个可以复用,为什么
                    • 多线程在实际应用的场景
                      • 写过SpringBoot starter吗
                        • Bean的生命周期
                        • MySQL
                          • MySQL索引什么时候回表
                            • MySQL执行update语句后,主键索引、辅助索引、联合索引,数据都是怎么变的
                              • MVCC的实现
                              • 项目
                              • 算法
                                • 旋转链表
                                相关产品与服务
                                云数据库 MySQL
                                腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档