AQS 原理详解

news2024/11/15 10:18:47

日常开发中,我们经常使用锁或者其他同步器来控制并发,那么它们的基础框架是什么呢?如何实现的同步功能呢?本文将详细讲解构建锁和同步器的基础框架--AQS,并根据源码分析其原理。


一、什么是 AQS?

(一) AQS 简介

AQS(Abstract Queued Synchronizer),抽象队列同步器,它是用来构建锁或其他同步器的基础框架。虽然大多数程序员可能永远不会使用到它,但是知道 AQS 的原理有助于理解一些锁或同步器的是如何运行的。

那么有哪些同步器是基于 AQS 实现的呢?这里仅是简单介绍,详情后续会单独总结一篇文章。

同步器

说明

CountDownLatch

递减的计数器,直至所有线程的任务都执行完毕,才继续执行后续任务。

Semaphore

信号量,控制同时访问某个资源的数量。

CyclicBarrier

递增的计数器,所有线程达到屏障时,才会继续执行后续任务。

ReentrantLock

防止多个线程同时访问共享资源,类似 synchronized 关键字。

ReentrantReadWriteLock

维护了读锁和写锁,读锁允许多线程访问,读锁阻塞所有线程。

Condition

提供类似 Object 监视器的方法,于 Lock 配合可以实现等待/通知模式。

FutureTask

当一个线程需要等待另一线程把某个任务执行完后才能继续执行,此时可以使用 FutureTask

如果你理解了 AQS 的原理,也可以基于它去自定义一个同步组件。

(二) AQS 数据结构

AQS 核心是通过对同步状态的管理,来完成线程同步,底层是依赖一个双端队列来完成同步状态的管理

  • 当前线程获取同步状态失败后,会构造成一个 Node 节点并加入队列末尾,同实阻塞线程。
  • 当同步状态释放时,会把头节点中的线程唤醒,让其再次尝试获取同步状态

如下图,这里只是简单绘制,具体流程见下面原理分析:

这里的每个 Node 节点都存储着当前线程、等待信息等。

(三) 资源共享模式

我们在获取共享资源时,有两种模式:

模式

说明

示例

独占模式

Exclusive,资源同一时刻只能被一个线程获取

ReentrantLock

共享模式

Share,资源可同时被多个线程获取

Semaphore、CountDownLatch

二、AQS 原理分析

先简单说下原理分析的流程:

  1. 同步状态相关源码;
  2. 须重写的方法;
  3. Node 节点结构分析;
  4. 独占模式下的同步状态的获取与释放;
  5. 共享模式下的同步状态的获取与释放;

(一) 同步状态相关

上面介绍到, AQS 核心是通过对同步状态的管理,来完成线程同步,所以首先介绍管理同步状态的三个方法,在自定义同步组件时,需要通过它们获取和修改同步状态。

//保证可见性
private volatile int state

//获取当前同步状态。
protected final int getState() {
    return state;
}

//设置当前同步状态。
protected final void setState(int newState) {
    state = newState;
}

//使用 CAS 设置当前状态,保证原子性。
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

(二) 须重写的方法

AQS 是基于模板方法模式的,通过第一个 abstract 也可知道,AQS 是个抽象类,使用者需要继承 AQS 并重写指定方法。

以下这些方式是没有具体实现的,需要在使用 AQS 时在子类中去实现具体方法,等到介绍一些同步组件时,会详细说明如何重写。

//独占式获取同步状态,实现该方法须查询并判断当前状态是否符合预期,然后再进行CAS设置状态。
protected boolean tryAcquire (int arg) 

//独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
protected boolean tryRelease (int arg) 

//共享式获取同步状态,返回大于0的值表示获取成功,反之获取失败。
protected int tryAcquireShared (int arg)

//共享式释放同步状态。
protected boolean tryReleaseShared (int arg)

//当前同步器是否再独占模式下被线程占用,一般用来表示是否被当前线程独占。
protected boolean isHeldExclusively ()

(三) Node 源码

Node 是双端队列中的节点,是数据结构的重要部分,线程相关的信息都存在每一个 Node 中。

1. Node 结构源码

