【JUC系列-06】深入理解Semaphore底层原理和基本使用

news2024/12/23 5:48:43

JUC系列整体栏目


内容链接地址
【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429
【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786
【三】熟练掌握Atomic原子系列基本使用https://blog.csdn.net/zhenghuishengq/article/details/132543379
【四】精通Synchronized底层的实现原理https://blog.csdn.net/zhenghuishengq/article/details/132740980
【五】通过源码分析AQS和ReentrantLock的底层原理https://blog.csdn.net/zhenghuishengq/article/details/132857564
【六】深入理解Semaphore底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132908068

深入理解Semaphore的底层原理和基本使用

  • 一、深入理解Semaphore的底层原理和基本使用
    • 1,代码举例
    • 2,Semaphore底层源码剖析
      • 2.1,尝试获取锁
      • 2.2,结点获取锁失败入队
      • 2.3,Node结点阻塞
      • 2.4,Node结点唤醒
      • 2.5,结点出队以及传播
    • 3,总结

一、深入理解Semaphore的底层原理和基本使用

在上一篇中,讲解了AQS和ReentrantLock的底层原理和基本使用,除了这个Reentrant锁是AQS实现之外,还有很多线程协作的并发工具类也是通过这个AQS的底层来实现的,如CountDownLatch、Semaphore和CyclicBarrier 等,接下来要讲解的主角就是 Semaphore

在很多限流的工具类中,其底层实现都是采用这个Semaphore信号量来实现的,如sentinel等,其内部是通过PV操作来实现线程间的同步和互斥的。接下来先通过代码举一个例子,看看这个Semaphore信号量是如何使用的

在后续讲解源码时,一定得先看上一篇AQS的底层实现。

1,代码举例

首先先建议一个线程池工具类,线程池部分参数设置如下,假设这是一个io密集型的线程,因此设置最大线程数为空闲处理器的两倍(一个cpu对应两个处理器),队列为链表阻塞队列。

package com.zhs.study.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;

/**
 * 线程池工具
 * @author zhenghuisheng
 * @date : 2023/9/15
 */
public class ThreadPoolUtil {
    //日志级别(由高到低):fatal -> error -> warn -> info -> debug,低级别的会输出高级别的信息,高级别的不会输出低级别的信息
    private static final Logger log = LoggerFactory.getLogger(ThreadPoolUtil.class);
    //构建线程池
    public static ThreadPoolExecutor pool = null;

    //向线程池中添提交任务,无参数返回
    //判断核心线程数数量,阻塞队列,创建非核心线程数,拒绝策略
    public static void execute(Runnable runnable) {
        getThreadPool().execute(runnable);
    }

    //向线程池中添提交任务,将任务返回
    //判断核心线程数数量,阻塞队列,创建非核心线程数,拒绝策略
    public static <T> Future<?> submit(Runnable runnable) {
        //提交任务,并将任务返回
        Future<?> future = getThreadPool().submit(runnable);
        //将任务存储在hash表中
        return future;
    }


    /**
     * io密集型:最大核心线程数为2N,可以给cpu更好的轮换,
     *           核心线程数不超过2N即可,可以适当留点空间
     * cpu密集型:最大核心线程数为N或者N+1,N可以充分利用cpu资源,N加1是为了防止缺页造成cpu空闲,
     *           核心线程数不超过N+1即可
     * 使用线程池的时机:1,单个任务处理时间比较短 2,需要处理的任务数量很大
     */

    public static synchronized ThreadPoolExecutor getThreadPool() {
        if (pool == null) {
            //获取当前机器的cpu
            int cpuNum = Runtime.getRuntime().availableProcessors();
            log.info("当前机器的cpu的个数为:" + cpuNum);
            int maximumPoolSize = cpuNum * 2 ;
            pool = new ThreadPoolExecutor(
                    maximumPoolSize - 2,
                    maximumPoolSize,
                    5L,   //5s
                    TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(),  //链表无界队列
                    Executors.defaultThreadFactory(), //默认的线程工厂
                    new ThreadPoolExecutor.AbortPolicy());  //直接抛异常,默认异常
        }

        return pool;
    }
}

接下来定义一个线程任务类,里面设置这个Semaphore为全局对象,由于需要返回数据,因此可以实现这个Callable接口,如果不需要的话也可以直接实现这个Runnable接口。在call方法中,通过acquire去获取锁,通过release去释放锁,通过sleep睡眠3秒中模拟业务逻辑

/**
 * @author zhenghuisheng
 * @date : 2023/9/15
 */
@Data
public class AqsTask implements Callable, Serializable {

