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

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) {//获取当前机器的cpuint cpuNum = Runtime.getRuntime().availableProcessors();log.info("当前机器的cpu的个数为:" + cpuNum);int maximumPoolSize = cpuNum * 2 ;pool = new ThreadPoolExecutor(maximumPoolSize - 2,maximumPoolSize,5L,   //5sTimeUnit.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;}@Overridepublic 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 failureNode 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 GCfailed = 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();		//此时为0int next = current + releases;	//修改成1if (next < current) // overflowthrow 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))	//将状态改成默认状态0continue;          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 GCfailed = false;return;}}}}
}

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

private void setHeadAndPropagate(Node node, int propagate) {Node h = head; // Record old head for check belowsetHead(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.rhkb.cn/news/133140.html

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

相关文章

VRTK4⭐二.VRTK4的项目基础配置

文章目录 &#x1f7e5; 硬件基本配置&#x1f7e7; 设置XR Plug-in Management&#x1f7e8; 添加项目Tilia&#x1f7e9; 配置项目Hierarchy &#x1f7e5; 硬件基本配置 解决使用OpenXR,HTC头显正常追踪,但手柄无法使用的问题. 问题如下: 当我们按照官方的标准流程配置完Op…

【HttpRunnerManager】搭建接口自动化测试平台实战

一、需要准备的知识点 1. linux: 安装 python3、nginx 安装和配置、mysql 安装和配置 2. python: django 配置、uwsgi 配置 二、我搭建的环境 1. Centos7 &#xff08;配置 rabbitmq、mysql 、Supervisord&#xff09; 2. python 3.6.8 &#xff08;配置 django、uwsgi&…

pcl--第四节 采样一致性算法RANSAC

RANSAC随机采样一致性算法简介 RANSAC是“RANdom SAmple Consensus”&#xff08;随机抽样共识或采样一致性&#xff09;的缩写&#xff0c;它是一种迭代方法&#xff0c;用于从包含异常值的一组数据中估计数学模型的参数。该算法由Fischler和Bolles于1981年发布。 RANSAC算法…

基于SpringBoot+微信小程序的智慧医疗线上预约问诊小程序

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 近年来&#xff0c;随…

关于Linux服务器.sh文件启动问题

问题描述 在linux服务器上使用文本编辑&#xff08;并非vim操作&#xff09;对.sh脚本文件进行修改后无法启动&#xff0c;显示’\r’识别错误等。 错误如下&#xff1a; 错误原因 因为.sh文件在经过这种编辑后格式产生了错误&#xff0c;由unix转为了doc格式&#xff0c;需…

CentOS7上从0开始搭建Zookeeper集群

CentOS7上搭建Zookeeper集群 环境准备安装jdk安装zookeeper下载zookeeper解压zookeeper修改zookeeper配置文件 搭建zookeeper集群修改zoo.cfg文件添加myid文件启动zookeeper集群 环境准备 首先你需要准备三台zookeeper&#xff08;待会会讲zookeeper的安装流程&#xff09;&am…

Spring Cloud学习笔记【消息总线-SpringCloud Bus】

SpringCloud Bus概述 概述 Spring Cloud Bus是Spring Cloud生态系统中的一个组件&#xff0c;用于实现微服务架构中的消息总线。它利用了轻量级消息代理&#xff08;如RabbitMQ或Kafka&#xff09;作为通信中间件&#xff0c;实现了在分布式系统中的消息传递和事件广播。 Sp…

redis I/O多路复用机制

一、基础回顾 1.1 多路复用要解决什么问题 并发多客户端连接场景&#xff0c;在多路复用之前最简单和典型的方案就是同步阻塞网络IO模型。 这种模式的特点就是用一个进程来处理一个网络连接(一个用户请求),比如一段典型的示例代码如下。 直接调用 recv 函数从一个 socket 上…

RHCSA-VM-Linux安装虚拟机后的基础命令

1.代码命令 1.查看本机IP地址&#xff1a; ip addr 或者 ip a [foxbogon ~]$ ip addre [foxbogon ~]$ ip a 1&#xff1a;<Loopback,U,LOWER-UP> 为环回2网卡 2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP>为虚拟机自身网卡 2.测试网络联通性&#xff1a; [f…

万字长文总结检索增强 LLM

连接&#xff1a;https://zhuanlan.zhihu.com/p/655272123 ChatGPT 的出现&#xff0c;让我们看到了大语言模型 ( Large Language Model, LLM ) 在语言和代码理解、人类指令遵循、基本推理等多方面的能力&#xff0c;但幻觉问题 Hallucinations[1] 仍然是当前大语言模型面临的一…

模电课设:用Multisim设计和分析差分放大电路

1 课设内容 1&#xff09;设计一个差分放大器电路&#xff1b; 2&#xff09;用电流源替换发射极电阻&#xff0c;看看会发生什么&#xff1f; 3&#xff09;差分放大器电路特性之差模传输特性。 2 模型搭建 1&#xff09;设计一个差分放大器电路。 差分放大器电路是由对称…

Spring之IOC容器(依赖注入)基本介绍基本配置多模块化

标题一&#xff1a;什么是spring&#xff0c;它能够做什么? Spring是一个开源框架&#xff0c;它由Rod Johnson创建。它是为了解决企业应用开发的复杂性而创建的。Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而&#xff0c;Spring的用途不仅限于服务器端的…

Leetcode 504.七进制数

给定一个整数 num&#xff0c;将其转化为 7 进制&#xff0c;并以字符串形式输出。 示例 1: 输入: num 100 输出: "202"示例 2: 输入: num -7 输出: "-10" 我的答案&#xff1a; 一、信息 1.目的实现十进制向其他进制的转换。 2.原理&#xff1a;公…

地产高质量发展时代:房企为何需要“利他思维”?

中秋将至&#xff0c;凉意初显&#xff0c;但楼市开始有了些暖意。 此前&#xff0c;中央政治局会议明确指出&#xff0c;要适应房地产市场供求关系发生重大变化的新形势&#xff0c;因城施策用好政策工具箱&#xff0c;更好满足居民刚性和改善性住房需求&#xff0c;促进房地…

Linux内核 6.6版本将遏制NVIDIA驱动的不正当行为

导读Linux 内核开发团队日前宣布&#xff0c;即将发布的 Linux 6.6 版本将增强内核模块机制&#xff0c;以更好地防御 NVIDIA 闭源驱动的不正当行为。 Linux 内核开发团队日前宣布&#xff0c;即将发布的 Linux 6.6 版本将增强内核模块机制&#xff0c;以更好地防御 NVIDIA 闭…

使用Process Explorer查看线程的函数调用堆栈去排查程序高CPU占用问题

目录 1、问题描述 2、使用Process Explorer排查软件高CPU占用的一般思路 3、使用Process Explorer工具进行分析 3.1、找到CPU占用高的线程 3.2、查看CPU占用高的线程的函数调用堆栈&#xff0c;找到出问题的代码 3.3、libwebsockets库导出接口lws_service的说明 3.4、解…

无涯教程-JavaScript - LOOKUP函数

描述 需要查看单个行或一列并从第二行或第二列的同一位置查找值时,请使用LOOKUP函数。使用"查找"功能搜索一行或一列。 使用VLOOKUP函数可搜索一行或一列,或搜索多行和多列(如表)。它是LOOKUP的改进版本。 有两种使用LOOKUP的方法- 矢量形式 − Use this form of…

ArrayList

目录 一、ArrayList是什么 二、ArrayList的使用 &#xff08;1&#xff09;导包 &#xff08;2&#xff09;ArrayList的构造方法 三、ArrayList的常用方法 &#xff08;1&#xff09;添加元素 &#xff08;2&#xff09;删除元素 &#xff08;3&#xff09;获取元素 &a…

一场深刻的开源聚会:KCC@北京 9.2 活动回顾

开源为我们带来了什么&#xff1f;这是这场聚会的宣传文的标题&#xff1a;https://mp.weixin.qq.com/s/5sR6TPEpQmYNBnCtVilkzg 同样这个问题也可以是极具个体化的&#xff1a;开源为我带来了什么&#xff1f;秋天的周末&#xff0c;预报有雨&#xff0c;北京的开源人还是相聚…

《golang设计模式》第二部分·结构型模式-05-门面模式Facade)

文章目录 1. 概述1.1 角色1.2 类图 2. 代码示例2.1 设计2.2 代码2.2 类图 1. 概述 门面&#xff08;Facade&#xff09;向客户端提供使用子系统的统一接口&#xff0c;用于简化客户端使用子系统的操作。 1.1 角色 门面角色&#xff08;Facade&#xff09; 客户端可以调用的接…