synchronized的轻量级锁居然不会自旋?

news2024/12/24 0:58:35

《Java并发编程的艺术》中说到「如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁」,并且下文所配的流程图中明确表示自旋失败后才会升级为重量级锁,但《深入理解Java虚拟机》又说「如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁」,到底会不会呢?其实相信synchronized源码很少有人愿意去扒去看,本文会尽量用简洁易懂的方式说清synchronized的原理。

只对实现原理感兴趣可以直接跳过到「synchronized实现原理」

synchronized基本使用
一般有三种方式:

修饰普通方法:锁this

  // 1. synchronized用在普通方法上,默认的锁就是this,当前实例
    public synchronized void method() {}

修饰静态方法:锁this.class

  // 2. synchronized用在静态方法上,默认的锁就是当前所在的Class类
  // 所以无论是哪个线程访问它,需要的锁都只有一把
    public static synchronized void method() {}

同步代码块:自定义锁对象
自定义锁对象可以是实例,也可以是Class对象

synchronized (this) {}
synchronized(SynchronizedObjectLock.class){}

抛出异常会释放锁
无论正常退出还是抛出异常,synchronized都保证能够释放锁。

锁与happens-before规则
我们知道,解锁操作 happens-before 加锁,因此:

首先有个变量a,没有用volatile修饰

int a = 0;

线程A先执行:

public synchronized void writer() { // 1
        a++; // 2
} // 3

线程B后执行:

public synchronized void reader() { // 4
        int i = a; // 5
} // 6 

由h-b规则,3 h-b 4,再由as if serial和传递性原则,因此2 h-b 5,而h-b从开发人员的角度来说,你就可以理解为2在5之前执行,并且2的结果对5可见,因此5处读到的a,一定为1。

synchronized的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM会把该线程对应的本地内存置为无效

可以看到:

锁释放与volatile写有相同的内存语义;

锁获取与volatile读有相同的内存语义。

synchronized实现原理
下面我用尽量清晰简洁,绕过虚拟机源码的方式来讲一下:

会跳过一些源码细节的实现,不会影响整体流程和理解

要了解实现原理,第一步我会先看一下字节码指令:

透过字节码看异常如何释放锁
synchronized修饰的方法会被加上 ACC_SYNCHRONIZED的flag。

而同步代码块的字节码是这样的:

monitorenter    
...
monitorexit 
goto xxx
monitorexit
athrow
return
 Exception table:
from   to  target type
 4     14    17    any
 17    20    17    any

可以看到,monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出。

有两个monitorexit,因为javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。如果不知道字节码的Exception table是什么可以参考:异常处理实现原理

尽管字节码通常都能帮助我们更好地理解语义,但关于synchronized的语义也就到此为止了,接下来就要深入虚拟机源码看看monitorenter(获取锁)和monitorexit(释放锁)到底都干了些什么,不过在此之前:

因为synchronized有四种锁状态,而锁状态的实现依赖于Java对象的mark word,这是实现synchronized的基础,我们先来看mark word如何表达锁状态的。

Java中的每一个对象都可以作为一个锁,包括Class对象。

四种锁状态
Java对象头的mark word
在这里插入图片描述

注意轻/重锁的mark word内是持有一个指向锁记录的指针的。

因此,一个对象其实有四种锁状态,级别由低到高:

无锁状态

偏向锁状态

轻量级锁状态

重量级锁状态

1、无锁
释放轻量级锁,没有线程在尝试获取锁,也没有线程持有锁(正在执行同步代码块),就是无锁。

2、偏向锁(JDK15被废弃)
偏向锁在JDK1.6引入,在JDK15被废弃,了解即可。如果一定要用,需要手动打开:

-XX:+UseBiasedLocking

人们发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是有了偏向锁。

偏向锁顾名思义,偏向于第一个访问锁的线程。偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

当开启偏向锁功能时,创建的新对象是可偏向状态,此时mark word中的thread id为0,也叫做匿名偏向。当该对象第一次被CAS成功时,成为「偏向锁」。

在该线程又一次尝试获取该对象锁时,发现thread id就是自己,就可以不做CAS直接认为已经拿到了锁并执行同步代码块中的代码。