    private Integer x;
    private Integer y;
    //信号量
    Semaphore semaphore;

    public AqsTask(int x,int y,Semaphore semaphore){
        this.x = x;
        this.y = y;
        this.semaphore = semaphore;
    }

    @Override
    public Object call() throws Exception {
        semaphore.acquire();  	//获取锁
        if (semaphore.availablePermits() == 2) System.out.println("=============开始抢锁=============");
        System.out.println(Thread.currentThread().getName() + "拿到锁");
        Thread.sleep(3000);		//模拟业务逻辑
        semaphore.release();	//释放锁
        return x+y;				//返回数据
    }
}

接下来定义一个测试类,创建一个线程池和一个信号量锁,假设此时只允许三个线程在一段时间内同时获取锁

/**
 * @author zhenghuisheng
 * @date : 2023/9/15
 */
public class SemaphoreTest {
    //创建一个线程池
    static ThreadPoolExecutor threadPool = ThreadPoolUtil.getThreadPool();
    //信号量锁
    static Semaphore semaphore = new Semaphore(3);
    //主线程
    public static void main(String[] args) {
        //创建三个信号量
        for (int i = 0; i < 10; i++) {
            //创建一个任务
            AqsTask aqsTask = new AqsTask(i,i,semaphore);
            try {
                Future<?> future = threadPool.submit(aqsTask);
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

最终打印结果如下,就是每次只有三个线程可以在同一个时间段可以拿到锁。限流就是的底层原理就是这种,通过互斥+信号量的方式来实现底层获取锁的逻辑

11:41:50.319 [main] INFO com.zhs.study.util.ThreadPoolUtil - 当前机器的cpu的个数为:4
=============开始抢锁=============
pool-1-thread-1拿到锁
pool-1-thread-2拿到锁
pool-1-thread-3拿到锁
=============开始抢锁=============
pool-1-thread-4拿到锁
pool-1-thread-5拿到锁
pool-1-thread-6拿到锁
=============开始抢锁=============
...

2,Semaphore底层源码剖析

在这个 Semaphore 类中,是一个顶层类,没有实现其他的接口

public class Semaphore implements java.io.Serializable {...}

但是在这个类内部,是和reentrantLock一样,通过组合的方式将AQS整合进来,内部有一个Sync的静态内部类继承了AQS这个接口,并且该类有两个子类,实现了公平锁类和非公平锁类

![](https://img-blog.csdnimg.cn/10879217fbb84161b9789ee81b464c16.png)

接下来查看这个类的构造方法,permits表示的是信号量的个数,即一段时间内限流的个数,默认使用的是非公平锁,非公平锁可以减少Node结点的阻塞时间,其效率相对较高。也可以手动通过参数设置是公平锁还是非公平锁

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

通过以下代码,来对整个流程进行分析

Semaphore semaphore = new Semaphore(3);   	//初始化信号量
semaphore.acquire();						//抢锁
semaphore.release();						//释放锁

2.1,尝试获取锁

首先进入这个 acquire 方法,可以发现内部获取的是一把share的共享锁

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

接下来进入这个 acquireSharedInterruptibly 尝试获取锁的方法,内部主要会验证该线程是否被中断

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted()) //验证当前线程是否处于中断状态
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0) //尝试获取锁
        doAcquireSharedInterruptibly(arg);
}

如果当前线程不是中断状态,那么就会调用 tryAcquireShared 方法尝试获取这把共享锁

protected int tryAcquireShared(int acquires) {
    //非公平锁尝试获取锁
    return nonfairTryAcquireShared(acquires);
}

其获取锁的底层逻辑如下,这个state是AQS类中的属性,在Semaphore的同步监视器中,信号量的个数会等于同步监视器中state的个数,因此会通过getstate验证此时状态的个数,即可抢锁线程的个数。将小于0放在比较和交换的前面,这样在不满足条件是,可以减少比较和交换的次数,从而降低cpu的消耗

在这里插入图片描述

final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState(); //获取可以抢锁的线程的个数
        int remaining = available - acquires;  //外部传参为1,因此减1即可
        if (remaining < 0 ||
             //比较和交换状态
            compareAndSetState(available, remaining)) 
            return remaining;
    }
}

2.2,结点获取锁失败入队

上面这部分是获取锁的逻辑,将最后的状态返回,返回可以用的信号量个数小于0,那么就会执行这个 doAcquireSharedInterruptibly 方法,里面就是阻塞的逻辑

if (tryAcquireShared(arg) < 0) //尝试获取锁
        doAcquireSharedInterruptibly(arg);

