重新认识AbstractQueuedSynchronizer

news2024/11/27 4:28:26

开篇之前,烦请诸位允许我附庸风雅一次。近期因诸事繁杂,心情颇低落,遂于喜马拉雅APP中收听《老子》一文。其中的第八十一章《结天道》一文于我感悟颇深:和大怨,必有余怨,报怨以德,焉可以为善?是以执右契(指所有权凭证或借据)而不以责于人。故有德司契,无德司彻。夫天道无亲,恒与善人

看着本篇文章要梳理的AbstractQueuedSynchronizer,我又想起了当年那场不分青红皂白死磕其源码的战争。为了能够彻底弄懂,我不分昼夜地仔细学习,可终是悟性不足,无法习得其要领。或许大家已经觉察道路,我不止一次说过这样的话。其实我有认真反思过这个问题,个人觉得最主要原因是自己的理论知识不够扎实,还有就是思考力有所欠缺。不过常言有云:一艺之成,当尽毕生之力,因此这些并不会促使我放弃目前所从事专业。

1 概述

回过头来看AbstractQueuedSynchronizer类,我们发现它并非一个现成的类,而是java并发包(java.util.conncurrent.locks)中的一个抽象基类,主要用于构建锁或其他同步器它提供了一个框架来实现依赖于共享资源管理的同步器AbstractQueuedSynchronizer通常又被简称为AQS,其核心概念有:

  1. 同步器AQS允许你创建一个同步器,它可以维护一个状态,并且提供方法来获取、释放这个状态。状态通常用来表示某种资源是否被占用
  2. 队列AQS维护了一个等待线程的FIFO队列(CLH锁队列)。当线程试图获取同步状态失败时,会被放入队列中等待。
  3. 独占与共享模式AQS支持两种类型的同步器——独占模式和共享模式。独占模式意味着一次只能有一个线程获得同步状态;而共享模式则允许多个线程同时获得同步状态

AbstractQueuedSynchronizer类,即AQS中的关键方法有以下几个:1) getState():获取当前同步状态;2) setState(int):设置同步状态;3) compareAndSetState(int expect, int update) :CAS操作更新状态。了解到这里,还是先看一下这个类的继承结构,具体如下所示:

从图中可以看出用到AbstractQueuedSynchronizer抽象类的几乎都是我们耳熟能详的工具,譬如LimitLatch、Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock。当然还有Worker,如果对这个工具类不太熟悉,可以看一下《ThreadPoolExecutor详解》这篇文章。另外ReentrantLock和ReentrantReadWriteLock是使用AbstractQueuedSynchronizer实现的独占锁的例子;Semaphore和CountDownLatch是使用AbstractQueuedSynchronizer实现的共享锁的例子。当然除了这些官方工具类外,我们还可以通过继承AQS并重写其中的关键方法(这些关键方法中存在着控制同步状态变化的逻辑)来实现自己的同步器,譬如下面这个例子(这是一个简单的独占锁的案例):

class MyLock extends AbstractQueuedSynchronizer {
    protected boolean tryAcquire(int arg) {
        // 如果状态为0,则尝试获取锁
        if (compareAndSetState(0, 1)) {
            return true;
        }
        return false;
    }
    protected boolean tryRelease(int arg) {
        // 释放锁,将状态设置为0
        setState(0);
        return true;
    }
    public void lock() {
        acquire(1);
    }
    public boolean tryLock() {
        return tryAcquire(1);
    }
    public boolean release() {
        return tryRelease(1);
    }
}

这个案例实现了一个简单的独占锁,也就是说当某个线程获取到这个锁时,其他线程并无资格获取,只能排队等待。

2 关于AQS及其相关类的理解

