ReentrantLock的源码实现和原理介绍

目录

一、概述

二、ReentrantLock的整体结构

三、ReentrantLock 和Synchronized相比

四、ReentrantLock 公平锁和非公平锁实现

4.1 ReentrantLock 源码解读

4.1.1 ReentrantLock 类源码解读

4.1.1.1 Lock接口

4.1.1.2 Sync抽象类

4.1.1.3 NonfairSync()和FairSync()

4.1.1.3.1 NonfairSync介绍

4.1.1.3.2 FairSync介绍

4.1.2 ReentrantLock涉及的AQS方法源码

4.1.2.1 acquire()

4.1.2.2 addWaite()

4.1.2.3 acquireQueued()

4.1.2.4 enq()

4.1.2.5 shouldParkAfterFailedAcquire()

4.2 公平锁、非公平锁流程

五、ReentrantLock 释放锁实现

5.1 释放锁源码

5.1.1 release()

5.1.2 unparkSuccessor()

5.2 释放锁流程


一、概述

ReentrantLock是一种基于AQS(抽象队列同步器)框架的应用实现,是Java提供的强大且灵活的可重入锁,支持公平和非公平特性。是JDK中一种线程并发访问的同步手段,它提供了与synchronized关键字相似的功能,但具有更多的灵活性和扩展性。

二、ReentrantLock的整体结构

ReentrantLock 实现了Lock通用接口,通过Sync继承了AQS特性,Syns下有两个实现,NonfairSyns和FairSync,分别实现的是非公平锁和公平锁功能,它的整体结构如下图所示:

三、ReentrantLock 和Synchronized相比

  • 在功能实现和原理上

首先Synchronized 实现的是公平锁,是基于对象锁实现的并发编程同步关键字;而ReentrantLock是lock接口实现,基于AQS+CAS实现,支持公平锁和非公平锁,相比Synchronized,lock锁提供的功能更完善,lock可以使用tryLock指定等待锁的时间;lock锁还提供了lockInterruptibly允许线程在获取锁的期间被中断。

  • 在使用上

Synchronized 是Java关键字,可以作用在方法上,也可以作用在代码块上,实现简单;对于并发编程掌握不好的编程人员来说比较友好。

ReentrantLock 的使用需要自己先初始化ReentrantLock,然后手动调用它的锁方法对同步代码加锁,使用完成后,需要在finally中调用释放锁方法释放。对于并发编程掌握不好的编程人员来说使用成本较高。而且可能极容易出错。

四、ReentrantLock 公平锁和非公平锁实现

4.1 ReentrantLock 源码解读

4.1.1 ReentrantLock 类源码解读

4.1.1.1 Lock接口

首先ReentrantLock实现了Lock接口,Lock接口是Java中对锁操作行为的统一规范,接口的定义如下:

public interface Lock {// 获取锁void lock();// 获取锁--支持响应中断void lockInterruptibly() throws InterruptedException;//尝试加锁,返回是否成功状态boolean tryLock();// 尝试加锁,返回是否成功状态,支持指定加锁时间,超时响应中断boolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 释放锁void unlock();// 条件变量Condition newCondition();
}
4.1.1.2 Sync抽象类

Sync继承了AbstractQueuedSynchronizer抽象接口,定义了抽象的lock方法,该方法需要子类自行实现,定义了一个nonfairTryAcquire非公平锁方法,定义了一个释放锁的tryRelease方法,它是ReentrantLock的核心类,具体源码和解释如下:

 // 获取锁,需要子类去实现abstract void lock();// 非公平锁获取state资源final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取当前状态int c = getState();if (c == 0) { // state等于0,代表可加锁// 使用cas锁,尝试将state修改为acquires,acquires等于1if (compareAndSetState(0, acquires)) {// 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程setExclusiveOwnerThread(current);// 返回true状态return true;}}else if (current == getExclusiveOwnerThread()) { // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数// state+1,累加重入次数int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个setState(nextc);// 返回true,表示成功return true;}// 返回false,表示失败return false;}

  // 释放锁protected final boolean tryRelease(int releases) {// 获取state状态值,然后减去releases,此处releases为1int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果持有锁线程不是当前线程,抛出异常。throw new IllegalMonitorStateException();// 返回的状态值,默认为falseboolean free = false;if (c == 0) { // 如果state-1后等于0,表示释放成功// 设置返回状态为truefree = true;// 清空持有锁线程setExclusiveOwnerThread(null);}// 如果state-1后不等于0,说明当前持有锁线程是重入锁状态,需要设置相应的释放次数setState(c);return free;}