接下来进入这个 doAcquireSharedInterruptibly 方法里面,首先会调用一个 addWaiter 方法,

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    //入队操作
    final Node node = addWaiter(Node.SHARED);
    ...
}

接下来查看这个 addWaiter 方法,如果组成CLH同步等待队列的Node双向链表不为空,则直接尾插法入队,如果双向链表是空的,则调用 enq 方法,创建一个双向链表,并且入队

private Node addWaiter(Node mode) {
    //创建一个结点,将当前线程作为参数
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;   //获取尾结点
    if (pred != null) {	//如果尾结点不为空
        node.prev = pred;    //则将新加入的结点的前驱指针指向尾结点
        if (compareAndSetTail(pred, node)) {	//将新加入的结点作为尾结点
            pred.next = node;	//之前的尾结点的后继指针指向现在加入的新结点
            return node;
        }
    }
    enq(node);  
    return node;
}

创建链表的enq方法的底层实现如下,首先会有一个for循环的自旋操作,保证线程一定可以入队。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //如果尾结点为空
        if (t == null) { // Must initialize
            //给头结点定义一个新的结点,自旋+cas实现,实现队列的初始化
            if (compareAndSetHead(new Node()))
                //此时头结点和尾结点是同一个结点
                tail = head;
        } else {
            //当前结点的前驱指针指向尾结点
            node.prev = t;
            //通过比较与交换
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

2.3,Node结点阻塞

在线程入队成功之后,又会通过一个自旋方法进行阻塞操作,这样可以保证结点一定可以阻塞成功。

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    try {
        //自旋
        for (;;) {
            final Node p = node.predecessor();
            //判断当前结点是不是头结点
            if (p == head) {
                //尝试获取共享锁,会和上面获取锁的逻辑一样
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //如果不是头结点,则会进行阻塞的操作
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    }
}

接下来查看这个park前的方法shouldParkAfterFailedAcquire ,里面有一个重要的修改结点状态的方法,将默认的状态修改成可被唤醒的状态

//将当前默认的状态0修改成可被唤醒的状态-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

设置完状态之后,会调用这个 parkAndCheckInterrupt 方法进行一个park阻塞和线程中断的操作,里面主要是通过这个LockSupport.park() 方法实现

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

2.4,Node结点唤醒

Semaphore信号量是通过调用release方法来实现结点的唤醒机制

public void release() {
    //释放共享锁
    sync.releaseShared(1);
}

在这个释放共享锁的方法中,首先会先去尝试释放这个共享锁

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

尝试释放共享锁的代码如下,主要是通过这个 tryReleaseShared 方法实现。内部是一个自旋操作,由于此时锁全被占用完,此时state的值为0,然后+1操作,让这个state变为1,随后通过比较与交换操作,将同步监视器中的state值修改成1,由于这个state用了volatile修饰,从而保证了可见性,那么就会让新的线程来抢锁

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();		//此时为0
        int next = current + releases;	//修改成1
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))	//比较和交换操作
            return true;
    }
}

虽然锁是可以被抢了,但是结点是被阻塞的,只有被唤醒才能来抢锁。因此继续看这个 doReleaseShared 方法,其逻辑如下。主要是会修改结点的状态,将被唤醒状态改成默认的初始状态,随后调用 unparkSuccessor 方法进行一个唤醒的操作

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;		//获取当前线程的结点状态
            if (ws == Node.SIGNAL) {	//如果是可唤醒状态
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))	//将状态改成默认状态0
                    continue;          
                unparkSuccessor(h);		//随后进行一个唤醒的操作
            }
            else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)  break;
    }
}

接下来查看这个 unparkSuccessor 唤醒的方法,里面最主要的就是这个 LockSupport.unpark 线程被唤醒的方法,node结点存储线程信息,从而通过操作结点来实现线程的阻塞和唤醒

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;  //获取将被唤醒的结点
    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) LockSupport.unpark(s.thread);
}

2.5,结点出队以及传播

由于上面通过了unpark唤醒了队列的线程,由于是队列结构,那么唤醒的是第一个header结点,并且此时同步状态器中的state的值为1,那么该结点就会去获取锁的操作,那么又会进入到 doAcquireSharedInterruptibly 方法里面,此时刚好Node结点是head,那么就会进入尝试获取锁的逻辑,由于state为1,那么返回值是大于0的,接下来就是进入一个重点方法 setHeadAndPropagate

private void doAcquireSharedInterruptibly(int arg)
    try {
        for (;;) {		//自旋
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);  //唤醒和传播
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
        }
    }
}

