面试官:能说说 Synchronized 吗?

答:Synchronized 是Java的一个关键字,使用于多线程并发环境下,可以用来修饰实例对象和类对象,确保在同一时刻只有一个线程可以访问被Synchronized修饰的对象,并且能确保线程间的共享变量及时可见性,还可以避免重排序,从而保证线程安全。

面试官:你背书呢?可以再具体的深入一点吗?

答:行!

1. 前言

相信很多 Android程序员跟我一样,最开始接触到 Synchronized 这个关键字是在创建单例的时候,如:

public class SingleTon {private static volatile SingleTon instance;public static SingleTon getInstance() {if (instance == null) {//同步锁,保证同一时刻只有一个线程进入该代码块。synchronized (SingleTon.class) {if (instance == null) {instance = new SingleTon();}}}return instance;}}

这里前辈们告诉我们,这叫同步锁,保证同一时刻只有一个线程进入同步锁修饰的代码块,从而保证在多线程的环境下也只会创建一个 SingleTon 实例,达到单例效果。

那除了单例,Synchronized 的其他使用方法及其原理,你有额外了解过吗?今天就让我们来重头学习一遍吧!

2. Synchronized 的使用方法

从Java语法上来说,它有三种使用方法,分别是:

  1. 修饰普通方法
  2. 修饰静态方法
  3. 修饰代码块

再来看一段代码:

public class SynchronizedTestRunnable implements Runnable {@Overridepublic void run() {a();b();}public void a() {System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());}public void b() {System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());}public static String getCurrentTime() {String dateFormat = "yyyy-MM-dd hh:mm:ss";SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);Calendar calendar = Calendar.getInstance();calendar.setTimeInMillis(System.currentTimeMillis());return simpleDateFormat.format(calendar.getTime());}public static void main(String[] args) {SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();new Thread(synchronizedTestRunnable).start();new Thread(synchronizedTestRunnable).start();}}

其执行结果为:

Thread-1 a start on 2020-11-24 11:49:04
Thread-0 a start on 2020-11-24 11:49:04
Thread-0 a end 2020-11-24 11:49:07
Thread-1 a end 2020-11-24 11:49:07
Thread-0 b start 2020-11-24 11:49:07
Thread-1 b start 2020-11-24 11:49:07
Thread-0 b end 2020-11-24 11:49:08
Thread-1 b end 2020-11-24 11:49:08

根据执行结果可以看到两个线程会同时执行同一个 runnable 中的方法 a() 与 b(),并且不存在顺序关系,接下来我们试试加上 Synchronized 关键字。

2.1 修饰普通方法

给普通方法 a() 与 b() 分别都加上 Synchronized 关键字修饰,如下:

public class SynchronizedTestRunnable implements Runnable {@Overridepublic void run() {a();b();}public synchronized void a() {System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());}public synchronized void b() {System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());}public static String getCurrentTime() {String dateFormat = "yyyy-MM-dd hh:mm:ss";SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);Calendar calendar = Calendar.getInstance();calendar.setTimeInMillis(System.currentTimeMillis());return simpleDateFormat.format(calendar.getTime());}public static void main(String[] args) {SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();new Thread(synchronizedTestRunnable).start();new Thread(synchronizedTestRunnable).start();}}

其执行结果为:

Thread-0 a start on 2020-11-24 11:50:10
Thread-0 a end 2020-11-24 11:50:13
Thread-0 b start 2020-11-24 11:50:13
Thread-0 b end 2020-11-24 11:50:14
Thread-1 a start on 2020-11-24 11:50:14
Thread-1 a end 2020-11-24 11:50:17
Thread-1 b start 2020-11-24 11:50:17
Thread-1 b end 2020-11-24 11:50:18

两个线程按顺序同步执行同一个 runnable 中的方法 a() 与 b(),达到同步锁效果。

思考一下:那要是两个线程分别执行两个 runnable 呢?如:

public static void main(String[] args) {SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();SynchronizedTestRunnable synchronizedTestRunnable2 = new SynchronizedTestRunnable();new Thread(synchronizedTestRunnable).start();new Thread(synchronizedTestRunnable2).start();
}

其执行结果为:

Thread-1 a start on 2020-11-24 11:51:02
Thread-0 a start on 2020-11-24 11:51:02
Thread-1 a end 2020-11-24 11:51:05
Thread-0 a end 2020-11-24 11:51:05
Thread-1 b start 2020-11-24 11:51:05
Thread-0 b start 2020-11-24 11:51:05
Thread-0 b end 2020-11-24 11:51:06
Thread-1 b end 2020-11-24 11:51:06

根据结果来看,虽然我们为方法 a() 与 方法 b() 都加上了 Synchronized 修饰,但是由于 Thread-0 与 Thread-1 执行的是两个runnable,所以两个线程还是同时并发执行了方法a() 与 方法 b(),这是为什么呢?思考一下,后面解释。

2.2 修饰静态方法

在 2.1 修饰普通方法 中我们用 Synchronized 修饰普通方法,但是我们发现,当我们用两个线程分别执行两个 runnable时,同步锁失效了,两个线程还是同时并发执行。
现在,我们稍微修改一下上述代码,将方法a() 与方法 b() 修改成静态方法,如下:

public class SynchronizedTestRunnable implements Runnable {@Overridepublic void run() {a();b();}public static synchronized void a() {System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());}public static synchronized void b() {System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());}public static String getCurrentTime() {String dateFormat = "yyyy-MM-dd hh:mm:ss";SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);Calendar calendar = Calendar.getInstance();calendar.setTimeInMillis(System.currentTimeMillis());return simpleDateFormat.format(calendar.getTime());}public static void main(String[] args) {SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();SynchronizedTestRunnable synchronizedTestRunnable2 = new SynchronizedTestRunnable();new Thread(synchronizedTestRunnable).start();new Thread(synchronizedTestRunnable2).start();}}

其执行结果为:

Thread-0 a start on 2020-11-24 11:51:46
Thread-0 a end 2020-11-24 11:51:49
Thread-0 b start 2020-11-24 11:51:49
Thread-0 b end 2020-11-24 11:51:50
Thread-1 a start on 2020-11-24 11:51:50
Thread-1 a end 2020-11-24 11:51:53
Thread-1 b start 2020-11-24 11:51:53
Thread-1 b end 2020-11-24 11:51:54

同样是两个线程执行两个runnble,但是与 2.1 修饰普通方法 的结果不同的是,两个线程变成了同步顺序执行,这是为什么呢?就加了两个 static 关键字,思考一下,往后看,后面解释。

2.3 修饰代码块

前面介绍的都是用 Synchronized 关键字来修饰方法,但是很多时候我们只需要同步一小块代码,而不需要同步整个方法,从而减小系统开销。现在,我们接着再修改一下代码,就将方法 a() 的 Thread.sleep(3000) 锁住,方法 b() 还是普通方法,如下:

public class SynchronizedTestRunnable implements Runnable {@Overridepublic void run() {a();b();}public void a() {System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());synchronized (this) {try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());}public void b() {System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());}public static String getCurrentTime() {String dateFormat = "yyyy-MM-dd hh:mm:ss";SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);Calendar calendar = Calendar.getInstance();calendar.setTimeInMillis(System.currentTimeMillis());return simpleDateFormat.format(calendar.getTime());}public static void main(String[] args) {SynchronizedTestRunnable synchronizedTestRunnable = new SynchronizedTestRunnable();new Thread(synchronizedTestRunnable).start();new Thread(synchronizedTestRunnable).start();}}

其执行结果为:

Thread-1 a start on 2020-11-24 11:52:35
Thread-0 a start on 2020-11-24 11:52:35
Thread-1 a end 2020-11-24 11:52:38
Thread-1 b start 2020-11-24 11:52:38
Thread-1 b end 2020-11-24 11:52:39
Thread-0 a end 2020-11-24 11:52:41
Thread-0 b start 2020-11-24 11:52:41
Thread-0 b end 2020-11-24 11:52:42

两个线程执行同一个runnable,Thread-0 与 Thread-1 同时进入方法 a(),但是由于锁的存在,Thread-0 与 Thread-1 存在竞争关系,这里 Thread-1 先获取到锁,所以会往下执行,而 Thread-0 则阻塞,直到 Thread-1 执行完方法 a(),Thread-0 才开始执行方法 a()。

2.4 总结

我们想要搞清楚这三个方法的区别,就需要知道它们本质上锁的到底是什么对象。比如 2.2 Synchronized 修饰静态方法 中,锁住的是类对象,所以在多线程中,尽管new了多个实例对象,但是本质上是属于同一个类对象,所以还是存在同步关系。而 2.1 Synchronized 修饰普通方法 中,锁住的是类的实例对象,所以在多线程中,如果多个线程执行同一个runnable,就存在同步关系,而如果new了多个实例对象,且线程间各自执行不同的runnable,线程之间就不存在同步关系了。