注意上述的所有,都只出现了一个线程

当第二个线程出现并尝试获取锁,无论如何都会升级成「轻量级锁」。

如果第一个线程正在执行同步代码块,锁偏向的线程继续拥有锁,当前线程升级该锁为「轻量级锁」。

如果第一个线程不在执行同步代码块,先将对象头的mark word改为无锁状态,再升级为「轻量级锁」。

也就是是要有两个线程尝试获取锁,不论是否出现资源竞争,升级为「轻量级锁」。

3、轻量级锁
升级到「轻量级锁」的条件是:存在多个线程尝试CAS获取同一把锁,尽管彼此之间互不影响。而「轻量级锁」继续膨胀为「重量级锁」的条件是:只要CAS失败,就升级,即发生了:一个线程正在执行同步代码块的同时,另一个线程尝试获取锁。

轻量级锁会自旋吗
自旋:不断尝试去获取锁,一般用循环来实现。

这是不对的,是网上最常见的错误之一,你问chatGPT他也是这个答案,但这就是个错误的答案。因为前面说的很清楚了,只要发生哪怕一次CAS失败,就不是「轻量级锁」了,何来自旋呢?

自旋的说法从何而来
《Java并发编程的艺术》(2015)原文是:

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

《深入浅出Java多线程1.0.0》原文是:

然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

总之,以上两位作者认为:发生竞争,自旋,并没有指出自旋前会发生锁膨胀。

《深入理解Java虚拟机》(2019)原文是:

如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。

周志明大大的意思是:出现两条以上的线程争用同一个锁的情况,就要升级为重量级锁,没有指出升级为重量级锁前要自旋。

显然,这两种观点是有冲突的,核心问题在于:

轻量级锁状态下,发生资源竞争,到底是自旋,还是立刻锁膨胀?

如何考证说法的正确性
那么我们也只能自己去看源码来验证说法的正确性了(但很少有人愿意看吧)

下文我会尽量清楚地用文字表达出源码传达的意思

轻量级锁实现原理
获取锁
发现是无锁状态,线程会把锁的Mark Word复制到自己的Displaced Mark Word(栈帧中的一块空间) ,然后通过CAS尝试将锁的Mark Word修改为一根指针,指向自己的Displaced Mark Word(Displaced Mark Word与原mark word的内容一模一样,保存了HashCode,GC年龄等信息)

发现处于轻量级锁状态

如果轻量级锁的markword指向自己的Displaced Mark Word,代表重入锁,那么获取锁成功(如果是重入,会将markword改为null,空指针,即0)

如果轻量级锁的markword不是指向自己,锁膨胀,升级为「重量级锁」

CAS失败直接膨胀

释放锁
首先,遍历线程栈,拿到所有需要做解锁操作的锁对象:

如果是null,代表可重入的锁,直接解锁成功

如果不是重入的锁:

还原成功,轻量级锁解锁成功

还原失败,仍然是「尝试解锁重量级锁」

如果markword被修改,说明发生了竞争,已经成为「重量级锁」了,「尝试解锁重量级锁」

如果markword没被修改,尝试CAS还原对象的markword

补充说明:线程A正在执行同步代码块时,此时有线程CAS失败,虽然升级为「重量级锁」,但仍然由线程A持有锁,「如何膨胀为重量级锁」后文马上分析

4、重量级锁
为了实现锁膨胀,避免并发膨胀锁,定义了四种膨胀锁状态:

膨胀完毕

膨胀中

无锁

轻量级锁

下面依次对这些情况的膨胀进行分析:

重量级锁的生成/锁膨胀
若膨胀完毕,直接返回monitor

若膨胀中,线程等待一会,直到别的线程膨胀完毕,然后拿到别人生成的monitor

从轻量级锁开始膨胀:

创建monitor对象

CAS将锁状态修改为「膨胀中」

将markword保存至monitor

设置持有monitor的线程

将monitor地址设置为mark word

返回monitor对象

失败,说明别人在膨胀了,等待,然后返回别人生成的monitor

成功:

从无锁开始膨胀,差不多:

创建monitor对象

将markword保存至monitor

CAS将锁状态修改为「膨胀中」