前一小节介绍过AQS的主要作用是构建锁或其他同步器。关于锁这一点还好理解,上一小节也梳理了使用AQS定义的各种锁,譬如:LimitLatch中的Sync,ReentrantLock中的Sync,ReentrantReadWriteLock中的Sync以及Worker(一个位于ThreadPoolExecutor中的内部类)等等。不过对于这点我还是有些不理解:定义完锁之后要干什么呢?或许只有弄懂AQS的基本原理之后我才会对这个问题有一些自己的想法。相比于这点,同步器这个概念着实让我有点头蒙。还是先来看看AQS的基本原理!(注意:以下内容参考并摘自《【并发基础】AQS(Abstract Queued Synchronizer)框架的使用和实现原理详解》这篇博客) 。上一小节我们提到了AQS的一些核心概念,譬如:同步器、队列等。这一小节我们将围绕这些核心概念展开,以便自己能够更充分的理解AQS,也希望通过这次梳理将前面描述的两个问题给解决掉。

2.1 同步状态

在AQS中维护了一个名为state,且由volatile修饰的整型成员变量。它的主要作用就是表示同步状态。这个变量的值可以用来表示锁是否被持有,或者用于计数等场景(通常情况下当其值大于等于0时表示被占用,小于0表示未被占用)。关于其定义可以参见下述代码:

private volatile int state;
// 下述两个方法是 state 属性的 getter 和 setter 方法
protected final int getState() {
    return state;
}
protected final void setState(int newState) {
    state = newState;
}
// compareAndSetState() 方法主要用于给 state 属性设置值(这里是通过 cas 的方式实现的)
// 注意这里的 STATE 是一个 VarHandle 类型的变量(关于这个类的用法将在后续文章中介绍)
protected final boolean compareAndSetState(int expect, int update) {
    return STATE.compareAndSet(this, expect, update);
}

这里再啰嗦一下,AQS支持两种同步方式:独占式或共享式。另外AQS使用模板模式提供了一些可供子类重写的模板方法(注意:最后一个方法并非模板方法),譬如:

  1. tryAcquire(int):该方法尝试用独占式方式获取同步状态,获取成功返回true,获取失败会返回false
  2. tryRelease(int) :该方法尝试用独占式方式释放同步状态
  3. tryAcquireShared(int) :该方法尝试以共享方式获取同步状态。当返回值大于等于0时表示获取成功,否则表示获取失败
  4. tryReleaseShared(int) :该方法尝试以共享方式释放同步状态
  5. isHeldExclusively():这个方法并非模板方法,不过也是一个非常重要的方法。这个方法会判断当前的状态是否是在独占模式下被线程占用

2.2 等待队列

在2.1小节中,我们一起梳理了AQS的同步状态,通过梳理我对AQS中的同步状态有了更加清晰的认知。本小节我想梳理一下AQS中用到的等待队列,即CLH(Craig-Landin-Hagersten)。

AQS使用CLH(Craig-Landin-Hagersten)锁队列来管理等待获取同步状态的线程。这是一个FIFO(先进先出)的双向链表,其中每个节点代表一个等待的线程。注意:这段描述中有这样一些关键信息——获取同步状态的是线程、CLH队列是一个FIFO(先进先出)的双向链表、这个链表中的每个节点代表一个等待的线程。

首先让我们一起看一下组成FIFO(先进先出)双向队列的节点元素Node。它位于AQS类中,是其中的一个由final修饰的静态内部类,其源码如下所示:

static final class Node {
    /** 一个标记,表示当前节点在共享模式下等待 */
    static final Node SHARED = new Node();
    /** 一个标记,表示当前节点在独占模式下等待 */
    static final Node EXCLUSIVE = null;

/** 实际上下面这些属性主要用于表示当前节点的状态 */
    /** waitStatus值,表示当前线程已经被取消 */
    static final int CANCELLED =  1;
    /** waitStatus值,表示后继线程需要被取消 */
    static final int SIGNAL    = -1;
    /** waitStatus值,表示线程正在等待条件 */
    static final int CONDITION = -2;
    /** waitStatus值,表示下一个acquireShared应无条件传播。 */
    static final int PROPAGATE = -3;

