并发编程-05AQS原理

news2024/11/25 7:15:06

并发编程-深入理解AQS之ReentrantLock

在这里插入图片描述

一 认识AQS

在讲解AQS原理以及相关同步器之前,我们需要对AQS有一些基本的认识,了解下它有什么样的机制,这样追踪源码的时候就不会太过于迷茫!

1.1 什么是AQS

java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于 AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。

image-20230706123223141

JDK中提供的大多数的同步器如Lock, Latch, Barrier等都是基于AQS框架来实现的

一般实现套路为:

  • 一般是通过一个内部类Sync继承 AQS
  • 将AQS方法的调用都映射到Sync对应的方法

1.2 AQS的特性

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

1.3 AQS信号灯

AQS内部维护了一个变量state来维持加锁和解锁,表示资源的可用状态,它是volatile修饰的

state有三种访问方式

  • getState()
  • setState()
  • compareAndSetState()

1.4 AQS对共享资源的访问方式

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock
  • Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch

1.5 AQS中的两种等待队列

  • 同步等待队列: 主要用于维护获取锁失败时入队的线程。

    AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。

    AQS 依赖CLH同步队列来完成同步状态的管理:

    • 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程。
    • 当锁释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
    • 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同步队列)。
  • 条件等待队列:

    AQS中条件队列是使用单向列表保存的,用nextWaiter来连接:

    • 调用await方法阻塞线程
    • 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条件队列)

1.7 Condition接口

image-20230707090159881

  1. 调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
  2. 调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用Condition#await方法而阻塞的线程。

1.6 AQS队列节点状态

AQS 定义了5个队列中节点状态:

  • 值为0,初始化状态,表示当前节点在sync队列中,等待着获取锁。
  • CANCELLED,值为1,表示当前的线程被取消;
  • SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark;
  • CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  • PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行

1.7 自定义同步器

不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

了解了一下AQS的基础知识后,现在应该脑子里还是很懵懵的,不知道这些东西到底是怎么结合起来来实现线程同步的,接下来通过ReentrantLock这个同步锁来分析下它是怎么基于AQS实现同步的。

二 ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。

相对于 synchronized, ReentrantLock具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量
  • 支持可重入

这里先总结下ReentrantLock和synchronized的区别

  • synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
  • synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过ReentrantLock#isLocked判断;
  • synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
  • synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以被中断的;
  • 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally块中显式释放锁;
  • ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
  • synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;

2.1 ReentrantLock的基本使用

1.非公平锁

ReentrantLock lock = new ReentrantLock();

ReentrantLock lock = new ReentrantLock(false);

public class NoFairLockDemo {


    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();

        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "t" + i).start();
        }
        // 1s 之后去争抢锁
        Thread.sleep(1000);

        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " running...");
                } finally {
                    lock.unlock();
                }
            }, "强行插入" + i).start();
        }

    }
}

运行结果:

image-20230707124402662

第一个循环的500个线程是排队执行的,但是第二个循环中的线程却可以抢占到锁,说明后来的线程是可以不用排队直接尝试去竞争锁的。

2. 公平锁

只需要把参数设置为true即可

ReentrantLock lock = new ReentrantLock(true);

image-20230707170136189

可以看到,第二个循环里的线程是等到第一个循环里的线程都释放锁后才开始排队执行。

3.可重入

public class ReentrantLockDemo1 {


    static ReentrantLock  lock = new ReentrantLock();
    public static void main(String[] args) {

        new Thread(()->{
            lock.lock();
            try {
                System.out.println("第一次获得锁");
                method2();
            }finally {
                lock.unlock();
            }
        }).start();

    }

    public static void method2(){
        lock.lock();
        try {
            System.out.println("第二次获得锁");
        }finally {
            lock.unlock();
        }
    }

}

运行结果:

image-20230707170936187

4.可打断

public class ReentrantLockDemo2 {