修饰方法:

a() 修饰普通方法,锁住类的实例对象 b() 修饰静态方法,锁住类对象

修饰代码块:

锁住当前类的实例对象 锁住当前类对象 锁住任意实例对象

3. Synchronized 原理分析

上面我们介绍了 Synchronized 的三种使用方法,分别是1. 修饰普通方法;2. 修饰静态方法;3. 修饰代码块。接下来为了更好的理解Synchronized 的工作原理,我们反编译一下这三种方法。

//1.修饰普通方法
public synchronized void a() {System.out.println(Thread.currentThread().getName() + " a start on " + getCurrentTime());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " a end " + getCurrentTime());
}//2.修饰静态方法
public static synchronized void b() {System.out.println(Thread.currentThread().getName() + " b start " + getCurrentTime());try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " b end " + getCurrentTime());
}//3.修饰代码块
public void c() {System.out.println(Thread.currentThread().getName() + " c start " + getCurrentTime());try {synchronized (this) {Thread.sleep(1000);}} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " c end " + getCurrentTime());
}

方法 a()、 b()、 c() 反编译的结果如下所示:

a() 修饰普通方法 b() 修饰静态方法 c() 修饰代码块

结果分析:(注意看上图我标红的地方)

  • 方法 c() : monitorenter 与 monitorexit 指令,monitorenter指令的作用是获取 monitor所有权,monitorexit指令的作用是释放 monitor所有权。monitorenter 与 monitorexit 指令都是成对出现的,但是注意仔细看上图 c() 的话,你会发现出现了一个 monitorenter 对应 2个 monitorexit,看到这里是不是内心有疑惑,不是刚说成对出现吗?打脸来的这么快?正常执行的情况下,23行 monitorenter 对应31行的 monitorexit,然后直接 goto 到40行,略过了37行的 monitorexit。注意:37行的 monitorexit 是在抛出异常的情况下执行的,正常情况下不执行,这样也就保证了始终都有一个 monitorexit 来对应 monitorenter,所以也就证实了他们都是成对出现的
  • 方法 a() 与 方法 b() :没有发现 monitorenter 与 monitorexit 指令,而是多出了 ACC_SYNCHRONIZED 标识符,JVM就是根据该标识符来判断是否需要实现方法同步。当方法调用时,调用指令会检查方法中是否含有 ACC_SYNCHRONIZED 标识符,如果有,则执行线程将获取 monitor所有权,成功获取到 monitor所有权之后才能往下执行方法体,方法体执行完后再释放 monitor所有权。并且在方法体执行期间,其他任何线程都无法再获得到同一个 monitor对象的所有权。 其实本质上与 monitorenter | monitorexit指令没有区别,只是是以一种隐式的方式来实现同步,无需通过字节码来完成,所以也被称为隐式同步

说了这么多获取 monitor所有权、释放 monitor所有权,那 monitor 又是什么呢
他被称为内置锁(intrinsic lock)或监视器锁(monitor lock),它其实是一种互斥锁,也就是同一时刻最多只有一个线程可以持有这种锁,当线程A想去获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放该锁,如果线程B永远不释放持有的该锁,那么线程A也将永远的等待或阻塞着。monitor lock 存在于 Java对象头中,获得 monitor lock 的唯一途径就是进入由这个锁(Synchronized)保护的同步代码块或方法中。

《Java并发编程实战》是这么介绍的:

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁和监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

总结一下:
我们用 Synchronized 修饰的同步方法中,是通过监视器锁来实现同步效果的,而这个监视器锁存在于 Java对象头中。

3.1 Java对象头

想要知道 Java对象头,我们就得先了解 Java对象是什么。

关于Java对象,在《深入理解Java虚拟机》中是这么介绍的:在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以分划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

这里我们主要关注对象头的 Mark Word,用于存储对象自身的运行时数据,如哈希码HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分的数据长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称为 Mark Word。
引用《深入理解Java虚拟机》中的插图,帮助理解,如下所示:

对象头的最后两位存储了锁的标志位,如00是轻量级锁,10位重量级锁。随着锁级别的不同,对象头里存储的内容不同,具体如上图所示。所以这里也验证了每个Java对象都可以做一个实现同步的锁

