synchronized关键字详解

文章目录

    • synchronized
      • 使用示例
      • 实现原理
      • 锁的升级
      • synchronized与可见性
      • synchronized与原子性
      • synchronized与有序性

synchronized

synchronized是Java提供的关键字译为同步,是Java中用于实现线程同步的一种机制。它可以确保在同一时间只有一个线程能够执行某段代码,从而避免线程安全问题。当它修饰一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。synchronized关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,大部分并发控制操作都能使用synchronized来完成。

synchronized的作用:

  • 互斥性:确保在同一时间只有一个线程可以执行被 synchronized 修饰的代码块或方法。
  • 可见性:当一个线程退出 synchronized 代码块时,它所做的所有修改对于进入 synchronized 代码块的其他线程是可见的。这是通过释放和获得监视器锁来实现的。

使用示例

修饰的对象作用范围作用对象
同步一个实例方法整个实例方法调用此方法的对象
同步一个静态方法整个静态方法此类的所有对象
同步代码块-对象整个代码块调用此代码块的对象
同步代码块-类整个代码块此类的所有对象
  • 同步一个实例方法。在这种情况下,increment方法被声明为同步方法。当一个线程调用这个方法时,它会获得该实例的监视器锁,其他线程必须等待这个线程释放锁后才能调用这个方法。
    public synchronized void increment() {count++;
    }
    
  • 同步一个静态方法。当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,而是类成员,因此通过class对象锁可以控制静态成员的并发操作。
    public static synchronized void increment() {count++;
    }
    
  • 同步代码块。在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,这样做就有点浪费。此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。
    public void increment() {synchronized (this) {count++;}
    }
    
    除了使用synchronized(this)锁定,当然静态方法是没有this对象的,也可以使用class对象来做为锁。
    public void increment() {synchronized (MainTest.class) {count++;}
    }
    

当如果没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁。