失败,说明别人在膨胀了,等待,然后返回别人生成的monitor

成功,返回monitor对象

重量级锁实现原理
生成了重量级锁,mark word会指向堆中实际生成的monitor对象,我们先来看看monitor对象的结构:
在这里插入图片描述
Contention List(cxq):所有请求锁的线程将被首先放置到该竞争队列,是先进后出的栈结构

Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List

Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set

OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck

Owner:获得锁的线程称为Owner

!Owner:释放锁的线程

获取锁
对于重量级锁,尝试获取锁具体是指:尝试用CAS将monitor对象的Owner从nullptr改变为自己

当一个线程尝试获得重量级锁时

首先尝试「自旋」,调用trySpin方法获取锁,如果第一次失败,再进行一次trySpin方法(最坏情况拿不到锁会调用两次trySpin),然后『用CAS的方式进入cxq』

进入cxq后,陷入「死循环」,死循环中,可能会从cxq转移到EntryList,可能阻塞,也可能调用trySpin方法自旋。后文再详细分析「死循环」

可以看到「死循环」的实现也依赖trySpin自旋,因此我们先来看看「自旋」的实现逻辑:

1、自旋锁
自旋:不断尝试去获取锁,一般用循环来实现。

如果是单核CPU,自旋是无意义的,所以只有多处理器才会开启自旋功能

自旋的出现,是为了避免切换到内核态,因为线程的阻塞和唤醒依赖内核,我们希望能够一定程度上避免这种内核态与用户态的切换,因此有了「自旋锁」。那么自旋多少次更合适呢?

在锁很快被释放时,自旋既不会带来CPU资源的浪费,还能提高运行效率。此时自旋次数过少,可能会导致没能顺利拿到锁,即使结束自旋后不久锁就被释放了。

在锁很久才被释放时,自旋空转占用CPU资源却迟迟拿不到锁,造成过多的CPU资源浪费。此时自旋次数过多,反而会得不偿失。

因此,JDK发明了自适应自旋,来适应各种情况的锁。

自适应自旋
自适应自旋为了权衡自旋次数过多和过少带来的弊端,它的基本思想是:

自旋成功拿到锁了,说明你下次成功的概率也很大,下次自旋的次数会更多

自旋失败,说明你下次也大概率拿不到,下次自旋的次数会更少

自适应自旋参数如下:
在这里插入图片描述
自旋逻辑:trySpin
首选预自旋11次(避免预自旋次数设置为0,源码后面对这个参数加了1),如果没拿到锁:

开始自旋5000次(假设是第一次开始自旋,上限就为5000)

成功,下次+100,下次可以最多自旋5100次

失败,下次- 200,下次可以最多自旋4800次,不会少于1000次

2、死循环
死循环主要是在「阻塞」和「自旋」之间切换

park阻塞,注意不会移动到WaitSet中

unpark唤醒,再次调用trySpin方法自旋获取锁,如果失败,陷入阻塞

只有释放锁时,才会调用unpark唤醒,进入自旋状态,此时并不是一定能拿到锁的。

唤醒的时机
释放锁时才会唤醒,且只会唤醒一个,唤醒逻辑取决于Policy参数。

cxq和EntryList内线程的行为
这两个区域内的线程几乎是全阻塞的,这两个区域内的线程,保证最多只有一个线程去竞争锁资源,这个被『释放锁时唤醒的唯一的线程』叫「假定继承人」,即Monitor结构中的「OnDeck」。

注意:只保证所有阻塞的线程,只有一个去竞争锁资源,仍然可能被外来的线程在进入cxq之前就抢到了锁,所以说synchronized是不公平的。

EntryList内的线程全部来自cxq,在释放锁与调用notify方法时,可能进入EntryList

释放锁
通过CAS的方式将Monitor结构的Owner修改为nullptr

根据QMode参数的不同,执行不同的逻辑

因为QMode默认值为0,我们来看一下默认的逻辑:

如果EntryList和cxq均为空:什么也不做

如果EntryList非空:就取EntryList首元素唤醒

如果EntryList为空,cxq非空:将cxq的所有线程放到EntryList,再唤醒EntryList首元素;

锁被持有时,EntryList和cxq的所有线程都阻塞,有且只有锁释放这唯一一个行为能够唤醒其中的一个线程。