setHeadAndPropagate 方法顾名思义,就是设置头结点并且传播的意思。意思就是说如果当前队列中的结点被唤醒之后,会判断当前结点是不是头结点,如果是头结点就去尝试获取锁,如果获取锁成功的话,就会判断当前结点的下一个结点是不是共享结点,如果是就会尝试着去唤醒和回去呀锁,在锁资源充足的情况下,就能获取锁,如果锁资源不充分,此时线程处于唤醒状态,但是state处于0,就会等state大于0时去获取资源

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node); //将当前结点设置成头结点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next; 	//获取当前结点的下一个结点
        if (s == null || s.isShared())	//判断当前结点是否为共享结点
            doReleaseShared();	//如果是共享结点,则会唤醒该结点
    }
}

这样做的好处就是可以提前的去唤醒线程,从而提高并发量。但是如果唤醒了多个线程的话,在获取锁时也会存在cas的锁竞争问题,没抢到锁的线程即使被唤醒也会继续阻塞。

3,总结

Semaphore信号量的阻塞逻辑和ReentrantLock是差不多的,都是先cas抢锁,抢不到入队,都是通过双向联表实现的CLH同步等待队列,队列不存在则先创建,存在则Node结点直接入队,随后修改结点的状态为可被唤醒状态-1,随后调用park方法进行阻塞。

唤醒逻辑有部分不一样,首先都是先修改结点的状态为0初始状态,随后调用unpark进行唤醒的操作,但是Semaphore在唤醒时,除了自身结点被唤醒之外,还会判断下一个结点是不是共享结点,如果是也会被唤醒去获取锁,通过提前唤醒机制来提高并发量。

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

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

相关文章

前端实现符合Promise/A+规范的Promise

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 介绍&#xff1a; Promise/A规范简介 1. Promise的三种状态&#xff1a; 2. 状态转换&#xff1a; 3. Promise的…

游戏联运和游戏代理的区别

游戏联运和游戏代理是游戏行业中两种不同的合作模式&#xff0c;它们有一些明显的区别&#xff1a; 合作性质&#xff1a; 游戏联运&#xff1a;游戏联运是多家游戏发行商或开发商之间的合作&#xff0c;它们合作共同推广、发布和运营游戏&#xff0c;每个合作方负责自己的一…

许可分析 license分析 第二章

许可分析是指对软件许可证进行详细的分析和评估&#xff0c;以了解组织内部对软件许可的需求和使用情况。通过许可分析&#xff0c;可以帮助组织更好地管理和优化软件许可证的使用。以下是一些可能的许可分析方法和步骤&#xff1a; 软件许可证更新和续订&#xff1a;及时跟踪和…

QT-day1

实现华清远见登陆界面 #include "mywnd.h" #include <iostream> #include <QDebug> #include <QPushButton> #include <QLineEdit> #include <QLabel>MyWnd::MyWnd(QWidget *parent): QWidget(parent) {//设置固定窗口大小长400&…

ToDoList待办事件(Vue实现)详解

组件化的编码流程&#xff08;通用&#xff09; 实现静态组件&#xff1a;抽取组件&#xff0c;使用组件实现静态页面效果展示动态数据 数据的类型名称 数据保存的组件位置交互-从绑定事件监听开始 实现静态页面 组件按照功能划分 也可以按照其他的划分&#xff0c;我是按照…

Mysql高级——索引(2)

常见索引 索引分类 在MySQL数据库&#xff0c;将索引的具体类型主要分为以下几类&#xff1a;主键索引、唯一索引、常规索引、全文索引。 分类含义特点关键字主键索引针对于表中主键创建的索引默认自动创建, 只能有一个PRIMARY唯一索引避免同一个表中某数据列中的值重复可以…

Linux系统编程(二):文件和目录

参考引用 UNIX 环境高级编程 (第3版)黑马程序员-Linux 系统编程 1. 文件存储 一个文件主要由两部分组成&#xff0c;dentry (目录项) 和 Inode所谓的删除文件&#xff0c;就是删除 Inode&#xff0c;但数据其实还是在硬盘上&#xff0c;以后会覆盖掉 1.1 Inode 其本质为结构体…

使用Jmeter+ant进行接口自动化测试(数据驱动)

最近在做接口测试&#xff0c;因为公司有使用jmeter做接口测试的相关培训资料&#xff0c;所以还是先选择使用jmeter来批量管理接口&#xff0c;进行自动化测试。话不多说&#xff0c;进入正题&#xff1a; 1.使用csv文件保存接口测试用例&#xff0c;方便后期对接口进行维护&…

netperf 测试时延和吞吐