4.1.1.3 NonfairSync()和FairSync()

Lock接口定义的函数不多,接下来ReentrantLock要去实现这些函数,遵循着解耦可扩展设计,ReentrantLock内部定义了专门的组件Sync, Sync继承AbstractQueuedSynchronizer提供释放资源的实现,NonfairSync 和 FairSync是基于Sync扩展的子类,他们分别是ReentrantLock的非公平模式与公平模式,它们作为Lock接口功能的基本实现。

4.1.1.3.1 NonfairSync介绍

在 ReentrantLock中支持两种获取锁的策略,分别是非公平策略与公平策略,NonfairSync 就是非公平策略。

此时大家会有问,什么是非公平策略?

在说非公平策略前,先简单的说下AQS(AbstractQueuedSynchronizer)流程,AQS为加锁和解锁过程提供了统一的模板函数,加锁与解锁的模板流程是,获取锁失败的线程,会进入CLH队列阻塞,其他线程解锁会唤醒CLH队列线程,如下图所示(简化流程):

接下来我们来解读下源码,看下NonfairSync是如何实现的

源码:

/*** 非公平锁实现*/static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;// 获取锁final void lock() {if (compareAndSetState(0, 1)) // 使用cas设置state状态为1,如果成功,代表加锁成功// 获取锁成功,设置当前持有锁的线程为当前线程setExclusiveOwnerThread(Thread.currentThread());else// 获取锁失败,执行AQS获取锁的模板方法流程acquire(1);}// 获取锁,使用的Sync提供的nonfairTryAcquire方法protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}

从源码我们可以知道,NonfairSync继承Sync,然后实现了lock()函数,lock()函数也非常简单,使用CAS设置状态值state为1,如果成功代表获取锁成功,否则执行AQS的acquire()函数( 获取锁模板 );另外NonfairSync还实现了AQS留给子类实现的tryAcquire()函数( 获取资源 ),函数直接使用Sync提供的nonfairTryAcquire()函数来实现tryAcquire(),最后子类实现的tryAcquire()函数在AQS的 acquire函数中被使用。

是不是有点绕?画个图给大家一起缕一缕:

首先 AQS 的acquire()函数是获取锁的流程模板,模板流程会先执行 tryAcquire()函数获取资源,tryAcquire()函数要子类实现,NonfairSync作为子类,实现了tryAcquire()函数,具体实现是调用了 Sync的 nonfairTryAcquire()函数。

我们接着看下nonfairTryAcquire() 的源码是如何实现的:

// 非公平锁获取state资源final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取当前状态int c = getState();if (c == 0) { // state等于0,代表可加锁// 使用cas锁,尝试将state修改为acquires,acquires等于1if (compareAndSetState(0, acquires)) {// 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程setExclusiveOwnerThread(current);// 返回true状态return true;}}else if (current == getExclusiveOwnerThread()) { // 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数// state+1,累加重入次数int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");// 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个setState(nextc);// 返回true,表示成功return true;}// 返回false,表示失败return false;}

对上述代码逻辑我们做个简单的概括,当前线程查看资源是否可获取:

  • 可获取,尝试使用CAS设置state为 1,CAS成功代表获取资源成功,否则获取资源失败
  • 不可获取,判断当线程是不是持有锁的线程,如果是,state重入计数,获取资源成功,否则获取资源失败

用下图说明一下流程:

4.1.1.3.2 FairSync介绍

有非公平策略,就有公平策略,FairSync 就是公平策略。

所谓公平策略就是,严格按照 CLH 队列顺序获取锁,线程释放锁时,会唤醒 CLH 队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH队列的线程参与竞争,为了保证公平,一定会让CLH队列线程竞争成功,如果非CLH队列线程一直占用时间片,那就一直失败( 构建成节点插入到CLH队尾,由AQS模板流程执行 ),直到时间片轮到CLH队列线程为止,所以公平策略的性能会更差。

接下来我们来看下FairSync 公平锁的源码和解析

源码:


// 公平锁static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;// 获取锁final void lock() {// 执行AQS获取锁的模板方法流程acquire(1);}// 获取锁protected final boolean tryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取当前state状态int c = getState();if (c == 0) { // state等于0,代表可加锁// hasQueuedPredecessors 判断当前线程是不是CLH队列中唤醒的线程// 使用cas锁,尝试将state修改为acquires,acquires等于1if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {// 修改state成功,代表加锁成功,设置当前持有锁的线程为当前线程setExclusiveOwnerThread(current);return true;}}// 如果当前state状态值!=0,且当前线程为持有锁线程,则state值+1,累加重入次数else if (current == getExclusiveOwnerThread()) {// state+1,累加重入次数int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 设置state状态(就是重入次数累加),此处不需要cas,因为持有锁的当前线程只有一个setState(nextc);return true;}return false;}}

