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

开发 前端
在 JDK 1.7 中它使用的是数组加链表的形式实现的,而数组又分为:大数组 Segment 和小数组 HashEntry。Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。

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的生命周期

图片图片

  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(多版本并发控制)。

项目

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

算法

旋转链表

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;
    }
}

责任编辑:武晓燕 来源: 小林coding
相关推荐

2020-09-03 07:55:02

并发操作系统Java

2018-01-03 10:34:20

创业公司事业

2022-05-27 12:20:18

微信智能手机

2020-07-07 09:19:01

LombokJava IDE

2013-07-17 09:13:19

2021-05-18 06:55:07

客户端HTML容器

2020-04-22 15:00:03

iPhone苹果刘海

2018-05-04 10:45:58

戴尔

2017-10-30 12:00:05

2019-06-27 22:23:56

谷歌Android开发者

2018-10-20 10:30:30

WindowsWindows 10Windows 10.

2022-01-09 23:44:14

5G4G手机

2023-04-20 17:51:41

马斯克微软

2023-01-19 00:13:28

代码技术计划

2016-01-08 10:32:48

虚拟现实头盔

2020-03-13 09:38:45

离职创业感受

2022-11-08 08:29:43

Goslog 库工具

2017-11-20 09:00:43

跳槽职场精英年终奖

2009-12-14 09:38:44

北京移动见习大学生

2012-07-31 09:19:02

程序员
点赞
收藏

51CTO技术栈公众号