一、Netperf是一种网络性能测试工具&#xff0c;主要基于TCP或UDP的传输。可以测量TCP和UDP传输的吞吐量、时延、CPU 占用率等性能参数。Netperf测试结果所反映的是一个系统能够以多块的速度向另一个系统发送数据&#xff0c;以及另一个系统能够以多块的速度接收数据。 二、打…

数据结构与算法:树

目录 树 定义 结构 二叉树 定义 结构 形式 满二叉树 完全二叉树 存储 链式存储结构 数组 孩子节点 父节点 应用 查找 维持相对顺序 遍历 深度优先遍历 前序遍历 中序遍历 后序遍历 广度优先遍历 层序遍历 二叉堆 定义 自我调整 操作 插入加点 删…

【VUE异常】el-popconfirm失效,@confirm事件不生效,点击没有任何反应,刷新页面才能点击

el-popconfirm失效&#xff0c;confirm事件不生效&#xff0c;点击没有任何反应&#xff0c;刷新页面才能点击 一、背景描述二、原因分析三、解决方案3.1 方案一&#xff1a;使用onConfirm3.2 方案二&#xff1a;confirm与onConfirm同时使用3.3 方案三&#xff1a;el-popconfir…

thinkphp:查询本周中每天中日期的数据,查询今年中每个月的数据,查询近五年每年的总数据

一、查询本周中每天中日期的数据 结果&#xff1a; 以今天2023-09-14为例&#xff0c;这一周为2023-09-11~2023-09-07 代码 后端thinkphp: //查询本周每天的的总金额数 //获取本周的起始日期和结束日期 $weekStart date(Y-m-d, strtotime(this week Monday)); $weekEnd …

Tomcat配置敏感信息屏蔽

一、tomcat敏感信息屏蔽 tomcat的错误信息会吧一些敏感信息给暴露出来比如版本号。 解决方案 在tomcat的conf文件下配置server.xml的 < Host > 标签 <Valve className"org.apache.catalina.valves.ErrorReportValve" showReport"false" showS…

leetcode92. 反转链表 II(java)

反转链表 II 题目描述哨兵技巧代码演示 题目描述 难度 - 中等 leetcode92. 反转链表 II 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 示例1&#xff…

老板要我开发一个简单的工作流引擎-读后感与补充

概述 最近读了一篇《老板要我开发一个简单的工作流引擎》 幽默风趣&#xff0c;干货较多&#xff0c;作为流程引擎的设计者、开发者、探索者&#xff0c;写的很好&#xff0c;合计自己的理解&#xff0c;对每个功能补充说明&#xff0c;对于流程引擎的应用场景&#xff0c;做出…

CoinW TOKEN2049 After Party落幕,分享如何在加密寒冬保持增长

CoinW, 全球领先的数字资产交易平台&#xff0c;在TOKEN2049新加坡期间举办的After Party&#xff0c;于当地时间13日晚落幕。Sui、1inch、Hacken、TinyTrader、AI Analysis等数十个项目方现场带来主题分享。CoinGecko、Cointelegraph、Coin Time、Coinlive、ODAILY、BlockBeat…

使用 Python 和机器学习掌握爬虫和情感分析

在本教程中&#xff0c;我们将抓取一个网站并使用自然语言处理来分析文本数据。 最终结果将是对网站内容的情感分析。以下是我们将遵循的步骤&#xff1a; 项目范围所需的库了解网页抓取抓取网站文本清理和预处理使用机器学习进行情感分析最后结果 一、项目范围 该项目的目…

Bean的管理

配置优先级 Bean管理 获取bean 默认情况下&#xff0c;Spring项目启动时&#xff0c;会把bean多创建好放在IOC容器中&#xff0c;如果想要主动获取这些bean&#xff0c;可以通过如下方式&#xff1a; bean作用域 Spring支持五种作用域&#xff0c;后三种在Web环境才生效&…

十、MySql的索引(重点)

文章目录 一、定义二、常见分类&#xff08;一&#xff09;案例 三、 认识磁盘&#xff08;一&#xff09;MySQL与存储&#xff08;二&#xff09;扇区&#xff08;三&#xff09;定位扇区&#xff08;四&#xff09;结论&#xff08;五&#xff09;磁盘随机访问(Random Access…

EasyX图形化界面

这里写目录标题 EasyX绘制简单的图形化窗口窗口坐标设置窗口属性实现基本绘图功能贴图原样贴图透明贴图认识素材 代码步骤 按键交互阻塞按键 鼠标交互 EasyX 绘制简单的图形化窗口 代码示例&#xff1a; while&#xff08;1&#xff09;&#xff1b; 可以防止闪屏 窗口坐标 …