源码如下:

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的值,表示当前节点在等待某个条件,正处于condition等待队列中
    static final int CONDITION = -2;
    
    //waitStatus的值,表示在当前有资源可用,能够执行后续的acquireShared操作
    static final int PROPAGATE = -3;

    //等待状态,值如上,1、-1、-2、-3。
    volatile int waitStatus;
    
    //前趋节点
    volatile Node prev;

    //后继节点
    volatile Node next;
    
    //当前线程
    volatile Thread thread;
    
    //等待队列中的后继节点,共享模式下值为SHARED常量
    Node nextWaiter;
    
    //判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
    //返回前趋节点,没有报NPE
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    //下面是三个构造方法
    Node() {}    // Used to establish initial head or SHARED marke
    
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

2. 设置头尾节点

Unsafe 类中,提供了一个基于 CAS 的设置头尾节点的方法,AQS 调用该方法进行设置头尾节点,保证并发编程中的线程安全。

//CAS自旋设置头节点
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}


//CAS自旋设置尾节点,expect为当前线程“认为”的尾节点,update为当前节点
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
    

(四) 独占模式

资源同一时刻只能被一个线程获取,如 ReentrantLock。

1. 获取同步状态

代码如下,调用 acquire 方法可以获取同步状态,底层就是调用须重写方法中的 tryAcquire。如果获取失败则进入同步队列中,即使后续对线程进行终端操作,线程也不会从同步队列中移除。