为什么要区分cxq和EntryList
是为了解决CAS的ABA问题,也能分散请求,提高性能。

cxq和EntryList都是为了存储所有阻塞的线程,但是:

释放锁并唤醒时,只会唤醒EntryList的线程,这是删除操作

线程自旋次数过多需要被阻塞时,只会插入cxq队列,这是添加操作

把这两种操作分离开来有什么好处呢?

提高性能
由于锁只有一把,因此做删除操作的线程只有一个,不存在线程安全问题,不需要做CAS,如果和添加操作混在一起,就不得不考虑线程安全问题了。这样只需要在cxq内考虑CAS即可。

解决ABA问题
因为多个线程同时add,不会有某个线程出现在cxq里两次,因此只add不会有ABA问题。而一旦存在删除操作,那么ABA问题就是有可能的。

可感知的锁控制权
现在知道了加解锁的原理,那其实我们已经有能力知道,释放锁时会唤醒哪个线程。(暂时不考虑wait/notify)

结论:先阻塞的线程,最晚获得锁。

有三个线程,t1,t2,t3。这三个线程都自旋失败,插入cxq,由于是个栈,越晚进入cxq的,反而越早进入EntryList,顺序为t3,t2,t1。而唤醒时是按照EntryList的顺序去唤醒的,因此「并不是所谓的随机唤醒」。当然,如果此时有别的线程t4自旋未进入cxq,是有可能拿到锁的,但我们保证:t3先于t2被唤醒,t2先于t1被唤醒

阶段性小结(一)
到这里,应该对锁机制非常熟悉了,你应该清楚:

Monitor锁结构

自旋的原理和应用,自旋不会出现在轻量级锁

重量级锁加解锁的逻辑

我们趁热打铁来学习一下wait/notify的底层原理,至今仍未露面的WaitSet终于要登场了,学完wait/notify整个synchronized也就 “证据链闭环” 了。

从趁热打铁的角度,趁你还对加解锁和Monitor结构足够熟悉,我非常推荐直接跳到「wait/notify底层原理」看,当然,在此之前请确保你对wait/notify的基础知识足够了解

等待通知机制:wait/notify
wait/notify必备的基础知识
wait/notify只能用在synchronized代码块内部,且必须是重量级锁。

只有持有锁的线程能够调用wait/notify方法

调用wait会使当前线程释放锁并陷入阻塞状态

从wait()方法返回的前提是获得了调用对象的锁

可以唤醒一个(notify)或多个(notifyAll)

调用notify无法保证被唤醒的线程一定拿到锁

当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

wait/notify基本使用
等待通知基本模型
等待者:


synchronized(对象) {
    while(条件不满足) {
        对象.wait();
    }
    对应的处理逻辑
}

通知者:

synchronized(对象) {
    改变条件
    对象.notifyAll();
}

等待超时模型
这样一个熟悉的场景:调用一个方法时等待一段时间(一般来说是给定一个时间段),如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。

public synchronized Object get(long mills) throws InterruptedException {
    long future = System.currentTimeMillis() + mills;
    long remaining = mills;
    // 当超时大于0并且result返回值不满足要求
    while ((result == null) && remaining > 0) {
        wait(remaining);
        remaining = future - System.currentTimeMillis();
    }
    return result;
}

wait/notify底层原理
wait方法
将当前线程包装成ObjectWaiter对象,放入WaitSet中,并调用park挂起

执行「释放锁」的逻辑。

只有notify方法有可能将线程从WaitSet拯救出来,处于WaitSet的线程永远是阻塞状态,不可能参与锁竞争

notify方法
从WaitSet中取出第一个线程,根据Policy的不同,将这个线程放入EntryList或者cxq队列中的起始或末尾位置

默认Policy为2,即:

EntryList队列为空,将线程放入EntryList

EntryList队列非空,将线程放入cxq队列的头部位置(栈顶);

强调一下:notify方法只是将线程从WaitSet移动到EntryList或者cxq,不是直接让它开始自旋CAS。

wait/notify理解实战
看下面这段代码,在不修改 HotSpot VM源码的情况下,考虑几个问题:

输出唯一确定吗?

如果确定,会输出什么?

public class NotifyDemo {
private static void log(String desc){
    System.out.println(Thread.currentThread().getName() + " : " + desc);
}

Object lock = new Object();

public void startThreadA(){
    new Thread(() -> {
        synchronized (lock){
            log("get lock");
            startThreadB();
            log("start wait");
            try {
                lock.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }

            log("get lock after wait");
            log("release lock");
        }
    }, "thread-A").start();
}

public void startThreadB(){
    new Thread(()->{
        synchronized (lock){
            log("get lock");
            startThreadC();
            sleep(100);
            log("start notify");
            lock.notify();
            log("release lock");

        }
    },"thread-B").start();
}

public void startThreadC(){
    new Thread(() -> {
        synchronized (lock){
            log("get lock");
            log("release lock");
        }
    }, "thread-C").start();
}

public static void main(String[] args){
    new NotifyDemo().startThreadA();
  }
}

输出唯一确定,为:

thread-A : get lock
thread-A : start wait
thread-B : get lock
thread-B : start notify
thread-B : release lock
thread-A : get lock after wait
thread-A : release lock
thread-C : get lock
thread-C : release lock

为什么最后四行A一定先于C发生?

线程C获取锁失败,直接放入cxq首部;线程A被notify,会被放入EntryList。之后B释放锁,发现EntryList内有线程A,就直接把A唤醒。

自定义抢锁逻辑:修改JVM参数
有两个参数会影响synchronized的行为逻辑:

Policy参数:唤醒线程
Policy参数决定如何唤醒线程

Policy == 0:放入EntryList队列的排头位置;

Policy == 1:放入EntryList队列的末尾位置;

Policy == 2:EntryList队列为空就放入EntryList,否则放入cxq队列的排头位置;

Policy == 3:放入cxq队列中,末尾位置

QMode参数:释放锁
QMode参数决定如何释放锁

QMode = 2,并且cxq非空:取cxq队列排头位置的ObjectWaiter对象,唤醒该线程,结束

QMode = 3,把cxq队列的全部元素放入EntryList尾部,然后执行步骤四;

QMode = 4,把cxq队列的全部元素放入EntryList头部,然后执行步骤四;

QMode = 0,不做什么,执行步骤4;(默认为0)

如果EntryList非空,就取首元素唤醒,否则整个cxq放到EntryList,再唤醒EntryList首元素;

通过修改这两个参数,就可以自定义notify和释放锁的逻辑。还是上面那个例子,只需要修改QMode为4,就可以确保最后四行C先于A执行。

阶段性小结(二)
其实wait/notify原理并不难懂,甚至可以说是非常好理解,就不再重复了。

到此为止,与synchronized的原理基本就讲解完毕了,接下来我们重新审视一下一些比较笼统而泛泛的问题,不仅能帮助你更好地理解synchronized的原理,也能对synchronized有一个更全面的认知。算是一些补充说明吧。

synchronized的特点
非公平锁
非公平锁完全可以从前文的原理体现出来:

新来的线程不断自旋不会阻塞,因此比起阻塞中的线程,更容易抢占锁

cxq先入后出,先陷入阻塞的线程反而更晚执行

notify唤醒的线程,如果EntrySet为空直接放入EntrySet,先于cxq被执行

可重入性
synchronized是可重入的

monitor有个计数器recursions,起初为0,Monitorenter + 1,Monitorexit - 1,减为0会释放锁。

乐观 or 悲观
什么是悲观锁,什么是乐观锁?

看似简单的概念,很多人第一次学习时都会顾名思义,但现在网络上主流的观点有两种:

乐观锁只是一种思想,认为不会竞争锁,仅此而已

乐观锁是线程先执行锁区域的内容,执行过程中检查是否出现竞争

核心的矛盾点在于,乐观锁到底是纯思想,还是对实现做了一些行为规范的定义(比如必须:什么都不操作直接执行同步代码块的内容)?

如果读者有关于「乐观锁」较为官方的定义,请在评论区告诉我,感激不尽