private byte[] lock = new byte[0];
public void method(){synchronized(lock) {// .....}
}

零长度的byte数组对象创建起来将比任何对象都经济。查看编译后的字节码,生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

byte[] emptyArray = new byte[0];0: iconst_0       // 将常量0推送到栈顶
1: newarray byte  // 创建一个新的byte类型数组
3: astore_1       // 将引用类型的数据存储到局部变量表中
Object lock = new Object();0: new           #2   // 创建一个新的对象
3: dup                // 复制栈顶的操作数栈顶的值,并将复制值压入栈顶
4: invokespecial #1   // 调用实例初始化方法, 使用Object.<init>
7: astore_1           // 将引用类型的数据存储到局部变量表中

实现原理

synchronized关键字在Java中通过进入和退出一个监视器来实现同步。监视器本质上是一种锁,它可以是类对象锁或实例对象锁。每个对象在JVM中都有一个与之关联的监视器。当一个线程进入同步代码块或方法时,它会尝试获得对象的监视器。如果成功获得锁,线程就可以执行同步代码;否则它将被阻塞,直到获得锁为止。

在Java中synchronized锁对象时,其实就是改变对象中的对象头的markword的锁的标志位来实现的。用javap -v MainTest.class命令反编译下面代码。

public class MainTest {synchronized void demo01() {System.out.println("demo 01");}void demo02() {synchronized (MainTest.class) {System.out.println("demo 02");}}}
  synchronized void demo01();descriptor: ()Vflags: ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String demo 015: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return
// ...
void demo02();descriptor: ()Vflags:Code:stack=2, locals=3, args_size=10: ldc           #5                  // class content/posts/rookie/MainTest2: dup3: astore_14: monitorenter5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;8: ldc           #6                  // String demo 0210: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V13: aload_114: monitorexit15: goto          2318: astore_219: aload_120: monitorexit21: aload_222: athrow23: return
// ...

通过反编译后代码可以看出:

  • 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步;
  • 对于同步代码块,JVM采用monitorentermonitorexit两个指令来实现同步;

其中同步代码块,有两个monitorexit指令的原因是为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

官方文档中关于同步方法和同步代码块的实现原理描述:

方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块使用 monitorentermonitorexit 两个指令实现。可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

其实无论是ACC_SYNCHRONIZED还是monitorentermonitorexit都是基于Monitor实现的,每一个锁都对应一个monitor对象。在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。在/hotspot/src/share/vm/runtime/objectMonitor.hpp中有ObjectMonitor的实现。

// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {_header       = NULL;_count        = 0; //记录个数_waiters      = 0,_recursions   = 0;_object       = NULL;_owner        = NULL;_WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet_WaitSetLock  = 0 ;_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;}
  • _owner:指向持有ObjectMonitor对象的线程;
  • _WaitSet:存放处于wait状态的线程队列;
  • _EntryList:存放处于等待锁block状态的线程队列;
  • _recursions:锁的重入次数;
  • _count:用来记录该线程获取锁的次数;

当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域,并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1,即获得对象锁。

在这里插入图片描述

若此时持有monitor的线程调用wait()方法,将释放当前对象持有的monitor_owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取monitor

ObjectMonitor中其他方法:

  bool      try_enter (TRAPS) ;void      enter(TRAPS);void      exit(bool not_suspended, TRAPS);void      wait(jlong millis, bool interruptable, TRAPS);void      notify(TRAPS);void      notifyAll(TRAPS);

sychronized加锁的时候,会调用objectMonitorenter方法,解锁的时候会调用exit方法。在JDK1.6之前,synchronized的实现直接调用ObjectMonitorenterexit,这种锁被称之为重量级锁,这也是早期synchronized效率低的原因。所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化。

早期的synchronized效率低的原因:
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,监视器锁monitor是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态。因此状态转换需要花费很多的处理器时间。

对于代码简单的同步块(如被synchronized修饰的getset方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是Java语言中一个重量级的操作。也是为什么早期的synchronized效率低的原因。

锁的升级

在JDK1.6之前,使用synchronized被称作重量级锁,它的实现是基于底层操作系统的mutex互斥原语的,这个开销是很大的。所以在JDK1.6时JVM对synchronized做了优化。synchronized锁对象时,其实就是改变对象中的对象头的markword的锁的标志位来实现的。对象头中markword锁状态的表示:

锁状态markword 锁标志位
无锁状态01
偏向锁状态01
轻量级锁状态00
重量级锁状态10
被垃圾回收器标记11

对象的锁状态,可以分为4种,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。其中这几个锁只有重量级锁是需要使用操作系统底层mutex互斥原语来实现,其他的锁都是使用对象头来实现的。

  • 无锁状态:markword锁的标志位0,偏向锁的标志位为1;例如:刚被创建出来的对象。
  • 偏向锁:如果一个线程获取了锁,此时markword的结构变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,直接可以获取锁。
    省去了大量有关锁申请的操作,从而也就提供程序的性能。
  • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞从而提高性能。
  • 重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时MarkWord中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态,所以开销是很大。

随着锁的竞争,锁从偏向锁升级到轻量级锁,再升级的重量级锁。锁升级过程:

  1. 无锁状态升级为偏向锁:一个对象刚开始实例化的时候,没有任何线程来访问它的时候,它是可偏向的,意味着它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程。此时对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,就不需要再使用CAS在进行操作。
  2. 偏向锁升级为轻量级锁:一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象的偏向状态。这时表明在这个对象上已经存在竞争了,JVM会检查原来持有该对象锁的线程是否依然存活,如果不存活,则可以将对象变为无锁状态,然后重新偏向新的线程。如果原来的线程依然存活,则马上执行这个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁。
  3. 轻量级锁升级为重量级锁:轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下,另一个线程就会释放锁。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞。当持有锁的线程退出同步块或方法时,会执行monitorexit指令释放锁。如果有其他线程在等待该锁,它们会被唤醒并竞争锁的所有权。

在所有的锁都启用的情况下,线程进入临界区时会先获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁。如果自旋也没有获取到锁,则使用重量级锁,将没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们。

偏向锁是在无锁争用的情况下使用的,也就是同步代码块在当前线程没有执行完之前,没有其它线程会执行该同步块。一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁。

锁可以升级,但是不可以降级,有的观点认为不会进行锁降级。实际上,锁降级确实是会发生的,当JVM进入安全点的时候,会检查是否有闲置的`Monitor,然后试图进行降级。也就是说,仅仅是发生在STW的时候,只有垃圾回收线程能够观测到它,在我们正常使用的过程中是不会发生锁降级的,只有在GC的时候才会降级。

安全点:程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点。

synchronized与可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以就可能出现线程1改了某个变量的值,但是线程2不可见的情况。

synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。但是为了保证可见性,有一条规则是这样的,“对一个变量解锁之前,必须先把此变量同步回主存中”,这样解锁后,后续线程就可以访问到被修改后的值。所以synchronized关键字锁住的对象,其值是具有可见性的。

public class VisibilityExample {private boolean flag = false;public synchronized void toggleFlag() {// 修改共享变量并确保可见性flag = !flag;// 其他操作}public synchronized boolean isFlag() {// 读取共享变量并确保可见性return flag;}
}

synchronized与原子性

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

线程是CPU调度的基本单位,CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit,这两个字节码指令,在Java中对应的关键字就是synchronized。通过monitorexitmonitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

举个例子,线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是它并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码,直到所有代码执行完,这就保证了原子性。

public class AtomicityExample {private int count = 0;public synchronized void increment() {// 原子性的递增操作count++;}public synchronized void decrement() {// 原子性的递减操作count--;}public synchronized int getCount() {// 原子性的读取操作return count;}public static void main(String[] args) {AtomicityExample example = new AtomicityExample();// 线程1:递增操作Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.increment();}});// 线程2:递减操作Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {example.decrement();}});// 启动线程thread1.start();thread2.start();try {// 等待两个线程执行完成thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}// 输出最终的计数结果System.out.println("Final Count: " + example.getCount());}
}

synchronized与有序性

有序性即程序执行的顺序按照代码的先后顺序执行。

除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save有可能被优化成load->save->add这就是可能存在有序性问题。这里需要注意的是,synchronized是无法禁止指令重排和处理器优化的,也就是说synchronized无法避免上述提到的问题。那synchronized是如何保证有序性的?

synchronized通过两个主要机制来保证有序性。synchronized的主要特性是互斥性,意味着在同一时刻只有一个线程可以进入同步块,既然是单线程就需要遵守as-if-serial语义,那么就可以认为单线程程序是按照顺序执行的。

as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

第二个保证就是内存屏障。编译器和CPU在执行代码时,可能会为了优化性能进行指令重排,但synchronized块内的指令不会被重排。原因就是Java内存模型通过在进入和退出synchronized块时插入内存屏障,来保证这些操作在多线程环境下的顺序执行。在进入synchronized块时,会插入一个LoadLoad屏障和一个LoadStore屏障,确保在锁被获取后,前面的所有读操作和写操作都已经完成。在退出synchronized块时,会插入一个StoreStore屏障和一个StoreLoad屏障,确保在锁被释放前,所有的写操作都已经完成,并且这些写操作对其他线程可见。

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

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

相关文章

【Python系列】数字的bool值

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

MVC之 IHttpModule管道模型《二》

》》》注意&#xff1a;在http请求的处理过程中&#xff0c;只能调用一个HttpHandler&#xff0c;但可以调用多个HttpModule。 HTTP Modules ASP.NET请求处理过程是基于管道模型的&#xff0c;这个管道模型是由多个HttpModule和HttpHandler组成&#xff0c;当请求到达HttpMod…

UART编程

Q:为什么使用串口前要先在电脑上安装CH340驱动&#xff1f; 中断的作用&#xff1f; 环形buffer的作用&#xff1f; static和valitate的作用 三种编程方式简介 也可以通过DMA方式减小CPU资源的消耗 直接把数据在SRAM内存和UART模块进行传输 &#xff0c;流程&#xff1a; …

玩家自行定制内存将古老的386 PC内存升级到64MB容量

比尔盖茨曾说&#xff1a;“无论对谁来说&#xff0c;640K内存都足够了。” 如果你是一个还停留在 30 针 SIMM 时代的老式电脑爱好者&#xff0c;那么你的内存升级选择是相当有限的。不过&#xff0c;YouTube 上的一个频道已经展示了如何将古老的 386 系统内存升级到令人"…

Linux内核编译安装 - Deepin,Debian系

为什么要自己编译内核 优点 定制化&#xff1a;你可以根据自己的硬件和需求配置内核&#xff0c;去掉不必要的模块&#xff0c;优化性能。性能优化&#xff1a;移除不需要的驱动程序和特性&#xff0c;减小内核体积&#xff0c;提高系统性能。最新特性和修复&#xff1a;获取…

【Leetcode】最小数字游戏

你有一个下标从 0 开始、长度为 偶数 的整数数组 nums &#xff0c;同时还有一个空数组 arr 。Alice 和 Bob 决定玩一个游戏&#xff0c;游戏中每一轮 Alice 和 Bob 都会各自执行一次操作。游戏规则如下&#xff1a; 每一轮&#xff0c;Alice 先从 nums 中移除一个 最小 元素&…

【Linux】System V消息队列 System V信号量

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 如果文章对…

Spark调度底层执行原理详解(第35天)

系列文章目录 一、Spark应用程序启动与资源申请 二、DAG&#xff08;有向无环图&#xff09;的构建与划分 三、Task的生成与调度 四、Task的执行与结果返回 五、监控与容错 六、优化策略 文章目录 系列文章目录前言一、Spark应用程序启动与资源申请1. SparkContext的创建2. 资…

Python | Leetcode Python题解之第233题数字1的个数

题目&#xff1a; 题解&#xff1a; class Solution:def countDigitOne(self, n: int) -> int:# mulk 表示 10^k# 在下面的代码中&#xff0c;可以发现 k 并没有被直接使用到&#xff08;都是使用 10^k&#xff09;# 但为了让代码看起来更加直观&#xff0c;这里保留了 kk,…

Excel第31享:基于left函数的截取式数据裂变

1、需求描述 如下图所示&#xff0c;在“Excel第30享”中统计2022年YTD各个人员的“上班工时&#xff08;a2&#xff09;”&#xff0c;需要基于工时明细表里的“日期”字段建立辅助列&#xff0c;生成“年份”字段&#xff0c;本文说明“年份”字段是怎么裂变而来的。 下图为…

图像处理:使用 OpenCV-Python 卡通化你的图像(2)

一、说明 在图像处理领域&#xff0c;将图像卡通化是一种新趋势。人们使用不同的应用程序将他们的图像转换为卡通图像。如今&#xff0c;玩弄图像是许多人的爱好。人们通常会点击图片并添加滤镜或使用不同的东西自定义图像并将其发布到社交媒体上。但我们是程序员&#xff0c;…

godis源码分析——Redis协议解析器

前言 redis这个目录下的所有代码就是为了一个事情&#xff0c;就是适配redis。 流程 redis下的基本流程 源码 在redis/client/client.go 主要是客户端处理 package clientconst (created iotarunningclosed )type B struct {data chan stringticker *time.Ticker }// …

Docker安装RabbitMQ(带web管理端)

1.拉取带web管理的镜像 可以拉取rabbitmq对应版本的web管理端&#xff0c;比如&#xff1a;rabbitmq:3.9.11-management&#xff0c;也可以直接拉取带web管理端的最新版本 rabbitmq:management. docker pull rabbitmq:3.9.11-management 注意&#xff1a;如果docker pull ra…

jenkins系列-06.harbor

https://github.com/goharbor/harbor/releases?page2 https://github.com/goharbor/harbor/releases/download/v2.3.4/harbor-offline-installer-v2.3.4.tgz harbor官网&#xff1a;https://goharbor.io/ 点击 Download now 链接&#xff0c;会自动跳转到上述github页面&am…

C++ | Leetcode C++题解之第233题数字1的个数

题目&#xff1a; 题解&#xff1a; class Solution { public:int countDigitOne(int n) {// mulk 表示 10^k// 在下面的代码中&#xff0c;可以发现 k 并没有被直接使用到&#xff08;都是使用 10^k&#xff09;// 但为了让代码看起来更加直观&#xff0c;这里保留了 klong l…

Redis系列命令更新--Redis哈希命令

一、设置密码验证&#xff1a; 使用文本编辑器&#xff0c;这里使用Notepad&#xff0c;打开Redis服务配置文件。 注意&#xff1a;不要找错了&#xff0c;通常为redis.windows-service.conf&#xff0c;而不是redis.windows.conf。后者是以非系统服务方式启动程序使用的配置…

《BASeg: Boundary aware semantic segmentation for autonomous driving》论文解读

期刊&#xff1a;Neural Networks | Journal | ScienceDirect.com by Elsevier 年份&#xff1a;2023 代码&#xff1a;https://github.com/Lature-Yang/BASeg 摘要 语义分割是自动驾驶领域街道理解任务的重要组成部分。现有的各种方法要么专注于通过聚合全局或多尺度上下文…

旷野之间20 - Google 研究的推测 RAG

为什么选择 RAG 新兴能力 直到最近&#xff0c;人们发现 LLM 具有新兴能力&#xff0c;即在与用户或任务交互过程中出现的意外功能。 这些功能的示例包括&#xff1a; 解决问题&#xff1a; LLM 可以利用其语言理解和推理能力&#xff0c;为未经过明确培训的任务提供富有洞…

python的字符串

字符串 简单操作 创建 利用 ‘ ’ 或 “ ” 将字符或数字包裹起来的都为字符串 a"你好" 格式化字符串 元组的字符格式化 字符串格式化函数 srt.format() f格式化 方法 split()//指定分割符经行分割 strip()//指定移除字符头尾的字符 join()//指定序列中的字符连接成新…

5、 测试

这里写目录标题 1、自动化测试简介&#xff08;1&#xff09;自动化测试是什么&#xff08;2&#xff09;为什么要写测试测试节约你的时间发现错误&#xff0c;预防错误测试使得代码更有吸引力 2、基础测试策略3、开始写第一个测试&#xff08;1&#xff09;首先得有个bug&…