public final void acquire(int arg) {
    //调用须重写方法中的tryAcquire
    if (!tryAcquire(arg) &&
        //失败则进入同步队列中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

获取失败会先调用 addWaiter 方法将当前线程封装成独占式模式的节点,添加到AQS的队列尾部,源码如下。

private Node addWaiter(Node mode) {
    //将当前线程封装成对应模式下的Node节点
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;//尾节点
    if (pred != null) {
        //双端队列需要两个指针指向
        node.prev = pred;
        //通过CAS方式
        if (compareAndSetTail(pred, node)) {
            //添加到队列尾部
            pred.next = node;
            return node;
        }
    }
    //等待队列中没有节点,或者添加队列尾部失败则调用end方法
    enq(node);
    return node;
}

//Node节点通过CAS自旋的方式被添加到队列尾部,直到添加成功为止。
private Node enq(final Node node) {
    //死循环,类似 while(1)
    for (;;) {
        Node t = tail;
        if (t == null) { // 须要初始化,代表队列的第一个元素
            if (compareAndSetHead(new Node()))
                //头节点就是尾节点
                tail = head;
        } else {
            //双端队列需要两个指针指向
            node.prev = t;
            //通过自旋放入队列尾部
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

此时,通过 addWaiter 已经将当前线程封装成独占模式的 Node 节点,并成功放入队列尾部。接下来会调用acquireQueued 方法在等待队列中排队。

final boolean acquireQueued(final Node node, int arg) {
    //获取资源失败标识
    boolean failed = true;
    try {
        //线程是否被中断标识
        boolean interrupted = false;
        //死循环,类似 while(1)
        for (;;) {
            //获取当前节点的前趋节点
            final Node p = node.predecessor();

            //前趋节点是head,即队列的第二个节点,可以尝试获取资源
            if (p == head && tryAcquire(arg)) {
                //资源获取成功将当前节点设置为头节点
                setHead(node);
                p.next = null; // help GC,表示head节点出队列
                failed = false;
                return interrupted;
            }
            //判断当前线程是否可以进入waitting状态,详解见下方
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())	//阻塞当前线程,详解见下方
                interrupted = true;
        }
    } finally {
        if (failed)
            //取消获取同步状态,源码见下方的取消获取同步状态章节
            cancelAcquire(node);
    }
}

//将当前节点设置为头节点
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

//判断当前线程是否可以进入waitting状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前趋节点的等待状态,含义见上方Node结构源码
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)	//表示当前节点的线程需要被唤醒
        return true;
    if (ws > 0) {	//表示当前节点的线程被取消

        //则当前节点一直向前移动,直到找到一个waitStatus状态小于或等于0的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        //排在这个节点的后面
        pred.next = node;
    } else {
        //通过CAS设置等待状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

//阻塞当前线程
private final boolean parkAndCheckInterrupt() {
    //底层调用的UnSafe类的方法 park:阻塞当前线程, unpark:使给定的线程停止阻塞
    LockSupport.park(this);
    //中断线程
    return Thread.interrupted();
}

acquireQueued 方法中,只有当前驱节点等于 head 节点时,才能够尝试获取同步状态,这时为什么呢?

因为 head 节点是占有资源的节点,它释放后才会唤醒它的后继节点,所以需要检测。还有一个原因是因为如果遇到了非 head 节点的其他节点出队或因中断而从等待中唤醒,这时种情况则需要判断前趋节点是否为 head 节点,是才允许获取同步状态。

获取同步状态的整体流程图如下:

2. 释放同步状态

调用须重写方法中的 tryAcquire 进行同步状态的释放,成功则唤醒队列中最前面的线程,具体如下。

public final boolean release(int arg) {
    //调用须重写方法中的tryRelease
    if (tryRelease(arg)) {
        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)
        //小于0则,则尝试CAS设为0
        compareAndSetWaitStatus(node, ws, 0);

    //获取后继节点
    Node s = node.next;

    //后继节点为空或者等待状态大于0,代表被节点被取消
    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)
        //底层调用的UnSafe类的方法 park:阻塞当前线程, unpark:使给定的线程停止阻塞
        LockSupport.unpark(s.thread);
}

3. 其他情况的获取同步状态

除此之外,独占模式下 AQS 还提供了两个获取同步状态的方法,可中断的获取同步状态和超时获取同步状态。

acquire 方法获取锁失败的线程是不能被 interrupt 方法中断的,所以提供了另一个方法 ,从而让获取锁失败等待的线程可以被中断。

底层源码如下:

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())//中断则抛出异常
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

通过调用 tryAcquireNanos 可以在超时时间内获取同步状态,可以理解为是上述中断获取同步状态的增强版。


public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())//中断则抛出异常
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

上面个两个方法的源码均与普通的独占获取同步状态的源码基本类似,感兴趣的话可以自行阅读,这里不做赘述。

(五) 共享模式

资源可同时被多个线程获取,如 Semaphore、CountDownLatch。

1. 获取同步状态

代码如下,调用 acquireShared 方法可以获取同步状态,底层就是先调用须重写方法中的 tryAcquireShared。

tryAcquireShared 返回值的含义

  • 负数:表示获取资源失败
  • 0:表示获取资源成功,但是没有剩余资源
  • 正数:表示获取资源成功,还有剩余资源

public final void acquireShared(int arg) {
    //调用须重写方法中的tryAcquireShared
    if (tryAcquireShared(arg) < 0)
        //获取资源失败,将当前线程放入队列的尾部并阻塞
        doAcquireShared(arg);
}

若获取资源失败,调用如下方法将当前线程放入队列的尾部并阻塞,直到有其他线程释放资源并唤醒当前线程。

//部分方法与独占模式下的方法公用,这里不再重复说明,详情见独占模式下的获取同步状态源码。
private void doAcquireShared(int arg) {
    //将当前线程封装成独占式模式的节点,添加到AQS的队列尾部,源码在独占模式中已分析。
    final Node node = addWaiter(Node.SHARED);

    //获取资源失败标识
    boolean failed = true;
    try {
        //线程被打断表示
        boolean interrupted = false;
        
        //死循环,类似 while(1)
        for (;;) {
            //获取当前节点的前趋节点
            final Node p = node.predecessor();
            //前趋节点是head,即队列的第二个节点,可以尝试获取资源
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //将当前节点设置为头节点,若还有剩余资源,则继续唤醒队列中后面的线程。
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC 表示head节点出队列
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //判断当前线程是否可以进入waitting状态,源码在独占模式中已分析。
            if (shouldParkAfterFailedAcquire(p, node) &&
                //阻塞当前线程,源码在独占模式中已分析。
                parkAndCheckInterrupt()) 
                interrupted = true;
        }
    } finally {
        if (failed)
            //取消获取同步状态,源码见下方的取消获取同步状态章节
            cancelAcquire(node);
    }
}

/*
 * propagate就是tryAcquireShared的返回值
 *	● 负数:表示获取资源失败
 *	● 0:表示获取资源成功,但是没有剩余资源
 *	● 正数:表示获取资源成功,还有剩余资源
 */
private void setHeadAndPropagate(Node node, int propagate) {
    //将当前节点设置为头节点,源码在独占模式中已分析。
    Node h = head; //这时的h是旧的head
    setHead(node);

    // propagate > 0:还有剩余资源
    // h == null 和 h = head) == null: 不会成立,因为addWaiter已执行
    // waitStatus < 0:若没有剩余资源,但waitStatus又小于0,表示可能有新资源释放
    // 括号中的 waitStatus < 0: 这里的 h 是此时的新的head(当前节点),
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        
        //获取当前节点的后继节点
        Node s = node.next;

        //后继节点不存在或者是共享锁都需要唤醒,可理解为只要后继节点不是独占模式,都要唤醒
        //可能会导致不必要的唤醒
        if (s == null || s.isShared())
            //唤醒操作在此方法中,详情见下方的释放源码
            doReleaseShared();
    }
}