但如果「乐观锁」仅仅是一种思想,那可以说:synchronized的所有线程,只要没有被阻塞,那就是乐观的,只有重量级锁中那些在cxq和EntryList的阻塞的线程是悲观的(WaitSet是自愿阻塞不算在内)。因为如果足够悲观,早就阻塞等待去了,为啥还要自旋CAS呢?

编译器对synchronized的优化
锁消除
如果编译器发现不会发生线程安全问题,就会无视了你的锁。

锁粗化
比如执行插入数据商品时,是对店铺加锁。那么批量执行的时候,只需要加一次锁。而不是每插入一次就加/释放一次锁。

  StringBuffer sb = new StringBuffer();
     sb.append(s1);
     sb.append(s2);
     sb.append(s3);
  // 线程安全的buffer类,append会加锁,但显然这是可以锁粗话的,会优化成只获得/释放一次锁

synchronized与包装类的坑
Integer并不适合当作锁对象。

因为有缓存机制,-128~127有缓存。容易导致锁失效。

volatile static Integer ticket = 10

比如两个线程抢票,不能锁住 ticket。抢完票以后ticket–,一个线程A锁的是ticket = 10的对象,另一个线程B执行完ticket = 10的临界区代码,ticket–,再走临界区,他的锁变成了9,与A竞争的都不是一把锁,因此两者都会抢到锁。

因此:

锁住的对象尽量是静态的不变的,比如class类

不能是各种有缓存的包装类

在idea中 没有声明final的对象加synchronized会提示不安全

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

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

相关文章

汇编-PROTO声明过程

64位汇编 64 模式中,PROTO 伪指令指定程序的外部过程,示例如下: ExitProcess PROTO ;指定外部过程,不需要参数.code main PROCmov ebx, 0FFFFFFFFh mov ecx,0 ;结束程序call ExitProcess ;调用外部过程main ENDP END 32位…

眼镜清洗机原理是怎么样的?2023年眼镜清洗机推荐

在日常生活中有许多小伙伴是因为看太多书或者是看太多电子产品导致近视佩戴上了眼镜,毕竟眼镜佩戴上后就再也离不开它了,像日常佩戴的眼镜上会积累非常多污垢以及堆积细菌,而我们手动清洗眼镜时不能除菌也不能清洁到缝隙中,像眼镜…

鸿蒙原生应用/元服务开发-AGC分发如何配置签名信息

使用制作的私钥(.p12)文件、在AGC申请的证书文件和Profile(.p7b)文件,在DevEco Studio配置工程的签名信息,以构建携带发布签名信息的APP。 1.打开DevEco Studio,菜单选择“File > Project S…

20s上手!文本生成3D模型

公众号:算法一只狗 硅谷初创公司Luma AI发布了一款名为Genie的Discord机器人,用于生成文本到3D内容,为游戏开发、虚拟制作和艺术创作带来变革。用户只需输入文本指令,Genie即可在20秒内生成四个简单的3D模型,并支持进一…

MMDetection3.0以上如何在推理是不显示类名?

找到/mmdet/visualization/local_visualizer.py这个文件,从156行开始 for i, (pos, label) in enumerate(zip(positions, labels)):# 这里先把类名拼接在了label_text中if label_names in instances:label_text instances.label_names[i]else:label_text classes…

内衣专用洗衣机怎么样?口碑最好的小型洗衣机

随着人们的生活水平的提升,越来越多小伙伴来开始追求更高的生活水平,一些智能化的小家电就被发明出来,而且内衣洗衣机是其中一个。现在通过内衣裤感染到细菌真的是越来越多,所以我们对内衣裤的清洗频次会高于普通衣服,…

基于springboot实现留守儿童爱心网站项目【项目源码+论文说明】

基于springboot实现留守儿童爱心网站平台系统演示 摘要 随着留守儿童爱心管理的不断发展,留守儿童爱心网站在现实生活中的使用和普及,留守儿童爱心管理成为近年内出现的一个热门话题,并且能够成为大众广为认可和接受的行为和选择。设计留守儿…

springboot+vue项目如何集成onlyoffice开源文档组件

一、onlyoffice是什么 ONLYOFFICE 是一个开源的办公套件,适合多人在线协作。由总部位于总部在拉脱维亚的 IT 公司Acensio System SIA 开发。它提供在线协作文档编辑器(包括文档、电子表格、演示文稿和表单),适用于 Windows、Linu…