    /**
     * 状态字段,仅接受以下数据:
     *   SIGNAL:    The successor of this node is (or will soon be)
     *               blocked (via park), so the current node must
     *               unpark its successor when it releases or
     *               cancels. To avoid races, acquire methods must
     *               first indicate they need a signal,
     *               then retry the atomic acquire, and then,
     *               on failure, block.
     *   CANCELLED:  This node is cancelled due to timeout or interrupt.
     *               Nodes never leave this state. In particular,
     *               a thread with cancelled node never again blocks.
     *   CONDITION:  This node is currently on a condition queue.
     *               It will not be used as a sync queue node
     *               until transferred, at which time the status
     *               will be set to 0. (Use of this value here has
     *               nothing to do with the other uses of the
     *               field, but simplifies mechanics.)
     *   PROPAGATE:  A releaseShared should be propagated to other
     *               nodes. This is set (for head node only) in
     *               doReleaseShared to ensure propagation
     *               continues, even if other operations have
     *               since intervened.
     *   0:          None of the above
     *
     * The values are arranged numerically to simplify use.
     * Non-negative values mean that a node doesn't need to
     * signal. So, most code doesn't need to check for particular
     * values, just for sign.
     *
     * The field is initialized to 0 for normal sync nodes, and
     * CONDITION for condition nodes.  It is modified using CAS
     * (or when possible, unconditional volatile writes).
     */
    volatile int waitStatus;
    /** 当前节点的前驱节点 */
    volatile Node prev;
    /** 当前节点的后继节点 */
    volatile Node next;
    /** 与当前节点关联的线程 */
    volatile Thread thread;

    /**
     * Link to next node waiting on condition, or the special
     * value SHARED.  Because condition queues are accessed only
     * when holding in exclusive mode, we just need a simple
     * linked queue to hold nodes while they are waiting on
     * conditions. They are then transferred to the queue to
     * re-acquire. And because conditions can only be exclusive,
     * we save a field by using special value to indicate shared
     * mode.
     */
    Node nextWaiter;

    /**
     * Returns true if node is waiting in shared mode.
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * Returns previous node, or throws NullPointerException if null.
     * Use when predecessor cannot be null.  The null check could
     * be elided, but is present to help the VM.
     *
     * @return the predecessor of this node
     */
    final Node predecessor() {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    /** Establishes initial head or SHARED marker. */
    Node() {}

    /** Constructor used by addWaiter. */
    Node(Node nextWaiter) {
        this.nextWaiter = nextWaiter;
        THREAD.set(this, Thread.currentThread());
    }

    /** Constructor used by addConditionWaiter. */
    Node(int waitStatus) {
        WAITSTATUS.set(this, waitStatus);
        THREAD.set(this, Thread.currentThread());
    }

    /** CASes waitStatus field. (采用 CAS 方式设置节点状态) */
    final boolean compareAndSetWaitStatus(int expect, int update) {
        return WAITSTATUS.compareAndSet(this, expect, update);
    }

    /** CASes next field.(采用 CAS 方式为节点设置下一节点) */
    final boolean compareAndSetNext(Node expect, Node update) {
        return NEXT.compareAndSet(this, expect, update);
    }

    final void setPrevRelaxed(Node p) {
        PREV.set(this, p);
    }

    // VarHandle mechanics(下述属性是通过反射方式创建的,后面会有文章对此进行讲解)
    private static final VarHandle NEXT;
    private static final VarHandle PREV;
    private static final VarHandle THREAD;
    private static final VarHandle WAITSTATUS;
    static {
        try {
            MethodHandles.Lookup l = MethodHandles.lookup();
            NEXT = l.findVarHandle(Node.class, "next", Node.class);
            PREV = l.findVarHandle(Node.class, "prev", Node.class);
            THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
            WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
        } catch (ReflectiveOperationException e) {
            throw new ExceptionInInitializerError(e);
        }
    }
}

通过源码可以看出Node节点内部存在一些重要的属性:waitStatus(节点状态)、prev(当前节点的前驱节点)、next(当前节点的后继节点)、thread(与当前节点绑定的处于等待状态的线程)以及nextWaiter(下一节点的等待模式——独占还是共享)。另外Node内部也提供了对这些属性进行操作的函数,比如:compareAndSetWaitStatus(int expect, int update)——用于设置节点状态、compareAndSetNext(Node expect, Node update)——用于设置当前节点的下一节点。

节点元素有了,接下来就是该节点使用方的定义了。在AQS中有这样两个属性,它们的类型均为Node,它们均被volatile和transient关键字修饰,其中一个属性的名字为head,另一个属性的名字为tail。个人理解这就是AQS中CLH队列的头部和尾部。关于CLH队列的结构,可以参见下面这幅图:

从图中可以看出AQS类中的head属性指向一个Node节点,表示双向链表的开始节点。tail属性指向一个Node节点,表示双向链表的结束节点。

通过上面的描述,我们知道使用通过AQS构建的工具,在出现多个线程争抢同一资源时,无法获得资源的线程会被添加到这个双向链表中进行等待。那这个队列的过程究竟是怎样的呢?下面就让我们以ReentrantLock(这个类位于org.apache.tomcat.util.threads包中)为例来研究一下。看下面这个案例:

public class AQSTest {
    ReentrantLock reentrantLock = new ReentrantLock(true);
    private int i = 0;
    private void add() {
        reentrantLock.lock();
        i += 1;
        reentrantLock.unlock();
    }
    public static void main(String[] args) throws Throwable {
        AAAA a = new AAAA();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++)
                    a.add();
            }
        });
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++)
                    a.add();
            }
        });
        t.start();
        t1.start();
        t.join();
        t1.join();
        System.out.println(a.i);
    }
}