    public static void main(String[] args) {

        ReentrantLock  lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {

            System.out.println("线程1启动");

            try {
                lock.lockInterruptibly();
                try {
                    System.out.println("线程1获取锁");
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("线程1等待锁被打断");
            }

        }, "t1");


        lock.lock();
        try {
            System.out.println("main线程获取线程");
            t1.start();
            //先让线程t1执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.interrupt();
            System.out.println("打断线程1");
        } finally {
            lock.unlock();
        }

    }
   
}

运行结果:

image-20230707171850621

5.锁超时

public class ReentrantLockDemo3 {



    public static void main(String[] args) throws InterruptedException{

        ReentrantLock  lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {


            // 注意: 即使是设置的公平锁,此方法也会立即返回获取锁成功或失败,公平策略不生效
            try {
                if (!lock.tryLock(1000,TimeUnit.MILLISECONDS)) {
                    System.out.println("线程1等待超时");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }, "线程1");

        lock.lock();
        try {
            System.out.println("main线程获取锁成功");
            t1.start();
            //先让线程t1执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }

    }  
}

6.条件变量

@Slf4j
public class ReentrantLockDemo4 {

    private static Lock lock = new ReentrantLock();
    //是否有烟
    private static boolean hasCig = false;
    //外卖是否送到
    private static boolean hasTake = false;


    private static Condition cigCon  = lock.newCondition();

    private static Condition takeCon  = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {


        ReentrantLockDemo4 test = new ReentrantLockDemo4();
        new Thread(() ->{
            test.cigratee();
        }).start();


        new Thread(() ->{
            lock.lock();
            try {
                hasCig = true;
                //唤醒送烟的等待线程
                cigCon.signal();
            }finally {
                lock.unlock();
            }


        },"t1").start();


        Thread.sleep(300L);
        new Thread(() -> {
            test.takeout();
        }).start();

        new Thread(() ->{
            lock.lock();
            try {

                hasTake = true;
                //唤醒送饭的等待线程
                takeCon.signal();
            }finally {
                lock.unlock();
            }
        },"t2").start();

    }

    //送烟
    public void cigratee(){
        lock.lock();
        try {
            while(!hasCig){
                try {
                    log.debug("没有烟,歇一会");
                    cigCon.await();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            log.debug("有烟了,干活");
        }finally {
            lock.unlock();
        }
    }


    //送外卖
    public void takeout(){
        lock.lock();
        try {
            while(!hasTake){
                try {
                    log.debug("没有饭,歇一会");
                    takeCon.await();

                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            log.debug("有饭了,干活");
        }finally {
            lock.unlock();
        }
    }
}

运行结果:

image-20230711123443914

2.2 ReentrantLock的原理

在学习源码的时候,我们需要把握重点去学习,比如ReentrantLock实现同步的原理,加锁和解锁的原理,公平锁和非公平锁的原理,线程入队原理,线程出队原理,可打断原理、条件变量原理。

1.继承关系

image-20230711124342128

可以看到ReentrantLock提供了两个同步器,分别实现公平锁和非公平锁,默认是非公平锁!

2.公平锁和非公平锁创建

这一块我们从ReentrantLock的构造方法来看

无参构造:

    public ReentrantLock() {
        sync = new NonfairSync();
    }

有参构造:

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

从构造方法来看,不传参默认创建非公平锁。

3.加锁逻辑(lock方法)

非公平锁
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
公平锁
        final void lock() {
            acquire(1);
        }

从这一块看出来,非公平锁是来竞争的线程都直接先进行一次cas操作尝试是否能够获取锁,竞争失败才会调用acquire(1)方法,而公平锁是直接进行调用acquire(1)方法,而加锁失败阻塞的逻辑主要在acquire(1)方法中

acquire(1)方法

acquire(1)方式是由AbstractQueuedSynchronizer同步锁实现,代码如下

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

先说明下这几个方法的意思:

tryAcquire(arg):再次尝试竞争锁,

addWaiter(Node.EXCLUSIVE):构建队列

acquireQueued():阻塞线程

AbstractQueuedSynchronizer并没有直接实现tryAcquire方法,而是交给子类自行扩展,对于ReentrantLock来说,它的公平锁和非公平锁实现是一样的,我们看下tryAcquire的具体实现:

        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) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这个方式也是ReentrantLock锁可重入的体现,首先判断信号量是否为0,为0的话再次去进行cas获取锁,不为0的话,判断获取锁的线程是否是自己,是自己的话把信号量加1,并返回true。

假设我们竞争锁失败,返回false,进入第二个判断 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),我们需要先看addWaiter(Node.EXCLUSIVE)这个方法是干嘛的,这里多了一个Node类型,我们看下Node是一个什么样的数据结构

      	//共享模式
	   static final Node SHARED = new Node();
	   	//独占模式
        static final Node EXCLUSIVE = null;
		
        static final int CANCELLED =  1;

        static final int SIGNAL    = -1;

        static final int CONDITION = -2;

        static final int PROPAGATE = -3;

        volatile int waitStatus;
    
        volatile Node prev;

        volatile Node next;
		
        volatile Thread thread;

很明显,Node是一个双向链表的数据结构,接下来我们看下addWaiter这个方法的逻辑

    private Node addWaiter(Node mode) {
        //构建一个Node节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

这里我们先顺着逻辑往下看,第一个竞争锁的线程pred为空,肯定会进入enq(node)方法:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail; //t是尾节点,只有第一次的时候既是头节点也是尾节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

tail仍然为空,通过cas操作,新建一个头节点,这就是并发的精髓了,通过一个死循环,第二次循环的时候tail不为空,进入else逻辑,把当前线程所在的节点的前驱节点指向前边的结点,并把当前线程节点设置为尾结点。(这里通过cas保证线程安全问题),构建完队列,发现我们的线程还没有阻塞,玄机就在acquireQueued方法中

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取前驱节点
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这里假设调用tryAcquire方法竞争锁失败,进入第二个if判断,仍然是两个逻辑判断:

shouldParkAfterFailedAcquire(p, node);

parkAndCheckInterrupt());

先看shouldParkAfterFailedAcquire(p, node)

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这里判断很简单,就是判断当前线程节点的前置节点是否是-1,是-1就返回true,否则,就把前一个节点的状态设置为-1,第二轮循环判断是-1,则返回true,进入parkAndCheckInterrupt())方法

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

这里就进行了线程的阻塞,并返回打断标志,到这里,我们的入队流程就结束了。

4.解锁逻辑(unlock方法)

    public void unlock() {
        sync.release(1);
    }

release方法是AbstractQueuedSynchronizer同步器来实现的,那么出队逻辑也是在AbstractQueuedSynchronizer中实现的

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease是解锁方法,这里是由同步器子类自定义实现

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

这里也是锁可重入的解锁流程,每次减1,直到把state减到0,将线程置为空,然后进入if逻辑中,现在就是出队的逻辑了,主要是unparkSuccessor方法

    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

这里通过unpark直接唤醒的是头节点的下一个线程,那么到这里,我们就要回到入队的acquireQueued方法这里(因为之前的线程是在这个方法阻塞的)

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这里会进入第一个if中,设置当前节点为新的头结点,并清空线程。

5.可打断原理

先看不可打断的park逻辑

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 需要获得锁后, 才能返回打断状态
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //因为interrupt被唤醒, 返回打断状态为 true,但是此时还是会在队列中park
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

再看看可打断的park逻辑

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //抛出异常
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

当其他线程调用interrupt唤醒阻塞的线程的时候,这里直接抛出异常。

6.条件变量原理

t arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;😉 {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//抛出异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}


当其他线程调用interrupt唤醒阻塞的线程的时候,这里直接抛出异常。

### 6.条件变量原理







本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1904166.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java探索之旅】多态:重写、动静态绑定

文章目录 &#x1f4d1;前言一、重写1.1 概念1.2 方法重写的规则1.3 重写和重载的区别1.4 重写的设计原则 二、动静态绑定2.1 静态绑定&#xff1a;2.2 动态绑定&#xff1a; &#x1f324;️全篇总结 &#x1f4d1;前言 在面向对象编程中&#xff0c;重写和动静态绑定是重要的…

如何利用Github Action实现自动Merge PR

我是蚂蚁背大象(Apache EventMesh PMC&Committer)&#xff0c;文章对你有帮助给项目rocketmq-rust star,关注我GitHub:mxsm&#xff0c;文章有不正确的地方请您斧正,创建ISSUE提交PR~谢谢! Emal:mxsmapache.com 1. 引言 GitHub Actions 是 GitHub 提供的一种强大而灵活的自…

VMware虚拟机搭建CentOS7环境

相关资料 安装VMware 双击VMware-workstation(16.1.1软件安装包.exe安装文件,点下一步 激活码文件复制激活码激活安装linux 1、点击创建虚拟机

python等级考试——一级知识点汇总(turtle画图部分)

&#xff08;本篇文章是针对中国电子学会青少年编程等级考试的&#xff0c;适合初学者以及青少年编程学习者&#xff09; 本篇文章主要介绍turtle画图部分&#xff0c;其他一级考试知识点请移步下方链接&#xff1a;python等级考试——一级知识点汇总&#xff08;不包含turtle…

【JAVA多线程】线程池概论

目录 1.概述 2.ThreadPoolExector 2.1.参数 2.2.新任务提交流程 2.3.拒绝策略 2.4.代码示例 1.概述 线程池的核心&#xff1a; 线程池的实现原理是个标准的生产消费者模型&#xff0c;调用方不停向线程池中写数据&#xff0c;线程池中的线程组不停从队列中取任务。 实现…

导航栏样式,盒子模型

1.代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style>li{he…

《机器学习》读书笔记:总结“第4章 决策树”中的概念

&#x1f4a0;决策树 基于树结构进行决策。 一棵决策树包括&#xff1a; 一个 根节点&#xff08;起点&#xff09;若干 叶节点&#xff08;没有下游节点的节点&#xff09;若干 内部节点(分支节点) 即&#xff1a; #mermaid-svg-Mxe3d0kNg29PM2n8 {font-family:"treb…

笔记本电脑内存不够

笔记本电脑内存不够是众多笔记本用户面临的常见问题&#xff0c;尤其是对于一些需要处理大型文件或者运行复杂软件的用户&#xff0c;这个问题可能会严重影响笔记本的使用体验。那么&#xff0c;我们应该如何解决笔记本电脑内存不够的问题呢&#xff1f;本文将从几个方面进行详…

联想电脑更新 Microsoft store 提示 (我们需要更新 Microsoft store 错误码为0x80070422 可在需要时使用)

1. 检查Windows Update服务是否启动 按下WinR组合键打开运行对话框&#xff0c;输入“services.msc”并按回车&#xff0c;打开服务面板。在列表中找到Windows Update服务&#xff0c;双击进入属性。如果服务状态为停止&#xff0c;请点击“启动”按钮&#xff0c;确保启动类型…

缓存-分布式锁-原理和基本使用

分布式锁原理和使用 自旋 public Map<String, List<Catelog2Vo>> getCatalogJsonFromDBWithRedisLock() {Boolean b redisTemplate.opsForValue().setIfAbsent(Lock, Lock, Duration.ofMinutes(1));if (!b) {int i 10;while (i > 0) {Object result redisTe…

上网监控软件有哪些?3款实力出众的上网监控软件

为什么需要上网监控软件&#xff1f; 据说&#xff0c;99%的员工上班都会摸鱼&#xff0c;1%的员工上班会窃取公司信息。 所以&#xff0c;因此&#xff0c;监控员工的上网行为是很有必要滴。 总结下来&#xff0c;上网监控软件的作用是&#xff1a; 1.提高生产力&#xff1…

从零开始读RocketMq源码(一)生产者启动

目录 前言 获取源码 总概论 生产者实例 源码 A-01:设置生产者组名称 A-02:生产者服务启动 B-01&#xff1a;初始化状态 B-02&#xff1a;该方法再次对生产者组名称进行校验 B-03&#xff1a;判断是否为默认生产者组名称 B-04: 该方法是为了实例化MQClientInstance对…

机器学习第四十六周周报 FMP

文章目录 week46 FMP摘要Abstract1. 题目2. Abstract3. FMP3.1 优化框架3.2 优化器 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程 5. 结论6.代码复现1. FMP2. fairGNN小结参考文献 week46 FMP 摘要 本周阅读了题为Chasing Fairness in Graphs: A GNN Architecture Per…

永磁同步电机控制算法--最大转矩电流比控制(虚拟信号注入法)

目前&#xff0c;国内外相关学者对 MTPA 控制方法进行了一系列的理论研究与仿真分析。通过研究取得的成果综合来看&#xff0c;该控制方法主要有&#xff1a;直接公式计算法、曲线拟合法、查表法、搜索法、高频信号注入法以及参数辨识法等。 之前的文章中已经介绍了直接公式计…

ASP.NET Core----基础学习03----开发者异常页面 MVC工作原理及实现

文章目录 1. 开发者异常页面(1)Startup.cs 页面的基础配置(2)自定义显示报错代码的前后XX行 2. MVC 的原理3. MVC 的实现4.默认路由路径5.返回Json字符串 1. 开发者异常页面 (1)Startup.cs 页面的基础配置 namespace ASP.Net_Blank {public class Startup{private readonly IC…

Matlab协方差矩阵分解法生成随机场

Matlab协方差矩阵分解法生成随机场 相关系数矩阵 % function outcohesion(x,y,mu,theta) % end % xyload(F:\Research-OUC\基于机器许学习模型的海底斜坡可靠度研究\基于comsol的斜坡稳定性分析\comsol网格操作\grid_operate-matlab.mphtxt); % xxy(:,1); % yxy(:,2); Xlinspac…

前端JS特效第22集:html5音乐旋律自定义交互特效

html5音乐旋律自定义交互特效&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下(全部代码在文章末尾)&#xff1a; <!DOCTYPE html> <html lang"en" > <head> <meta charset"UTF-8"> <title>ChimeTime™</title…

Spring源码十二:事件发布源码跟踪

上一篇我们在Spring源码十一&#xff1a;事件驱动中&#xff0c;介绍了spring refresh方法的initMessageSource方法与initApplicationEventMulticaster方法&#xff0c;举了一个简单的例子进行简单的使用的Spring为我们提供的事件驱动发布的示例。这一篇我们将继续跟踪源码&…

谷粒商城学习笔记-使用renren-fast-vue框架时安装依赖包遇到的问题及解决策略

文章目录 1&#xff0c;npm error Class extends value undefined is not a constuctor or null2&#xff0c;npm warn cli npm v10.8.1 does not support Node.js v16.20.2.3&#xff0c;npm error code CERT_HAS_EXPIRED学习心得 这篇文章记录下使用renren-fast-vue&#xff…

花所Flower非小号排名20名下载花所Flower

1、Flower花所介绍 Flower花所是一家新兴的数字货币交易平台&#xff0c;致力于为全球用户提供安全、便捷的交易体验。平台以其强大的技术支持和丰富的交易产品闻名&#xff0c;为用户提供多样化的数字资产交易服务&#xff0c;涵盖了主流和新兴数字货币的交易需求。 2. Flowe…