如何搭建Splunk Enterprise平台并结合内网穿透工具实现公网访问

文章目录 前言1. 搭建Splunk Enterprise2. windows 安装 cpolar3. 创建Splunk Enterprise公网访问地址4. 远程访问Splunk Enterprise服务5. 固定远程地址 前言 Splunk Enterprise是一个强大的机器数据管理平台,可帮助客户分析和搜索数据,以及可视化数据…

Linux下安装python3步骤:

1.下载Python3源码 你需要从Python官网下载Python3的源码包。本文以Python 3.9.9为例。你可以使用wget命令来下载源码包到你的Linux主目录中: wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz2.编译和安装Python3 下载好源码包后,你需要解压它&…

【Python】itertools模块,补充:可迭代对象、迭代器

Python中 itertools模块创建高效迭代器、处理序列数据集。 此模块所有函数返回迭代器,可用for循环获取迭代器中的内容,也可用list(...)用列表形式显示内容。 import itertools[ x for x in dir(itertools) if not x.startswith(_)] # 结果:…

2023软件应用类下载系统平台源码/手机软件应用、新闻资讯下载站/软件库网站源码

源码简介: 这个是最新软件应用类平台源码、手机应用下载系统源码、软件应用市场下载站源码、新闻资讯软件下载。2023软件应用类平台源码/手机软件应用、新闻资讯下载站,它是软件库网站源码。 最新软件应用类平台源码 手机应用下载系统源码 软件应用市场…

如何从 C# 制作报表到 FastReport Cloud

众所周知,我们的世界在不断发展,新技术几乎每天都会出现。如今,不再需要在办公室内建立整个基础设施、雇用人员来监控设备、处理该设备出现的问题和其他困难。 如今,越来越多的服务提供业务云解决方案,例如FastReport…

我的虚拟人物介绍

背景 大家好,我是小欣,是这个博客的虚拟助手。在这里,我将为大家提供各种有趣、实用、甚至是意想不到的信息。作为一个年轻的语言模型,我的目标是为你们呈现出最有趣和有深度的内容。 我喜欢与大家分享知识、解答问题&#xff0…

Comsol Multiphysics 6.2 for Mac建模仿真软件

COMSOL Multiphysics是一款多物理场仿真软件,旨在帮助工程师、科学家和研究人员解决各种复杂的工程和科学问题。该软件使用有限元分析方法,可以模拟和分析多个物理场的相互作用,包括结构力学、热传导、电磁场、流体力学和化学反应等。 COMSOL…

小辰的智慧树(差分+前缀和)

登录—专业IT笔试面试备考平台_牛客网 1.考虑总长度之和不能超过m,2考虑限制每棵树高度不能低于ci,如果用二分最短输能截到的高度,还要另外去判断,是否每棵树mid都能严格大于ci ,这样容易超时,换个角度&…

Linux:动静态库

目录 一、软硬链接 1、软链接 2、硬链接 二、动态库和静态库 编写一个库 ①静态库 使用静态库的方法 ②动态库 使用动态库的方法 库存在的意义 一、软硬链接 软硬链接的本质区别就是:有无独立的inode 软链接有独立的inode,也就意味着软链接是一…

Vue3 封装组件库并发布到npm仓库

一、创建 Vue3 TS Vite 项目 输入项目名称,并依次选择需要安装的依赖项 npm create vuelatest 项目目录结构截图如下: 二、编写组件代码、配置项和本地打包测试组件 在项目根目录新建 package 文件夹用于存放组件 (以customVideo为例&a…

「Verilog学习笔记」不重叠序列检测

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点,刷题网站用的是牛客网 题目要求检测a的序列,a为单bit输入,每个时刻可能具有不同的值, 当连续的六个输入值符合目标序列表示序列匹配,当六个输入值的…

VINS-MONO代码解读----vins_estimator(重点部分)

1. 代码目录如下,重点和难点是factor部分,是关于IMU部分的,有较多关于IMU预积分公式的推导。 1. 条件变量con.wait读取测量值:getMeasurements() 读取buf中IMU和IMG的数据,并进行align,最后的结果是这样…