通过代码不难发现,如果去掉add()方法中的reentrantLock.lock()和reentrantLock.unlock(),那么main(String[])方法中的执行结果很大概率不是2000(这个在本地有试验过,结果与预期一致,且概率很高)。如果不去掉add()方法中的这两行代码,那么main(String[])方法中的执行结果是2000(无论执行多少次结果都是这个)。出现这种情况的原因是i+=1不是一个原子性的操作,也就是说这个算法应该分为i+1运算,和重新赋值给i这两个步骤(姑且这么理解)。由于是两个线程同时操作i这个数据,这个时候出现线程t拿到i的值进行运算,还未将计算结果重新赋值给i,而线程t1又读取到i的值(注意此时线程t和t1拿到的i的值是一样的)并进行运算的概率很高。这个时候虽然进行了两次运算,但运算的结果实际上都是一样的,也就是说实际上线程t1和t做了一样的工作。如何解决这个问题呢?引起这个问题的根源是同步资源未得到有效的管理。对于资源i的操作应在线程安全的情况下进行,而使用ReentrantLock锁可以完美的解决这个问题,这是因为ReentrantLock可以将对资源i的所有操作序列化,其结果就是同一时刻只能有一个操作这操作共享资源i。那ReentrantLock是如何完成这些操作的?

2.2.1 加锁

本小节我们将来介绍ReentrantLock类的加锁流程,具体可以先看一下ReentrantLock类的lock()方法执行过程中的源码,如下所示:

// ReentrantLock 的 lock() 方法
public void lock() {
    sync.acquire(1);
}
// 注意下面这个方法位于 AQS 类中
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
// 下面这个方法位于 ReentrantLock 类的静态内部类 FairSync 类中
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;
}
// 下面这几个方法位于 AQS 类中
private Node addWaiter(Node mode) {
    Node node = new Node(mode);

    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return node;
            }
        } else {
            initializeSyncQueue();
        }
    }
}
final boolean acquireQueued(final Node node, int arg) {
    boolean interrupted = false;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node))
                interrupted |= parkAndCheckInterrupt();
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        if (interrupted)
            selfInterrupt();
        throw t;
    }
}
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.
         */
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