2. 释放同步状态

代码如下,调用 releaseShared 方法可以释放同步状态,底层就是先调用须重写方法中的 tryReleaseShared。

public final boolean releaseShared(int arg) {
    //调用须重写方法中的tryReleaseShared
    if (tryReleaseShared(arg)) {
        //尝试释放资源成功,会继续唤醒队列中后面的线程。
        doReleaseShared();
        return true;
    }
    return false;
}

//唤醒队列中后面的线程
private void doReleaseShared() {

    //死循环,自旋操作
    for (;;) {
        //获取头节点
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //signal表示后继节点需要被唤醒
            if (ws == Node.SIGNAL) {
                //自旋将头节点的waitStatus状态设置为0
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                
                //唤醒头节点的后继节点,源码见独占模式的释放
                unparkSuccessor(h);
            }
            //后继节点不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        //判断头节点是否变化,没有则退出循环。
        //有变化说明其他线程已经获取了同步状态,需要进行重试操作。
        if (h == head)                   // loop if head changed
            break;
    }
}

(六) 取消获取同步状态

无论是独占模式还是共享模式,所有的程获取同步状态的过程中,如果发生异常或是超时唤醒等,都需要将当前的节点出队,源码如下。

//一般在获取同步状态方法的finally块中
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;		//当前线程节点设为null
    Node pred = node.prev;		//获取前驱节点

    //前趋节点为取消状态,向前遍历找到非取消状态的节点
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    Node predNext = pred.next;	//获取非取消节点的下一个节点

    node.waitStatus = Node.CANCELLED;	//将当前节点的等待状态设为取消状态

    //当前节点是尾节点,则自旋将尾节点设置为前一个非取消节点
    if (node == tail && compareAndSetTail(node, pred)) {
        //将尾节点设为前一个非取消的节点,并将其后继节点设为null,help GC
        compareAndSetNext(pred, predNext, null);
    } else {
     
        int ws;//用于表示等待状态

        //pred != head:前一个非取消的节点非头节点也非尾节点
        //ws == Node.SIGNAL:当前等待状态为待唤醒
        //若不是待唤醒则CAS设置为待唤醒状态
        //前一个非取消的节点的线程不为null
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            //符合所有条件后,获取当前节点的后继节点
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                //前一个非取消的节点的后继节点设为当前节点的后继节点
                //这样当前节点以及之前的已取消节点都会被移除
                compareAndSetNext(pred, predNext, next);
        } else {
            //前一个非取消的节点为头节点
            //唤醒后继节点的线程,详情见独占模式释放同步状态的源码
            //唤醒是为了执行shouldParkAfterFailedAcquire()方法,详解见上面的acquireQueued源码
            //该方法中从后往前遍历找到第一个非取消的节点并将中间的移除队列
            unparkSuccessor(node);
        }
        //移除当前节点
        node.next = node; // help GC
    }
}

三、总结

AQS 是用来构建锁或其他同步器的基础框架,底层是一个双端队列。支持独占和共享两种模式下的资源获取与释放,基于 AQS 可以自定义不同类型的同步组件。

在独占模式下,获取同步状态时,AQS 维护了一个双端队列,获取失败的线程都会被加入到队列中进行自旋,移出队列的条件就是前趋节点为 head 节点并成功获取同步状态。释放同步状态时,会唤醒 head 节点的后继节点。

在共享模式下,获取同步状态时,同样维护了一个双端队列,获取失败的的线程也会加入到队列中进行自旋,移除队列的条件也与独占模式一样。

但是在唤醒操作上,在资源数量足够的情况下,共享模式会将唤醒事件传递到后面的共享节点上,进行了后续节点的唤醒。