其实从上面的源码中我们不难发现 FairSync 的流程与 NonfairSync 基本一致,唯一的区别就是在 CAS执行前,多了一步调用hasQueuedPredecessors()函数,这一步就是判断当前线程是不是 CLH 队列被唤醒的线程,如果是就执行CAS操作,否则获取资源失败,具体流程图如下所示:

4.1.2 ReentrantLock涉及的AQS方法源码

4.1.2.1 acquire()

acquire是一个业务方法,里面并没有实际的业务处理,都是在调用其他方法,首先执行tryAcquire()函数,这个函数其实最终调用的是NonfairSync和FairSync里面的tryAcquire() 函数;在执行加锁不成功后,会将当前线程封装成Node节点加入到CLH队列中,具体源码和解析如下:

// 核心acquire     arg = 1
public final void acquire(int arg) {//1. 调用tryAcquire方法:尝试获取锁资源(非公平、公平),拿到锁资源,返回true,直接结束方法。 没有拿到锁资源,//   需要执行&&后面的方法//2. 当没有获取锁资源后,会先调用addWaiter:会将没有获取到锁资源的线程封装为Node对象,//   并且插入到AQS的队列的末尾,并且作为tail//3. 继续调用acquireQueued方法,查看当前排队的Node是否在队列的前面,如果在前面(head的next),尝试获取锁资源//   如果没在前面,尝试将线程挂起,阻塞起来!if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
4.1.2.2 addWaite()

addWaite()函数,主要是将当前获取锁失败的线程,封装成功Node对象节点,加入到CLH队列中,具体源码和解析如下:

// 将当前线程封装为Node对象,并且插入到AQS队列的末尾private Node addWaiter(Node mode) {// 将当前线程封装为Node对象,mode为null,代表互斥锁Node node = new Node(Thread.currentThread(), mode);// pred是tail节点Node pred = tail;if (pred != null) { // 如果pred不为null,有线程正在排队// 将当前节点的prev,指定tail尾节点node.prev = pred;// 以CAS的方式,将当前节点变为tail节点if (compareAndSetTail(pred, node)) {// 之前的tail的next指向当前节点pred.next = node;return node;}}// 添加的流程为,  自己prev指向、tail指向自己、前节点next指向我// 如果上述方式,CAS操作失败,导致加入到AQS末尾失败,如果失败,就基于enq的方式添加到AQS队列enq(node);return node;}
4.1.2.3 acquireQueued()

 // 查看当前排队的Node是否是head的next,// 如果是,尝试获取锁资源,// 如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// 拿到上一个节点final Node p = node.predecessor();//p == head 说明当前节点是head的next// tryAcquire 竞争锁资源,成功:true,失败:falseif (p == head && tryAcquire(arg)) {// 进来说明拿到锁资源成功// 将当前节点置位head,thread和prev属性置位nullsetHead(node);p.next = null; // help GC// 设置获取锁资源成功failed = false;// 不管线程中断。return interrupted;}// 如果不是或者获取锁资源失败,尝试将线程挂起// shouldParkAfterFailedAcquire 当前节点的上一个节点的状态正常!// parkAndCheckInterrupt 挂起线程if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}
4.1.2.4 enq()

private Node enq(final Node node) {for (;;) {// 拿到tailNode t = tail;if (t == null) { // 如果tail为null,说明当前没有Node在队列中if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;// 以CAS的方式,将当前节点变为tail节点if (compareAndSetTail(t, node)) {// 之前的tail的next指向当前节点t.next = node;return t;}}}}
4.1.2.5 shouldParkAfterFailedAcquire()

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//拿到上一个节点的状态int ws = pred.waitStatus;// 如果上一个节点为 -1if (ws == Node.SIGNAL)// 返回true,挂起线程return true;if (ws > 0) { // 如果上一个节点是取消状态// 循环往前找,找到一个状态小于等于0的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 将小于等于0的节点状态该为-1compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

4.2 公平锁、非公平锁流程

经过上面的源码解读,我们基本上对ReentrantLock的公平锁、非公平锁有了一个而大概的了解,为了使我们理解更深刻,我们这里总结一下他们的加锁流程。

公平锁调用流程:

非公平锁调用流程:

公平锁、非公平锁整体加锁流程:

五、ReentrantLock 释放锁实现

5.1 释放锁源码

5.1.1 release()

public final boolean release(int arg) {// 核心的释放锁资源方法if (tryRelease(arg)) {Node h = head;// 释放锁资源释放干净了。  (state == 0)if (h != null && h.waitStatus != 0)// 唤醒线程unparkSuccessor(h);return true;}// 释放锁成功,但是state != 0return false;}

5.1.2 unparkSuccessor()

// 唤醒节点private void unparkSuccessor(Node node) {// 拿到头节点状态int ws = node.waitStatus;// 如果头节点状态小于0,换为0if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 拿到当前节点的nextNode s = node.next;// 如果s == null ,或者s的状态为1if (s == null || s.waitStatus > 0) {// next节点不需要唤醒,需要唤醒next的nexts = null;// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}// 经过循环的获取,如果拿到状态正常的节点,并且不为nullif (s != null)// 唤醒线程LockSupport.unpark(s.thread);}

为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?

因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是能上一个节点的next指针,指向当前Node。

如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~

如果从后往前找,肯定可以找到全部的节点。

5.2 释放锁流程

下面用一张图说明一下释放锁的流程:

今天ReentrantLock的源码实现和原理介绍的相关内容就分享到这里,如果帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

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

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

相关文章

EasyCVR视频技术:城市电力抢险的“千里眼”,助力抢险可视化

随着城市化进程的加速和电力需求的不断增长&#xff0c;电力系统的稳定运行对于城市的正常运转至关重要。然而&#xff0c;自然灾害、设备故障等因素常常导致电力中断&#xff0c;给城市居民的生活和企业的生产带来严重影响。在这种情况下&#xff0c;快速、高效的电力抢险工作…

产品介绍|九芯语音芯片的特点与应用市场

随着物联网与智能家居的普及&#xff0c;越来越多的电子产品有了语音播报的需求。九芯语音芯片集成了语音识别和语音合成技术&#xff0c;能够准确地捕捉并解析人类的语言&#xff0c;同时以清晰、自然的语调进行回应&#xff0c;为各类智能设备注入了强大的语言交互能力。 特点…

OSI 七层模型与五层模型

OSI&#xff08;开放系统互连&#xff09;七层模型和五层模型是描述计算机网络协议的两种不同层次划分方法。两者用于帮助理解和设计网络协议&#xff0c;但它们在层次划分上有所不同。

使用Elasticsearch Python SDK 查询Easysearch

随着数据分析需求的不断增长&#xff0c;能够高效地查询和分析大数据集变得越来越重要。Elasticsearch作为一种强大的分布式搜索和分析引擎&#xff0c;被广泛应用于各种场景。Easyearch 支持原生 Elasticsearch 的 DSL 查询语法&#xff0c;确保原业务代码无需调整即可无缝迁移…

优化校园设施维护,故障类型功能全解析

在智慧校园的日常运作中&#xff0c;报修管理系统的故障类型功能扮演着至关重要的角色。它不仅简化了设备维修的流程&#xff0c;还极大地提升了校园设施的维护效率。该功能的核心在于&#xff0c;它允许系统管理员创建、编辑和删除一系列故障类型&#xff0c;涵盖从网络连接问…

vue实现动态图片(gif)

目录 1. 背景 2. 分析 3. 代码实现 1. 背景 最近在项目中发现一个有意思的小需求&#xff0c;鼠标移入一个盒子里&#xff0c;然后盒子里的图就开始动起来&#xff0c;就像一个gif一样&#xff0c;然后鼠标移出&#xff0c;再按照原来的变化变回去&#xff0c;就像变形金刚…

QT--控件篇二

一、文本框 1. QLineEdit 文本框通常使用QLineEdit和QTextEdit这两个类来实现。 QLineEdit&#xff1a;用于单行文本输入。QTextEdit&#xff1a;用于多行文本输入&#xff0c;可以包含丰富的文本格式。 用setText(QString txt);设置默认的显示内容&#xff0c;用QString tex…

【NOI】C++数据结构入门之一维数组(一)数组基础

文章目录 前言一、概念1.导入2.数组2.1 数组的创建2.2 数组的使用 二、例题讲解问题&#xff1a;1423 - 考试成绩的简单统计问题&#xff1a;1153 - 查找“支撑数”问题&#xff1a;1156 - 排除异形基因问题&#xff1a;1155 - 找找谁的身高超过全家的平均身高问题&#xff1a;…

计算机网络生成树协议介绍与实践

生成树协议 1.环路 二层环路&#xff1a;数据链路层&#xff0c;交换机&#xff08;二层设备&#xff09;通过线路连接环状。即物理成环并且没有开启防环协议。 危害&#xff1a;广播风暴&#xff1a;交换机将未知帧广播&#xff0c;收到后的交换机继续广播&#xff0c;不断…

全国地级市-产业升级、高级化、合理化数据集(1999-2022年)

数据年份&#xff1a;1999-2022年 数据范围&#xff1a;地级市以上城市 数据来源&#xff1a;中国城市统计NJ 数据整理&#xff1a;内含原始版本、线性插值版本、ARIMA填补版本 数据说明&#xff1a;参考干春晖&#xff08;2011&#xff09;《经济研究》的文章 &#xff0c…

数据结构(单链表(1))

前言 线性表中有着许多的结构&#xff0c;如顺序表和链表。而单链表则是链表的最基础的一种形式&#xff0c;下面就让我们对其做一个了解。 概念 概念&#xff1a;链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次…

ENSP中VLAN的设置

VLAN的详细介绍 VLAN&#xff08;Virtual Local Area Network&#xff09;即虚拟局域网&#xff0c;是一种将一个物理的局域网在逻辑上划分成多个广播域的技术。 以下是关于 VLAN 的一些详细介绍&#xff1a; 一、基本概念 1. 作用&#xff1a; - 隔离广播域&#xff1a…

Notebook 在复现数据科学研究成果中的丝滑使用

对于数据科学和 AI 科研人员而言&#xff0c;研究成果的复现至关重要。成果复现既是一种研究算法的方式&#xff0c;也有助于科研人员找到研究的新途径。 IDP 中提供自研 notebook 交互式编程环境&#xff0c;它非常适合做数据分析与代码展示&#xff0c;主要功能包括&#xf…

java基础概念01-注释、关键字、字面量、变量

一、注释 注释内容不会参与编译和运行&#xff0c;仅仅是对代码的解释说明。 1-1、注释的三种类型 1、单行注释&#xff1a;//…… // 这是单行注释 2、多行注释&#xff1a;/*…….*/ /* 这是一个 多行注释 */ 3、文档注释 特殊的多行注释&#xff0c;以/**开头&#xf…

【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力

初阶数据结构相关知识点可以通过点击以下链接进行学习一起加油&#xff01;时间与空间复杂度的深度剖析深入解析顺序表:探索底层逻辑深入解析单链表:探索底层逻辑深入解析带头双向循环链表:探索底层逻辑深入解析栈:探索底层逻辑深入解析队列:探索底层逻辑深入解析循环队列:探索…

Qt类 | QObject类详解

文章目录 一、QObject类介绍二、Properties&#xff08;属性&#xff09;三、Public Functions&#xff08;公共函数&#xff09;四、Public Slots&#xff08;公共槽函数&#xff09;五、Signals&#xff08;信号&#xff09;六、Static Public Members&#xff08;静态公共成…

react Ant Design 动态表头添加操作列

模拟后端返回的表头、列表数据 注意&#xff1a;我们要在表头数据中添加一个 render 函数&#xff0c;里面就是你操作列的内容&#xff0c;value是你数据列表每行的对象 &#xff0c;item 是你表头的对象 页面中去处理这个两个数组 dataList.forEach((item, index) > {item.…

昇思25天学习打卡营第24天 | RNN实现情感分类

概述 情感分类是自然语言处理中的经典任务&#xff0c;是典型的分类问题。本节使用MindSpore实现一个基于RNN网络的情感分类模型&#xff0c;实现如下的效果&#xff1a; 输入: This film is terrible 正确标签: Negative 预测标签: Negative输入: This film is great 正确标…

dmasmtool工具详细用法

DMASMTOOL 是 DMASM 文件系统管理工具&#xff0c;提供了一套类 Linux 文件操作命令&#xff0c;用于管理 ASM 文件&#xff0c;是管理、维护 DMASM 的好帮手。DMASMTOOL 工具使用 DMASMAPI 连接到 DMASMSVR&#xff0c;并调用相应的 DMASMAPI 函数&#xff0c;实现创建、拷贝、…

C1W2.LAB.Visualizing Naive Bayes

理论课&#xff1a;C1W2.Sentiment Analysis with Nave Bayes 文章目录 导入包Calculate the likelihoods for each tweetUsing Confidence Ellipses to interpret Nave Bayes 理论课&#xff1a; C1W2.Sentiment Analysis with Nave Bayes 导入包 在下面的练习中&#xff0…