从代码可以看出,当调用ReentrantLock上的lock()方法后,会将调用转交给ReentrantLock持有的Sync类型的sync对象上的acquire(1)方法。由于Sync类继承了AQS类,所以调用acquire(1)方法其实就是在调AQS中的acquire(int)方法。这个方法的源码在上面源码部分也进行了展示。AQS的acquire(int)方法接着调用模板方法tryAcquire(int),由于这个方法的实现位于实现类中,也就是ReentrantLock类FairSync内部类中的tryAcquire(int)。FairSync中的tryAcquire(int)方法主要做了这样几件事:

  1. 获取当前线程对象,即Thread对象
  2. 调用父类Sync中的getState()方法,实际上调用的就是AQS类中的getState()方法,拿到当前的状态值
  3. 接着判断当前的状态值是否为0。如果是,则调用hasQueuePredecessors()方法,这个方法的主要作用就是判断当前线程是否在线程等待队列中,如果在则返回true,否则返回false。当hasQueuePredecessors()方法返回false,则接下来调用compareAndSetState(int expect, int update)方法将要更新的值(1)设置到state属性中,这里用到了CAS语法。如果设置成功,则调用setExclusiveOwnerThread()方法,将当前线程设置到属性exclusiveOwnerThread(这个属性定义在AbstractOwnableSynchronizer类中)上,然后返回true给调用者,表示加锁成功。如果当前c的值不为0,则判断当前线程是否等于持有共享状态的线程,如果是,则将共享状态加1(个人理解这里就是实现锁重入的核心逻辑),然后调用setState(int)方法,将变更的状态设置到state属性上,然后返回true给调用者,表示加锁成功。如果c既不等于0,且持有状态的线程不能与当前线程则返回false,表示加锁失败

调用完tryAcquire(int)方法后,会得到一个布尔值,如果这个值为false(即加锁失败),则接下来会调用AQS中的addWaiter(Node.EXCLUSIVE)方法。这个方法首先创建一个Node节点,然后通过CAS的方式将Node节点设置到线程等待队列中。这个方法的逻辑是这样的:

  1. 创建Node节点(这个Node节点会包含一个表示当前节点模式的属性:独占还是共享)
  2. 编写一个死循环,直至处理成功才返回。这个死循环中的处理逻辑是这样的:拿到AQS中的尾节点。如果尾节点为空,则调用initializeSyncQueue()(这个方法的主要作用就是初始化AQS中的头尾节点,此时头尾节点同时指向同一个节点,即new Node(),追头节点的设置时通过CAS方式完成的)。如果尾节点不为空,则将当前方法接收的Node节点通过CAS的方式设置为尾节点,同时将原有的尾节点设置为Node节点的前驱节点,当通过CAS方式将Node节点设置为尾节点后,会将原来尾节点的下一节点设置为当前节点Node。注意:这里所有的操作都是线程安全的(通过compareAndSetTail()方法可以看出这里用到了CAS方式来保证操作的线程安全)

调用完addWaitr(Node.EXCLUSIVE)之后,会接着调用acquireQueued()方法,注意这个方法接收addWaiter(Node.EXCLUSIVE)返回的Node对象。这个方法主要做了以下一些操作:

  1. 编写无限循环体,在循环体中做以下逻辑
  2. 取出当前节点的前驱节点
  3. 判断当前节点的前驱节点是否等于head节点(比如跟踪是head节点初始化时头尾指针都指向的节点,而此时当前节点的前驱节点恰好等于这个节点),如果等于接下来就尝试获取锁,如果获取成功,就将head指针指向当前节点,同时将当前节点的thread属性和prev属性重置为null,同时将当前节点的next属性重置为null,接下来返回false给调用者,表示唤醒成功。如果当前节点的前驱节点不等于head指针指向的节点,那么调用shouldParkAfterFailedAcquire(p, node),这里会将当前节点的前驱节点的waitStatus值设置为-1,注意原来是0。接下来来会再次执行循环体,此时依旧跳过第一个if,然后执行第二个if分支,不过此时由于当前节点的前驱节点的waitStatus值已经被设置成了-1,因此这次会走到第二个if分支,然后执行parkAndCheckInterrupt()方法,执行LockSupport.park(this);代码【之后线程就在这里阻塞了】

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

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

相关文章

C++,std::bind 详解

文章目录 1. 概述2. 基本用法2.1 使用占位符2.2 示例 3. 总结 1. 概述 std::bind 是 C11 引入的一个功能&#xff0c;它允许你将函数&#xff08;或成员函数、函数对象&#xff09;与其参数绑定&#xff0c;生成一个新的可调用对象。这个功能在需要将函数及其参数一起传递给其…