关于 AQS 重要的几个组件的特点、原理以及对应的应用场景,后续会单独写一篇文章。若发现其他问题欢迎指正交流。


参考:

[1] 翟陆续/薛宾田. Java 并发编程之美.

[2] 方腾飞/魏鹏/程晓明. Java 并发编程的艺术.

[3] Lev Vygotsky. Java 并发编程实践

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

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

相关文章

Oracle+ASM+High冗余详解及空间计算

Oracle ASM&#xff08;Automatic Storage Management&#xff09;的High冗余模式是一种提供高度数据保护的策略&#xff0c;它通过创建多个数据副本来确保数据的可用性和安全性。 以下是关于Oracle ASM High冗余的详细解释&#xff1a; 一、High冗余的特点 1.数据冗余度 在Hi…

Java | Leetcode Java题解之第342题4的幂

题目: 题解&#xff1a; class Solution {public boolean isPowerOfFour(int n) {return n > 0 && (n & (n - 1)) 0 && n % 3 1;} }

【Datawhale AI夏令营第四期】 魔搭-大模型应用开发方向笔记 Task03 大咖项目分享 人话八股文Bakwaan_Buddy项目开发尝试

【Datawhale AI夏令营第四期】 魔搭-大模型应用开发方向笔记 Task03 人话八股文Bakwaan_Buddy项目开发尝试 Idea: 我们草台班子目前的想法是解决大家计算机学院毕业面临的BUG——不爱背、背不下来八股文&#xff0c;觉得枯燥、烦、工作了用不着&#xff0c;反正就是知识他不进…

Python酷库之旅-第三方库Pandas(085)

目录 一、用法精讲 356、pandas.Series.str.isnumeric方法 356-1、语法 356-2、参数 356-3、功能 356-4、返回值 356-5、说明 356-6、用法 356-6-1、数据准备 356-6-2、代码示例 356-6-3、结果输出 357、pandas.Series.str.isdecimal方法 357-1、语法 357-2、参数…

RabbitMQ的核心概念

RabbitMQ是一个消息中间件&#xff0c;也是一个生产者消费者模型&#xff0c;负责接收&#xff0c;存储和转发消息。 核心概念 Producer 生产者&#xff0c;是RabbitMQ Server的客户端&#xff0c;向RabbitMQ发送消息。 Consumer 消费者&#xff0c;是RabbitMQ Server的客…

Ps:首选项 - 单位与标尺

Ps菜单&#xff1a;编辑/首选项 Edit/Preferences 快捷键&#xff1a;Ctrl K Photoshop 首选项中的“单位与标尺” Units & Rulers选项卡允许用户根据工作需求定制 Photoshop 的测量单位和标尺显示方式。这对于保持工作的一致性和精确性&#xff0c;尤其是在跨设备或跨平台…

mybatis plus 查询部分源码分析,typehandler怎么实现的?FastjsonTypehandler 查询问题怎么解决?

我们在使用mysql的json字段的时候有时为了方便&#xff0c;最好是查询的时候直接反序列化为对象比较好&#xff0c;这时候我们就用到了typehandler这个属性 首先mybatis plus 会初始化一系列的 typeHandler,并且扫描用户设置的typeHandler路径&#xff08;mybatis-plus: type-…

Flutter-->AAPT: error: resource android:attr/lStar not found.

更新Flutter 3.24.0之后, 打包出现AAPT: error: resource android:attr/lStar not found.问题, 这里出一个我的解决方案. 更新Flutter 3.24.0之后, Android编译sdk需要使用34, 否则就会出现很多问题… 由于很多库都不可能及时更新适配到Android sdk 34, 所以可以等pub get将子…

硅谷物理服务器有哪些关键优势和特点

硅谷的物理服务器设施全球知名&#xff0c;为各类企业提供了卓越的IT基础设施支持。下面将逐一探讨硅谷物理服务器的关键优势和特点&#xff0c;rak小编为您整理发布硅谷物理服务器有哪些关键优势和特点。 1. 卓越的性能 高性能计算能力&#xff1a;硅谷的物理服务器采用最新一…

Authentik:开源身份提供商

Authentik 是一个开源身份提供商&#xff0c;旨在实现最大的灵活性和适应性。 它可轻松集成到现有环境中并支持新协议。 它是一个全面的解决方案&#xff0c;用于在您的应用程序中实现注册、帐户恢复等功能&#xff0c;无需手动管理这些任务。 Authentik 可以无缝集成到现有…