3.2 虚拟机底层原理

通过上面的学习,我们知道了用 Synchronized 修饰的同步方法,本质上是通过获取Java对象头中的监视器锁来实行同步的,接下来我们来看看底层虚拟机是通过怎样的方式来实现同步的(再来回忆一下,同一时刻只有一个线程可以进入到同步方法中)。

来看看底层虚拟机源码

ObjectMonitor() {_header       = NULL;_count        = 0; //用来记录当前线程获取该锁的次数_waiters      = 0, //等待线程数_recursions   = 0; //锁的重入次数_object       = NULL;_owner        = NULL; //表示持有ObjectMonitor对象的线程_WaitSet      = NULL; //线程队列:存放处于wait状态的线程_WaitSetLock  = 0 ;_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ; //线程队列:存放正在等待锁释放而处于block状态的线程_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0; //前一个持有者的线程ID}

引用一张 monitor 工作原理经典图

当多个线程同时访问同一同步代码时,先获取到 monitor 的线程会先成为 _owner,_count加一,而其他线程则进入 _EntryList 队列中,处于阻塞状态,直到当前线程 _owner 释放了 monitor(此时_count为0),这些处于 _EntryList 中阻塞的线程才会被唤醒然后去竞争 monitor,新竞争到 monitor 的线程就会成为新的 _owner。
获取到 monitor 的线程在调用 wait() 方法后,_owner 会释放 monitor,_count减一,该线程会加入到 _WaitSet 队列中,直到调用 notify()/notifyAll() 方法出队列,再次获取到 monitor。

_EntryList 与 _WaitList 的区别
注意:处于 _EntryList 队列中的线程是还没有进入到同步方法中的,而处于 _WaitList 队列的线程是已经进入到了同步方法中,但是由于某些条件(调用了wait()方法)暂时释放了 monitor,等待某些条件(调用notify()/notifyAll()方法)再次获取到 monitor。
这个问题也称之为锁阻塞与等待阻塞的区别,锁阻塞是被动地,还没进入到同步方法中,而等待阻塞是主动的,已经进入到了同步方法中,只是等待另一个获取到monitor锁的线程调用 notify() 唤醒。

上述回答中的 wait()/notify()/notifyAll() 方法也被称之为监控条件(Monitor Condition) ,它与 monitor 是相互关联的,所以你想使用监控条件必须先获取到 monitor,所以 wait()/notify()/notifyAll() 方法必须用在同步方法中(因为Synchronized修饰的同步方法中可以获取到 monitor),否则会抛出 IllegalMonitorStateException 异常。

进一步思考:那 _EntryList 与 _WaitList 里的线程会一起竞争monitor吗?还是说 _WaitList 里的线程会比 _EntryList 里的线程优先获取到 monitor 呢?
会公平竞争monitor
参考Does Java monitor’s wait set has a priority over entry set?

4. Q & A

由小伙伴提出的问题延伸,欢迎大家提出有疑问的地方,让我们共同进步呀 ~

4.1 类锁与对象锁有什么区别

想知道类锁与对象锁有什么区别,我们就要先知道类对象与类的实例对象有什么区别,因为他们四个的对应关系为:

类对象            --> 类锁
类的实例对象 --> 对象锁

我们在编写完 .java 文件后,JVM 是不能直接运行该文件的,需要先将 .java 文件编译成 .class 文件后,JVM 再把 class 文件加载到内存中,创建一个 Class对象,并且存在 JVM数据区中的方法区中,被所有线程所共享,这时候才可以使用这个 .java 文件,也就是这个类
这个Class对象,就是我们说的类对象,而我们刚刚说的类锁,就是这个类对象实现的。

总结一下:
一个Java类可以有很多的实例对象,但只有一个类对象,不管是类对象还是类的实例对象,都是 Java对象,所以类锁与对象锁其实都是内置锁,都是通过 Java对象头中的 Mark Word 中的标志位来实现的,其实现原理也是一样的。


OK,到这里是不是对 Synchronized 有了进一步的理解,其实还可以进阶 Synchronized 的锁优化,篇幅及能力关系,这里就不展开了。

参考文献:
《Java并发编程实战》
《深入理解Java虚拟机》
synchronized实现原理

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。
另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!

Android Synchronized 关键字学习相关推荐

  1. Android Volatile 关键字学习

    面试官:你平时是怎么创建单例的? 我:我一般用DCL双重检锁的方式来创建单例,然后为 instance 加上 volatile 修饰,防止 DCL 失效. 面试官:那你可以具体说说 volatile ...

  2. Java:这是一份全面 详细的 Synchronized关键字 学习指南

    前言 在Java中,有一个常被忽略 但 非常重要的关键字Synchronized 今天,我将详细讲解 Java关键字Synchronized的所有知识,希望你们会喜欢 目录 1. 定义 Java中的1 ...

  3. java synchronized关键字_Java:手把手教你全面学习神秘的Synchronized关键字

    前言 在Java中,有一个常被忽略 但 非常重要的关键字Synchronized 今天,我将详细讲解 Java关键字Synchronized的所有知识,希望你们会喜欢 目录 示意图 1. 定义 Jav ...

  4. Java多线程学习(二)synchronized关键字(1)

    转载请备注地址: https://blog.csdn.net/qq_34337272/article/details/79655194 Java多线程学习(二)将分为两篇文章介绍synchronize ...

  5. JAVE SE 学习day_09:sleep线程阻塞方法、守护线程、join协调线程同步方法、synchronized关键字解决多线程并发安全问题

    一.sleep线程阻塞方法 static void sleep(long ms) Thread提供的静态方法sleep可以让运行该方法的线程阻塞指定毫秒,超时后线程会自动回到RUNNABLE状态,等待 ...

  6. Java并发编程学习笔记——volatile与synchronized关键字原理及使用

    Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. 一.vo ...

  7. Java多线程闲聊(六):synchronized关键字

    Java多线程闲聊(六):synchronized关键字 前言 这篇文章我会在博客置顶,为什么呢?因为,三篇引用的文章写得太好了,我害怕后面找不到,看不到,然后忘了! 让我想想,感觉昨天的前言把最近肚 ...

  8. synchronized()_这篇文章带你彻底理解synchronized关键字

    Synchronized关键字一直是工作和面试中的重点.这篇文章准备彻彻底底的从基础使用到原理缺陷等各个方面来一个分析,这篇文章由于篇幅比较长,但是如果你有时间和耐心,相信会有一个比较大的收获,所以, ...

  9. android synchronized的使用

    今天,简单讲讲  synchronized 的使用. synchronized 是Java语言关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.sy ...

最新文章

  1. Print Model SQL
  2. pythonweb开发-Python Web开发
  3. android应用资源可以分为两大类,Android 应用资源(一)
  4. Tomcat解决HTTP GET中文乱码
  5. 迷宫(AHOI2016初中组T3)
  6. Spring的xml文件配置方式实现AOP
  7. 深入Java集合学习系列:HashSet的实现原理
  8. Confluence 6 嵌套用户组的示例
  9. Navicat的使用,连表查询,python代码操作sql语句
  10. linux服务器的性能分析与优化(十三)
  11. c语言调用子程序实例,C语言程序调用汇编语言子程序
  12. python科学计算库
  13. 根据.jgwx配准文件绘制并加载图层
  14. Java实现 LeetCode 273 整数转换英文表示
  15. Tanzu 学习系列之TKGm for vSphere 快速部署
  16. iOS打包静态库(完整篇)
  17. Linux学习第一节课
  18. XP系统安装打印机提示未安装打印机驱动程序,操作无法完成.
  19. matlab由方波转换为梯形波,matlab怎样将方波转换为二进制数据
  20. ro手游服务器维护公告,仙境传说ro手游9月26日5点至10点停服维护公告

热门文章

  1. JDBC保存和读取大文本数据类型
  2. 相机、imu 标定 简介
  3. GPT-4推理提升1750%!清华姚班校友提出全新ToT框架,让LLM反复思考
  4. 系统学习Python——2D绘图库Matplotlib:绘图函数matplotlib.pyplot.plot(plt.plot)
  5. 携手中兴软创 青云QingCloud助力传统农业向信息化转型
  6. 1.1python初入网络爬虫-网络连接和BeautifulSoup库的使用
  7. 【STM32】Keil5 LOAD按钮变灰的处理方法
  8. 高中计算机知识河南,河南省高二会考计算机试题和答案模拟的也行
  9. 信息时代的必修课:信息的作用(消除不确定性)
  10. Vue2进阶篇-组件间通信的6钟方式