DNF攻略:护石符文体系辅助详解,VMOS云手机助攻核心玩法!

在DNF游戏中&#xff0c;护石符文系统是提升角色实力的重要部分。当前版本中&#xff0c;护石符文体系经过了优化&#xff0c;使得获取方式更加便捷。以下是护石符文体系的详细介绍&#xff0c;以及如何使用VMOS云手机来更高效地管理和利用这一系统。 一、护石符文体系简介 护…

HarmonyOS 地图服务:深度解析其丰富功能与精准导航实力

目录 前期准备打造个性化地图&#xff1a;聚焦创建地图功能导入Map Kit相关模块通过MapOptions初始化地图切换地图类型设置地图中心点及层级展示定位按钮展示比例尺指定地图的日间夜间模式 通过MapComponentController对象方法控制地图切换地图类型开启3D建筑图层在指定的持续时…

【安当产品应用案例100集】008-UKEY在工业自动化数据传输中应用

工业自动化中的数据传输是确保生产过程高效、稳定运行的关键环节。工业自动化系统中&#xff0c;一般会有一个远程的客户端&#xff0c;负责将各个传感器、控制器等设备产生的信息传递到服务端&#xff0c;以实现生产过程的自动化控制和监控。它对于提高生产效率、降低生产成本…

SQL Server 2017上服务端设置强制加密启用SSL

在数据库服务端设置&#xff0c;强制所有客户端使用 SSL&#xff0c;设置完后&#xff0c;后续客户端所有连接&#xff0c;都将以密文传送&#xff0c;不论客户端是否指定安全连接&#xff08;即EncryptTrue/False&#xff09; 一、服务端强制加密使用 SSL 1.在数据库服务器上…

C++ 模板进阶知识

目录 一. 非类型模板参数 与类型模板参数确认的区别 何时确认 确认方式 二. 模板的特化 1. 概念 2. 函数模板特化 3. 类模板特化 3.1 全特化 3.2 偏特化 (1). 部分特化 (2). 进一步限制 4. 实际应用 三. 模板分离编译 1. 概念 2. 模板的分离编译 3. 解决方法 四…

【学习笔记】STM32F407探索者HAL库开发(一)STM32F4资源概要

【学习笔记】STM32F407探索者HAL库开发&#xff08;一&#xff09;STM32F4资源概要 1 硬件资源2 STM32命名规则3 STM32数据手册3.1 数据手册各章节内容概要3.2 引脚分布3.3 引脚定义3.4 引脚定义表的具体说明 1 硬件资源 STM32F407ZGT6具体的 内部资源如表 资源数量资源数量内…

鸿蒙Harmony实战开发:Touchscreen驱动器件硬件接口使用实例

功能简介 Touchscreen驱动用于驱动触摸屏使其正常工作&#xff0c;该驱动主要完成如下工作&#xff1a;对触摸屏驱动IC进行上电、配置硬件管脚并初始化其状态、注册中断、配置通信接口&#xff08;I2C或SPI&#xff09;、设定Input相关配置、下载及更新固件等操作。 在HDF&am…

考试:数据库系统(01)

数据库系统 ◆数据&#xff1a;是数据库中存储的基本对象&#xff0c;是描述事物的符号记录。 数据的种类&#xff1a;文本、图形、图像、音频、视频、学生的档案记录、货物的运输 情况等。 ◆数据库DB: 是长期存储在计算机内、有组织的、可共享的大量数据的集合。 ◆数据库…

安恒信息总裁宋端智,辞职了!活捉一枚新鲜出炉的餐饮人!

吉祥知识星球http://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247485367&idx1&sn837891059c360ad60db7e9ac980a3321&chksmc0e47eebf793f7fdb8fcd7eed8ce29160cf79ba303b59858ba3a6660c6dac536774afb2a6330#rd 《网安面试指南》http://mp.weixin.qq.com/s?…

雅菲奥朗 FinOps 认证培训:开启企业云财务管理转型之路