arcgis打开不同tif格式编码的栅格数据

1、如下图&#xff0c;将文件包包解压打开&#xff0c;看到【2020年GDP数据】。 2、点击进入【2020年GDP数据】文件夹如下图所示。接着去打开arcgis软件。 3、按照步骤来&#xff0c;在arcgis【目录】里面添加【文件夹】然后选中你刚刚解压的【GDP文件夹数据】&#xff0c;最…

21 注意力机制—自注意力

目录 1.自注意力和位置编码跟CNN,RNN对比位置编码(position encoding)1、和 CNN / RNN 不同,自注意力并没有记录位置信息2、为了使用序列的顺序信息,通过在输入表示中添加位置编码将位置信息注入到输入里3、P 的元素具体计算如下:位置编码矩阵绝对位置信息相对位置信息总…

Linux运维篇-yum命令报错 /lib64/libcurl.so.4相关

目录 项目场景&#xff1a;问题描述原因分析&#xff1a;解决方案&#xff1a; 项目场景&#xff1a; centos7&#xff0c;8&#xff0c;同样也适用openEuer&#xff0c;Kylin等redhat系的国产化操作系统 问题描述 在使用yum命令时报错&#xff1a; 主要报错信息为&#xff1…

诈骗未成功是否构成犯罪?

诈骗未成功不一定构成犯罪。在刑法上&#xff0c;构成诈骗罪需要满足特定的构成要件&#xff0c;包括有非法占有的目的、实施了虚构事实或隐瞒真相的行为、对方因此陷入错误认识并处分财产、行为人或第三方取得财产、被害人遭受财产损失。如果诈骗行为未能成功&#xff0c;即被…

[C#]基于winform结合photocartoon算法实现人物卡通化源码实现

【官方框架】 https://github.com/minivision-ai/photo2cartoon 简介 人像卡通风格渲染的目标是&#xff0c;在保持原图像ID信息和纹理细节的同时&#xff0c;将真实照片转换为卡通风格的非真实感图像。我们的思路是&#xff0c;从大量照片/卡通数据中习得照片到卡通画的映射…

HDRP管线下的开放世界游戏与跨平台优化,《仙剑世界》万字分享

《仙剑世界》作为仙剑 IP 系列的最新⻓篇⼒作&#xff0c;从故事和剧情上延续了仙剑的精髓。在仙剑 33 年的世界观下&#xff0c;《仙剑世界》打造出了⼀个由浪漫唯美的江南全景、磅礴恢弘的蜀⼭、神秘苗疆等区域构成的 384 平⽅公⾥完整的⽆缝开放⼤世界。以东⽅题材为起点&am…

Java入门-接口:JDK8开始接口新增方法,接口的多继承,接口注意事项

&#xff08;一&#xff09;新增接口注意事项&#xff1a; 接口A&#xff1a; package interface_jdk8;public interface A {//1.新增默认方法&#xff1a;必须使用defalut修饰&#xff0c;默认会被public修饰//注意&#xff1a;这种默认方法可以带方法体/*实例方法&#xff1…

openssh升级到9.8

升级步骤 1、查看版本 [rootlocalhost openssh-8.8p1]# ssh -V OpenSSH_8.8p1, OpenSSL 1.0.2k-fips 26 Jan 20172、下载安装包 cd /usr/local/src wget https://www.zlib.net/zlib-1.3.1.tar.gz wget https://www.openssl.org/source/openssl-3.2.1.tar.gz wget https://cdn.…

在小程序添加公司官网访问

在小程序添加公司官网访问 有时候由于业务需要&#xff0c;在小程序上加入自己公司官网的访问地址&#xff0c;点击后跳转到官网。 本文详细讲解整个过程。 一、小程序管理台配置 进入小程序管理台 开发管理-》业务域名 加入你的公司官网域名&#xff0c;具体如下图所示&…

2024新型数字政府综合解决方案(八)

新型数字政府综合解决方案结合人工智能、大数据、区块链与云计算等先进技术&#xff0c;旨在构建一个智能、高效、透明的政务服务体系&#xff0c;通过全面整合各部门的信息资源&#xff0c;实现数据的实时共享与高效管理&#xff0c;从而大幅提升政府应对复杂社会问题的能力&a…