前言&#xff1a; 在当今快速变化的商业环境中&#xff0c;企业面临着前所未有的IT财务挑战。随着云计算和数字化转型的推进&#xff0c;传统的财务管理方式已经不能满足“企业上云”的需求。FinOps&#xff0c;即“云财务管理”应运而生&#xff0c;成为帮助企业实现IT财务流…

Compose TextField详解

首先明确Compose TextField的底层依赖是&#xff1a; TextField BasicTextField CoreTextField 相较于Text&#xff0c;TextField需要提供可输入的能力以及渲染效果动态更新的能力。 // CompositionLocals// If the text field is disabled or read-only, we should not d…

智能菜谱推荐系统_ct3p7

TOC springboot575智能菜谱推荐系统_ct3p7--论文 第一章 概述 1.1 研究背景 近些年&#xff0c;随着中国经济发展&#xff0c;人民的生活质量逐渐提高&#xff0c;对网络的依赖性越来越高&#xff0c;通过网络处理的事务越来越多。随着智能菜谱推荐管理的常态化&#xff0c…

F1 F4 Fn lock 指示灯不亮 联想笔记本 thinkpad

问题描述&#xff1a;F1 F4 Fn lock 指示灯开机的时候亮&#xff0c;但是使用的时候虽然能够发挥正常功能&#xff0c;但是指示灯一直熄灭&#xff0c;指示灯不亮。 电脑型号&#xff1a;联想笔记本 thinkpad E14 Gen 2 。本方案应该适用于所有联想电脑。 解决方法&#xff1a;…

嵌入式和单片机有什么区别?

目录 &#xff08;1&#xff09;什么是嵌入式&#xff1f; &#xff08;2&#xff09;什么是单片机&#xff1f; &#xff08;3&#xff09;嵌入式和单片机的共同点 &#xff08;4&#xff09;嵌入式和单片机的区别 &#xff08;1&#xff09;什么是嵌入式&#xff1f; 关…

掉毛不愁!浮毛怎么去掉比较干净?这宠物空气净化器用上真能解决

这阵子天气热得让人只想宅家&#xff0c;门窗紧闭&#xff0c;空调一开就是一整天。室内凉爽宜人&#xff0c;但一出门再回来&#xff0c;那满屋的浮毛和异味简直让人措手不及&#xff0c;仿佛从天堂跌入地狱。幸好&#xff0c;我家有台宠物空气净化器这位“救星”&#xff0c;…

【Linux】线程控制|POSIX线程库|多线程创建|线程终止|等待|线程分离|线程空间布局

目录 ​编辑 POSIX线程库 多线程创建 独立栈结构 获取线程ID pthread_self 线程终止 return终止线程 pthread_exit pthread_cancel 线程等待 退出码问题 线程分离 测试 线程ID及地址空间布局 ​编辑 POSIX线程库 pthread线程库是 POSIX线程库的一部分&#xf…

MySQL运维学习(1):4种日志

1.错误日志 mysql错误日志记录了mysql发生任何严重错误时的信息&#xff0c;若数据库无法正常使用时&#xff0c;可以先查看错误日志 默认情况下错误日志是开启的&#xff0c;文件名为/var/log/mysqld.log&#xff0c;如果文件不在默认位置&#xff0c;可以通过下面的命令查看…

【Java】Junit的使用

Java系列文章目录 补充内容 Windows通过SSH连接Linux 第一章 Linux基本命令的学习与Linux历史 文章目录 Java系列文章目录一、前言二、学习内容&#xff1a;三、问题描述四、解决方案&#xff1a;4.1 Junit测试方法的使用4.2 测试规范 五、总结 一、前言 学习测试JunitMock后…

LLM如何理解图数据? Graph+LLM综述

对图推理&#xff08;RoG&#xff09;&#xff1a;忠实可解释的大语言模型推理方法&#xff08;ICLR2024&#xff09; https://github.com/RManLuo/reasoning-on-graphs 推理图&#xff08;Reasoning on Graphs, RoG&#xff09;提出了一个计划-检索-推理框架&#xff0c;该…