分类 其它 下的文章

在日新月异的 IT 行业中,每隔数年乃至数月,便会涌现出革新性的技术或前沿框架,引领行业潮流。

比如前端开发,我刚开始工作时,大部分都是静态页面+JavaScript,页面上只有一些简单的交互。

后来出现了
Ajax
技术和
JQuery
库,现在想起当年第一次使用
JQuery
时,真的觉得这就是前端库的终点。

结果没过几年,就兴起了
MVC/MVVM
框架,随之而来的就是
AngularJS

EmberJS
,这时,突然就觉得
JQuery
没有那么香了。

AngularJS

EmberJS
还没闹明白,
React
又横空出世,紧接着就是
Vue
。。。

再看后端开发,早期我主要使用
.NET
,或者用一些
Java
,不过那时候后端语言和框架不是那么被重视,

更多的精力都是放在数据库上,尤其是 Oracle,项目上还有
DBA
的角色,专门负责处理数据库的问题,

那时的应用基本都是单机的。

后来,随着应用规模的扩大,性能问题逐渐显现,开始引入缓存技术(Memcached,Redis),

同时,异步编程和多线程技术也开始被广泛应用。

这时候,各种 Web 框架也如雨后春笋般不断崛起,知名的比如 Spring,Django,Rails 等等。

再后来,互联网应用飞速发展,单机应用开始显得笨重且难以维护和扩展。

云服务,微服务,
Docker
成为主流,持续集成和持续部署(
CI/CD
)流程也被广泛采用,

数据安全和隐私保护的重要性也日益增加,后端开发在安全性和认证方面的要求也越来越高。

其他 IT 领域也类似,随着硬件的发展和互联网累计的数据量到达一定规模之后,神经网络,深度学习和强化学习让机器学习领域飞速发展,AI 真正开始走向普通人的生活中。

总之,我们一直处于技术迭代的循环中。

1. 选择通才还是专家?

像 IT 这样迭代如此之快的行业绝无仅有,那么,在技术的世界里,我们应该专注于一个领域还是尝试很多领域?

也就是说,我们应该成为
通才
(拥有广泛的知识,无论有用与否)还是成为
专家
(致力于一个主题或一个特定的分支)呢?

回答这个问题之前,首先了解一下通才和专家具体有什么不同。

1.1. 通才

通才
指的是那些勇于尝试、对广泛领域保持探索精神的人。

在技术的广阔天地里,他们不仅精通某一专业领域,更在多个领域内拥有深厚的知识积累。

这类人往往不会局限于单一的职业路径,而是倾向于在其职业生涯中跨越不同的领域,不断寻求新的挑战与成长。

这样的人能够改变世界。

著名的通才包括像史蒂夫·乔布斯和埃隆·马斯克这样的天才,他们擅长创新新事物。

通才不仅仅是在他们感兴趣的每一件事上都表现出色,他们还能够将解决一个问题时获得的知识,其应用于不同但相关的问题上。

当他们学会了某个领域的基本知识后,就会把这个知识用在之后接触的每个新领域里。这就是首席技术官的工作范围能横跨多种技术和不同领域的原因。

他们会用自己多年积累的见识和经验去应对每一个新挑战,同时还会不断学些新出现的技术。

通才专注于解决问题,而不是某个具体的技术。

1.2. 专家

相反,专家则展现出高度的专注力。

他们致力于深耕细作,在某一特定领域内稳步前行,犹如手持明灯的引路人。

专家们热衷于全面掌握某一领域的所有知识,他们孜孜不倦地研究、实验与学习,以追求更高的专业造诣和更深的理解。

绝大多数技术进展归功于那些在各自领域深耕细作的专家。

他们专注于机器学习(ML)、网络技术(Web)、移动技术、基础设施、中间件以及其他各类技术领域的开发与优化,凭借多年累积的专业知识与经验,推动了这些领域的持续发展与创新。

专家需系统掌握核心基础知识,并通过在特定领域内长期深耕,方能触及并精通该领域的高级课题。

成就斐然的背后,是无可替代的辛勤努力与积累,无捷径可循,比如各种编程语言,框架的发明者,机器学习领域的各位先驱等等。

各学科的重大发现与进步,很大程度上仰赖于这些领域专家的贡献与推进。

软件专家长期以来运用一套稳定的技术栈,在自身领域内积累了深厚的知识,并对外部进展保持洞察。

他们通过不断努力,成为了行业内的权威人士,同时积极寻求和实践创新的方法论。

不过,软件专家并非要求个体局限于单一技术领域,他们也可以自由探索其他技术领域。

2. 个人建议

我自己的感觉是
通才

专家
属于两个极端,作为一个普通人(包括我自己),我的选择是介于两者之间。

如果对某个技术领域非常感兴趣,那么可以花时间去掌握该领域的各个方面,

但不要限制自己,不要排斥接触其他相关领域。

比如,在
github
,我们可以看到很多前端高手,前端的知识和经验已经非常丰富了,也会去学习
Rust
,然后用 Rust 来开发提高前端开发效率的工具。

还有很多机器学习领域的高手,他们也会学习前端的技术,为自己的大模型制作交互界面,让更多的人能够使用大模型。

选择学习什么技术的时候,有两个很重要的因素值得我们好好参考,就是
兴趣

经验

兴趣
虽然有助于我们缩小选择的技术领域,但最好是尝试不同的技术领域之后,再决定自己的真正的兴趣。

比如,不要因为第一个工作接触的是前端,发现前端也挺有意思,就把自己的兴趣定在前端。

决定兴趣之前,多接触几种不同的领域,尝试在不同的领域做一些小工具玩玩,不同担心学了没用或者浪费时间,

想想通才,任何在其他领域中学到的技能都不会浪费。

其次,
经验
是另一个帮助我们做决定的重要因素。

要成为专家,需要花上好多年的时间去积累经验。如果你已经在某个领域里干了好些年,往专家方向发展通常是个不错的选择。

但是,如果你刚起步或者工作中本来就横跨了几个领域,那么,选择成为通才也许更有意义。

最后,有一个忠告,千万不要仅仅因为困难就放弃成为专家,转而成为通才。

因为通才一点也不比专家简单,通才不是“万事通”,更像是多个领域的专家。

总之,千万不要东搞搞西弄弄,一碰到难题就怂,然后又跑去折腾别的,并且自我安慰“我就是兴趣广泛,更适合做个通才!”。

我的博客地址:
如何避免旧请求的数据覆盖掉最新请求 - 蚊子的前端博客

在检索的场景中,经常会对同一个接口发起不同的检索条件的请求,若前一个请求响应较慢时,可能会覆盖掉我们后发起请求的结果。

如我们先发起一个搜索请求,参数是 A;这个请求还没结束,我们发起了参数是 B 的搜索请求;可能因网络原因或者后端服务处理等原因,后发起的参数 B 的请求先响应了,然后我们把数据展示到页面中;过一会儿之前发起参数是 A 的搜索请求也返回结果了。但实际上,参数 B 的响应结果,才是我们需要展示的最新的数据。

那么如何避免这种现象呢?

1. 请求锁定

可以对发起请求的按钮、输入框等,或者是在全局中,添加 loading,只有得到上一个请求的响应结果后,才取消 loading,才允许用户发起下一次的请求。

const App = () => {
  const [loading, setLoading] = useState(false);

  const request = async (data) => {
    if (loading) {
      // 若请求还没结束,则无法发起新的请求
      return;
    }
    setLoading(true);
    const result = await axios("/api", { data, method: "post" });
    setLoading(false);
  };

  return (
    <div className="app">
      <Form disabled={loading} onFinish={request}>
        <Form.Item>
          <Input />
        </Form.Item>
        <Button htmlType="submit" loading={loading}>
          搜索
        </Button>
      </Form>
    </div>
  );
};

直接用源头进行控制,根本不存在多个请求并行的情况,也就无所谓谁覆盖谁了。

2. 防抖

一般应用在纯输入框的搜索功能中,在用户停止输入一段时间后,才发起搜索,可以加大两次检索请求之间的时间间隔.

const App = () => {
  const request = async () => {};

  return (
    <div className="app">
      {/* 停止输入700ms后触发请求 */}
      <input onInput={debounce(request, 700)} />
    </div>
  );
};


防抖
措施并不能完全杜绝数据被覆盖。假如上一次的请求确实很慢,那也会出现覆盖后续请求的现象。

3. 取消上次的请求

当需要发起新的请求,上次的请求还没结束时,可以取消上次请求。

const App = () => {
  const cancelSouceRef = useRef(null);

  const request = async () => {
    if (cancelSouceRef.current) {
      // 若存在上次的请求还没结束,则手动取消
      cancelSouceRef.current.cancel("手动取消上次的请求");
    }
    const source = axios.CancelToken.source();
    cancelSouceRef.current = source;

    try {
      const response = await axios.get("/api/data", {
        cancelToken: source.token,
      });
      setData(response.data);
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log("请求被取消", error.message);
      } else {
        console.log("请求出错", error.message);
      }
    } finally {
      cancelSouceRef.current = null;
    }
  };

  return (
    <div className="app">
      <button onClick={request}>请求</button>
    </div>
  );
};

如果服务端已接收到了请求,不论前端是否取消了请求,服务端都会完整查询并返回,只是浏览器不再处理而已。

4. 时序控制

我们在每次请求时,都给该请求一个唯一标识,并在该组件的全局中保存最新的标识,若响应时的标识表示最新的标识,则不处理该响应的结果。

标识只要在当前组件中唯一即可,如自增数字、时间戳、随机数等,都可以。

const App = () => {
  const requestIdRef = useRef(0); // 保存最新的请求id

  const request = async () => {
    requestIdRef.current++; // 每次自增

    const curRequestId = requestIdRef.current;
    try {
      const response = await axios.get("/api/data", {
        cancelToken: source.token,
      });
      if (curRequestId < requestIdRef.current) {
        // 当前请求不是最新的,不做任何处理
        return;
      }
      setData(response.data);
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="app">
      <button onClick={request}>请求</button>
    </div>
  );
};

这是一种比较简单有效,同时能让用户任意搜索的方案。

5. 总结

当然,在实际中,肯定也是多方案的组合。比如纯输入框触发搜索的场景中,一般是
防抖+时序控制
的两种方案的组合,既能减少触发请求的次数,又能避免数据的相互覆盖。

有的同学可能想到「控制请求的并发数量」,用
队列

递归
等方式,每次将发起的请求都放到队列的后面,然后按照队列的顺序发起请求。如我们之前曾经在文章
JavaScript 中的 Promise 异步并发控制
探讨过这种场景。

这种方式倒也能解决问题,不过有种「杀鸡用牛刀」的感觉。因为在现在的场景中,对同一个接口发起多次请求时,其实我们更关心的是最新请求的结果,之前请求的结果直接可以扔掉了。

欢迎关注我的公众号:前端小茶馆。

前端小

1、概述

1.1 什么是Subject Alternative Name(证书主体别名)

SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。
它允许一个证书支持多个不同的域名。通过使用SAN字段,可以在一个证书中指定多个DNS名称(域名)、IP地址或其他类型的标识符,这样证书就可以同时用于多个不同的服务或主机上。
这种灵活性意味着企业不需要为每个域名单独购买和安装证书,从而降低了成本和复杂性。

先来看一看 Google 是怎样使用 SAN 证书的,下面是 Youtube 网站的证书信息:

这里可以看到这张证书的 Common Name 字段是 *.google.com,那么为什么这张证书却能够被 www.youtube.com 这个域名所使用呢。原因就是这是一张带有 SAN 扩展的证书,下面是这张证书的 SAN 扩展信息:


这里可以看到,这张证书的 Subject Alternative Name 段中列了一大串的域名,因此这张证书能够被多个域名所使用。对于 Google 这种域名数量较多的公司来说,使用这种类型的证书能够极大的简化网站证书的管理。

1.2 SAN的由来

在早期的互联网中,每个SSL/
TLS证书通常只包含一个CN字段,用于标识单一的域名或IP地址。随着虚拟主机技术的发展和企业对于简化管理的需求增加,需要一种机制能够允许单个证书有效地代表多个域名或服务。例如,一个企业可能拥有多个子域名,希望用单一的证书来保护它们。

为了解决这个问题,SAN扩展被引入到X.509证书标准中。最初在1999年的RFC 2459中提出,SAN提供了一种方法来指定额外的主题名称,从而使得一个证书能有效地代表多个实体。

1.3 SAN的作用和重要性

  1. 多域名保护:SAN使得一个证书可以保护多个域名和子域名,减少了管理的复杂性和成本。
  2. 灵活性增强:企业和组织可以更灵活地管理和部署证书,根据需要快速调整和扩展保护范围。
  3. 兼容性:随着技术的发展,现代浏览器和客户端软件都已经支持SAN。它们会优先检查SAN字段,如果找到匹配项,通常不会再回退到检查CN。

1.4 如何使用SAN

在申请SSL/TLS证书时,可以指定一个或多个SAN值。这些值通常是你希望证书保护的域名或IP地址。证书颁发机构(CA)在颁发证书时会验证这些信息的正确性,并将它们包含在证书的SAN字段中。

2、如何在OpenSSL证书中添加SAN

在OpenSSL中创建证书时添加SAN,需要在配置文件中添加一个subjectAltName扩展。这通常涉及到以下几个步骤:

  1. 准备配置文件:在配置文件中指定subjectAltName扩展,并列出要包含的域名和IP地址。
  2. 生成密钥:使用OpenSSL生成私钥。
  3. 生成证书签发请求(CSR):使用私钥和配置文件生成CSR,该CSR将包含SAN信息。
  4. 签发证书:使用CA(证书颁发机构)或自签名方式签发证书,证书中将包含SAN信息。

2.1 准备带有SAN扩展的证书请求配置文件

  • 创建带有SAN扩展字段的证书签名请求(CSR)的配置文件, alt_names 的配置段为SAN扩展字段配置。确保在将其保存至文件(如csr.conf)。

[ req ]  
default_bits = 2048  
prompt = no  
default_md = sha256  
req_extensions = v3_ext  
distinguished_name = dn  
  
[ dn ]  
C = CN  
ST = ShangDong
L = SZ  
O = Wise2c  
OU = Wise2c  
CN = zmc  
  
[ req_ext ]  
subjectAltName = @alt_names  
  
[ alt_names ]  
DNS.1 = *.zmcheng.com  
DNS.2 = *.zmcheng.net  
DNS.3 = *.zmc.com  
DNS.4 = *.zmc.net  
  
[ v3_ext ]  
basicConstraints=CA:FALSE  
keyUsage=keyEncipherment,dataEncipherment  
extendedKeyUsage=serverAuth,clientAuth  
subjectAltName=@alt_names

注意 1:SAN扩展字段不仅可以配置域名,还可以配置邮箱、IP地址、URI。

// Subject Alternate Name values. (Note that these values may not be valid
// if invalid values were contained within a parsed certificate. For
// example, an element of DNSNames may not be a valid DNS domain name.)
DNSNames       []string
EmailAddresses []string
IPAddresses    []net.IP // Go 1.1
URIs           []*url.URL // Go 1.10

2.2 生成密钥

  • 生成密钥位数为 2048 的 ca.key

openssl genrsa -out ca.key 2048
  • 依据 ca.key 生成 ca.crt (使用 -days 参数来设置证书有效时间):

openssl req -x509 -new -nodes -key ca.key -subj "/CN=zmc" -days 10000 -out ca.crt
  • 生成密钥位数为 2048 的 server.key

openssl genrsa -out server.key 2048

2.3 生成证书签发请求

  • 基于配置文件生成证书签名请求

openssl req -new -key server.key -out server.csr -config csr.conf

2.4 签发证书

  • 使用 ca.key、ca.crt 和 server.csr 生成服务器证书

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \-CAcreateserial -out server.crt -days 10000 \-extensions v3_ext -extfile csr.conf
  • 查看证书,可以看到创建出带有SAN扩展字段证书

openssl x509  -noout -text -in ./server.crt

3、客户端验证服务端证书步骤

客户端(一般是浏览器)在请求HTTPS网站时,验证服务端证书的过程确实是一个复杂且关键的安全步骤。以下是以浏览器为例,说下浏览器验证服务端证书步骤:

  1. 发起HTTPS请求:
    • 用户通过浏览器输入HTTPS网站的URL,并按下回车键或点击链接,浏览器开始发起HTTPS请求。
  2. 服务端响应并发送证书:
    • 服务器在收到HTTPS请求后,会将其SSL/TLS证书发送给浏览器。这个证书包含了服务器的公钥、证书颁发机构(CA)的信息、证书的有效期、以及可能包括的SAN(主题备用名称)和CN(通用名称)等字段。
  3. 浏览器验证证书颁发机构(CA):
    • 浏览器会检查证书是否由受信任的CA签发。浏览器内置了一个受信任的CA列表(也称为根证书列表),它会使用这些CA的公钥来验证证书链中每一级证书签名的有效性,直到找到信任的根证书。
  4. 检查证书有效期:
    • 浏览器会验证证书的有效期,确保当前时间在证书的有效期内。
  5. 验证证书域名

    • 浏览器会检查证书中的SAN字段(如果存在)和CN字段,确保它们与用户正在访问的域名相匹配。如果域名不匹配,浏览器将认为证书无效,并显示警告信息。
  6. 检查证书吊销状态(可选):
    • 浏览器可能会通过OCSP(在线证书状态协议)或CRL(证书吊销列表)来检查证书是否已被吊销。这一步是可选的,但有助于及时发现并阻止使用已泄露私钥的证书。
  7. 生成会话密钥(TLS握手过程的一部分):
    • 如果证书验证通过,浏览器和服务器将进行TLS握手,以协商一个安全的会话密钥。这个密钥将用于后续通信的加密和解密。
  8. 加密通信:
    • 一旦会话密钥协商完成,浏览器和服务器就可以开始加密通信了。浏览器会使用会话密钥加密发送给服务器的数据,而服务器则使用相同的会话密钥解密这些数据。

注意事项

  • 如果浏览器在验证证书的过程中发现任何问题(如证书不受信任、已过期、域名不匹配等),它将向用户显示警告信息,并可能阻止用户继续访问该网站。
  • SAN字段的优先级通常高于CN字段。如果证书中同时包含了SAN和CN字段,并且它们之间的域名不一致,浏览器将优先使用SAN字段中的域名进行验证。
  • 浏览器内置的受信任CA列表可能会随着时间的推移而更新,以反映新的CA或撤销旧的CA。因此,用户应该保持其浏览器和操作系统的更新,以确保其能够识别最新的受信任CA。

以上步骤详细描述了浏览器在请求HTTPS网站时验证服务端证书的过程。这个过程确保了用户与服务器之间的通信是安全、可信的。

4、示例——Kubectl客户端访问KubeApiserver

4.1 环境

KubeApiserver地址:10.20.32.205:6443

KubeApiserver证书信息:

[root@member-cluster2-master1 pki]# pwd
/etc/kubernetes/pki
[root@member-cluster2-master1 pki]# ls
apiserver.crt  apiserver-kubelet-client.crt  ca.crt  front-proxy-ca.crt  front-proxy-client.crt  sa.key
apiserver.key  apiserver-kubelet-client.key  ca.key  front-proxy-ca.key  front-proxy-client.key  sa.pub
[root@member-cluster2-master1 pki]# openssl x509  -noout -text -in ./apiserver.crt 
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 495742113187184862 (0x6e13ad34cb940de)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN = kubernetes
        Validity
            Not Before: Sep 11 01:13:49 2024 GMT
            Not After : Aug 18 01:16:09 2124 GMT
        Subject: CN = kube-apiserver
       .......

            X509v3 Subject Alternative Name: 
                DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:lb.zmc.local, DNS:localhost, DNS:member-cluster2-master1, DNS:member-cluster2-master1.cluster.local, IP Address:10.234.0.1, IP Address:10.20.32.205, IP Address:127.0.0.1
    Signature Algorithm: sha256WithRSAEncryption
       .......

4.2 Kubectl正常访问KubeApiserver(证书SAN扩展字段值包含访问地址)

  • 通过KubeApiserver ip地址访问

  • 通过域名主机映射访问,域名在SAN里面

4.3 Kubectl异常访问KubeApiserver(证书SAN扩展字段值不包含访问地址)

  • 通过域名主机映射访问,域名不在SAN里面,客户端验证证书失败。

5、其他——KubeApiserver证书SAN扩展字段是否要把整个集群IP都列上

第一次接触Kubernetes并安装集群时,网上翻了很多博客,在生成KubeApiserver证书环节,都写着要把整个待安装集群的ip地址都写到KubeApiserver证书请求文件的SAN扩展字段里面,也没讲什么原因。

至于在生成KubeApiserver证书环节是否要把整个集群的ip地址都写到KubeApiserver证书请求文件的SAN扩展字段里面,还是要看集群里面的组件如何访问KubeApiserver,如果:

  • 调度器(kube-scheduler)、控制器(kube-controller-manage)都是直接和当前节点的kube-apiserver组件直接交互;
  • kubelet、kubectl、kube-proxy通过VIP代理到kube-apiserver组件。

对于以上这种情况,SAN扩展字段可以只写VIP(负责均衡器IP等)、MasterIPs即可,WorkerIP没必要写到SAN扩展字段里面。

6、结论

Subject Alternative Name(主体别名)是一个证书扩展字段,用于指定证书所适用的主机名列表。当客户端连接到服务器时,会检查服务器返回的证书中的主体别名是否包含与请求的主机名匹配的条目。

SAN的引入极大地增强了数字证书的功能和应用范围,使得管理和保护多域名环境变得更加高效和简便。作为现代网络安全的一个关键组成部分,理解并正确使用SAN对于任何需要部署SSL/TLS保护的个人或组织都至关重要。随着网络环境的不断演变和新需求的不断出现,SAN将继续发挥其在保护网络通信安全中的重要作用。无论是IT专业人员还是普通用户,都应该了解SAN的基本概念和实践,以确保在数字世界中安全地通信和交互。

队列
是咱们开发中经常使用到的一种数据结构,它与

的结构类似。然而栈是后进先出,而队列是先进先出,说的专业一点就是
FIFO
。在生活中到处都可以找到队列的,最常见的就是排队,吃饭排队,上地铁排队,其他就不过多举例了。

队列的模型

在数据结构中,和排队这种场景最像的就是
数组
了,所以我们的队列就用数组去实现。在排队的过程中,有两个基本动作就是
入队

出队
,入队就是从队尾插入一个元素,而出队就是从队头移除一个元素。基本的模型我们可以画一个简图:

看了上面的模型,我们很容易想到使用数组去实现队列,

  1. 先定义一个数组,并确定数组的长度,我们暂定数组长度是5,而上面图中的长度是一样的;
  2. 再定义两个数组下标,
    front

    tail
    ,front是队头的下标,每一次出队的操作,我们直接取front下标的元素就可以了。tail是队尾的下标,每一次入队的操作,我们直接给tail下标的位置插入元素就可以了。

我们看一下具体的过程,初始状态是一个空的队列,

队头下标和队尾下标都是指向数组中的第0个元素,现在我们插入第一个元素“a”,如图:

数组的第0个元素赋值“a”,tail的下标+1,由指向第0个元素变为指向第1个元素。这些变化我们都要记住啊,后续在编程实现的过程中,每一个细节都不能忽略。然后我们再做一次出队操作:

第0个元素“a”在数组中移除了,并且front下标+1,指向第1个元素。

这些看起来不难实现啊,不就是给数组元素赋值,然后下标+1吗?但是我们想一想极端的情况, 我们给数组的最后一个元素赋值后,数组的下标怎么办?

tail如果再+1,就超越了数组的长度了呀,这是明显的
越界
了。同样front如果取了数组中的最后一个元素,再+1,也会越界。这怎么办呢?

循环数组

我们最开始想到的方法,就是当tail下标到达数组的最后一个元素的时候,对数组进行扩容,数组的长度又5变为10。这种方法可行吗?如果一直做入队操作,那么数组会无限的扩容下去,占满磁盘空间,这是我们不想看到的。

另外一个方法,当front或tail指向数组最后一个元素时,再进行+1操作,我们将下标指向队列的开头,也就是第0个元素,形成一个循环,这就叫做
循环数组
。那么这里又引申出一个问题,我们的下标怎么计算呢?

  1. 数组的长度是5;
  2. tail当前的下标是4,也就是数组的最后一个元素;
  3. 我们给最后一个元素赋值后,tail怎么由数组的最后一个下标4,变为数组的第一个下标0?

这里我们可以使用
取模
来解决:
tail = (tail + 1) % mod
,模(mod)就是我们的数组长度5,我们可以试一下,tail当前值是4,套入公式计算得到0,符合我们的需求。我们再看看其他的情况符不符合,假设tail当前值是1,套入公式计算得出2,也相当于是+1操作,没有问题的。只有当tail+1=5时,才会变为0,这是符合我们的条件的。那么我们实现队列的方法就选用循环数组,而且数组下标的计算方法也解决了。

队列的空与满

队列的空与满对入队和出队的操作是有影响的,当队列是满的状态时,我们不能进行入队操作,要等到队列中有空余位置才可以入队。同样当队列时空状态时,我们不能进行出队操作,因为此时队列中没有元素,要等到队列中有元素时,才能进行出队操作。那么我们怎么判断队列的空与满呢?

我们先看看队列空与满时的状态:

空时的状态就是队列的初始状态,front和tail的值是相等的。

满时的状态也是front == tail,我们得到的结论是,front == tail时,队列不是空就是满,那么到底是空还是满呢?这里我们要看看是什么操作导致的front == tail,如果是入队导致的front == tail,那么就是满;如果是出队导致的front == tail,那就是空。

手撸代码

好了,队列的模型以及基本的问题都解决了,我们就可以手撸代码了,我先把代码贴出来,然后再给大家讲解。

public class MyQueue<T> {

    //循环数组
    private T[] data;
    //数组长度
    private int size;
    //出队下标
    private int front =0;
    //入队下标
    private int tail = 0;
    //导致front==tail的原因,0:出队;1:入队
    private int flag = 0;

    //构造方法,定义队列的长度
    public MyQueue(int size) {
        this.size = size;
        data = (T[])new Object[size];
    }
    
    /**
     * 判断对队列是否满
     * @return
     */
    public boolean isFull() {
        return front == tail && flag == 1;
    }

    /**
     * 判断队列是否空
     * @return
     */
    public boolean isEmpty() {
        return front == tail && flag == 0;
    }

    /**
     * 入队操作
     * @param e
     * @return
     */
    public boolean add(T e) {
        if (isFull()) {
            throw new RuntimeException("队列已经满了");
        }
        data[tail] = e;
        tail = (tail + 1) % size;
        if (tail == front) {
            flag = 1;
        }

        return true;
    }

    /**
     * 出队操作
     * @return
     */
    public T poll() {
        if (isEmpty()) {
            throw new RuntimeException("队列中没有元素");
        }
        T rtnData = data[front];
        front = (front + 1) % size;
        if (front == tail) {
            flag = 0;
        }
        return rtnData;
    }
}

在类的开始,我们分别定义了,循环数组,数组的长度,入队下标,出队下标,还有一个非常重要的变量
flag
,它表示导致front == tail的原因,0代表出队,1代表入队。这里我们初始化为0,因为队列初始化的时候是空的,而且front == tail,这样我们判断
isEmpty()
的时候也是正确的。

接下来是构造方法,在构造方法中,我们定义了入参
size
,也就是队列的长度,其实就是我们循环数组的长度,并且对循环数组进行了初始化。

再下面就是判断队列空和满的方法,实现也非常的简单,就是依照
上一小节
的原理。

然后就是入队操作,入队操作要先判断队列是不是已经满了,如果满了,我们进行报错,不进行入队的操作。有的同学可能会说,这里应该等待,等待队列有空位了再去执行。这种说法是非常正确的,我们先把最基础的队列写完,后面还会再完善,大家不要着急。下面就是对循环数组的tail元素进行赋值,赋值后,使用我们的公式移动tail下标,tail到达最后一个元素时,通过公式计算,可以回到第0个元素。最后再判断一下,这个入队操作是不是导致了front==tail,如果导致了,就将flag置为1。

出队操作和入队操作类似,只不过是取值的步骤,这里不给大家详细解释了。

我们做个简单的测试吧,

 public static void main(String[] args) {
        MyQueue<String> myQueue = new MyQueue<>(5);
        System.out.println("isFull:"+myQueue.isFull()+" isEmpty:"+myQueue.isEmpty());
        myQueue.add("a");
        System.out.println("isFull:"+myQueue.isFull()+" isEmpty:"+myQueue.isEmpty());
        myQueue.add("b");
        myQueue.add("c");
        myQueue.add("d");
        myQueue.add("e");
        System.out.println("isFull:"+myQueue.isFull()+" isEmpty:"+myQueue.isEmpty());
        myQueue.add("f");
    }

我们定义长度是5的队列,分别加入
a b c d e f
6个元素,并且看一下空和满的状态。

打印日志如下:

isFull:false isEmpty:true
isFull:false isEmpty:false
isFull:true isEmpty:false
Exception in thread "main" java.lang.RuntimeException: 队列已经满了
	at org.example.queue.MyQueue.add(MyQueue.java:29)
	at org.example.queue.MyQueue.main(MyQueue.java:82)

空和满的状态都是对的,而且再插入f元素的时候,报错了”队列已经满了“,是没有问题的。出队的测试这里就不做了,留个小伙伴们去做吧。

并发与等待

队列的基础代码已经实现了,我们再看看有没有其他的问题。对了,第一个问题就是
并发
,我们多个线程同时入队或者出队时,就会引发问题,那么怎么办呢?其实也很简单,加上
synchronized
关键字就可以了,如下:

/**
 * 入队操作
 * @param e
 * @return
 */
public synchronized boolean add(T e) {
    if (isFull()) {
        throw new RuntimeException("队列已经满了");
    }
    data[tail] = e;
    tail = (tail + 1) % size;
    if (tail == front) {
        flag = 1;
    }

    return true;
}

/**
 * 出队操作
 * @return
 */
public synchronized T poll() {
    if (isEmpty()) {
        throw new RuntimeException("队列中没有元素");
    }
    T rtnData = data[front];
    front = (front + 1) % size;
    if (front == tail) {
        flag = 0;
    }
    return rtnData;
}

这样入队出队操作就不会有并发的问题了。下面我们再去解决上面小伙伴提出的问题,就是入队时,队列满了要等待,出队时,队列空了要等待,这个要怎么解决呢?这里要用的
wait()

notifyAll()
了,再进行编码前,我们先理清一下思路,

  1. 目前队列的长度是5,并且已经满了;
  2. 现在要向队列插入第6个元素,插入时,判断队列满了,要进行等待
    wait()
    ;
  3. 此时有一个出队操作,队列有空位了,此时应该唤起之前等待的线程,插入元素;

相反的,出队时,队列是空的,也要等待,当队列有元素时,唤起等待的线程,进行出队操作。好了,撸代码,

/**
 * 入队操作
 * @param e
 * @return
 */
public synchronized boolean add(T e) throws InterruptedException {
    if (isFull()) {
        wait();
    }
    data[tail] = e;
    tail = (tail + 1) % size;
    if (tail == front) {
        flag = 1;
    }
    notifyAll();
    return true;
}

/**
 * 出队操作
 * @return
 */
public synchronized T poll() throws InterruptedException {
    if (isEmpty()) {
        wait();
    }
    T rtnData = data[front];
    front = (front + 1) % size;
    if (front == tail) {
        flag = 0;
    }
    notifyAll();
    return rtnData;
}

之前我们抛异常的地方,统一改成了
wait()
,而且方法执行到最后进行
notifyAll()
,唤起等待的线程。我们进行简单的测试,

public static void main(String[] args) throws InterruptedException {
    MyQueue<String> myQueue = new MyQueue<>(5);
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    myQueue.add("a");
}

测试结果没有问题,可以正常打印"a"。这里只进行了出队的等待测试,入队的测试,小伙伴们自己完成吧。

if还是while

到这里,我们手撸的消息队列还算不错,基本的功能都实现了,但是有没有什么问题呢?我们看看下面的测试程序,

public static void main(String[] args) throws InterruptedException {
    MyQueue<String> myQueue = new MyQueue<>(5);
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    Thread.sleep(5000);
    myQueue.add("a");
}

我们启动了两个消费者线程,同时从队列里获取数据,此时,队列是空的,两个线程都进行等待,5秒后,我们插入元素"a",看看结果如何,

a
null

结果两个消费者都打印出了日志,一个获取到null,一个获取到”a“,这是什么原因呢?还记得我们怎么判断空和满的吗?对了,使用的是
if
,我们捋一下整体的过程,

  1. 两个消费者线程同时从队列获取数据,队列是空的,两个消费者通过
    if
    判断,进入等待;
  2. 5秒后,向队列中插入"a"元素,并唤起所有等待线程;
  3. 两个消费者线程被依次唤起,一个取到值,一个没有取到。没有取到是因为取到的线程将front加了1导致的。这里为什么说依次唤起等待线程呢?因为
    notifyAll()
    不是同时唤起所有等待线程,是依次唤起,而且顺序是不确定的。

我们希望得到的结果是,一个消费线程得到”a“元素,另一个消费线程继续等待。这个怎么实现呢?对了,就是将判断是用到的
if
改为
while
,如下:

/**
 * 入队操作
 * @param e
 * @return
 */
public synchronized boolean add(T e) throws InterruptedException {
    while (isFull()) {
        wait();
    }
    data[tail] = e;
    tail = (tail + 1) % size;
    if (tail == front) {
        flag = 1;
    }
    notifyAll();
    return true;
}

/**
 * 出队操作
 * @return
 */
public synchronized T poll() throws InterruptedException {
    while (isEmpty()) {
        wait();
    }
    T rtnData = data[front];
    front = (front + 1) % size;
    if (front == tail) {
        flag = 0;
    }
    notifyAll();
    return rtnData;
}

在判断空还是满的时候,我们使用
while
去判断,当两个消费线程被依次唤起时,还会再进行空和满的判断,这时,第一个消费线程判断队列中有元素,会进行获取,第二个消费线程被唤起时,判断队列没有元素,会再次进入等待。我们写段代码测试一下,

public static void main(String[] args) throws InterruptedException {
    MyQueue<String> myQueue = new MyQueue<>(5);
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();
    new Thread(() -> {
        try {
            System.out.println(myQueue.poll());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    Thread.sleep(5000);
    myQueue.add("a");
    Thread.sleep(5000);
    myQueue.add("b");
}

同样,有两个消费线程去队列获取数据,此时队列为空,然后,我们每隔5秒,插入一个元素,看看结果如何,

a
b

10秒过后,插入的两个元素正常打印,说明我们的队列没有问题。入队的测试,大家自己进行吧。

总结

好了,我们手撸的消息队列完成了,看看都有哪些重点吧,

  1. 循环数组;
  2. 数组下标的计算,用取模法;
  3. 队列空与满的判断,注意flag;
  4. 并发;
  5. 唤起线程注意使用
    while

世界那么大,我想去看看...

原文

不会打歌么学打歌阿哥怎摆你怎摆,大江大海江大海 ...

瞧,这个中年不油腻(不油腻的原因是大叔很穷)的大叔扛着音箱出场了,其实远没有这么拉风!

今年被动看到许多不好的消息和内容:充满了“失业”,“裁员”等。一度我已经更郁郁了。所以我今天不是来搞笑的。真心希望大家能越来越好,希望看到大家报喜的内容来抵消一下。

园子里tobin老兄说:

琢磨了几条路,不知道能不能走通: 回家继承家业 ** 小说里的情节,回家继承千万家产,过上富足的生活?现实是,家里唯一值钱的老母猪都卖了,子承父业怕是只能喝西北风。 合伙创业,攒个小公司接项目 ** 几个朋友联合起来,先有个能撑3-5个月的项目做,倒腾倒腾,未必不能成。说实话,有明确项目来源是关键,不然很容易陷入资金链困局。 技术变现 ** 把技术做成产品,比如中间件、报表系统或BI工具,卖版本赚钱? ——高手或许能成,但多数人缺少资源和市场,难度不小。 转行做实业,开店或做餐饮,什么商店,五金,酒店,饭店 ** 看起来容易,但每行都得交学费。如果有人带着,可能会少走些弯路。实业不是表面看着那么简单,稍有不慎,亏掉的可能是你所有的积蓄和时间。

大叔我其实也和大伙差不多,整理了一下自己的情况发现并不是有什么清新脱俗的地方。但是我已经45周岁也就是虚岁46了,这是千真万确的事。我搞过收银软件,供应链管理,MES,现在在实施ERP。我曾经也编码20多年。很多人说我很正力量,我身上的标签有:“自学成才”,“中专学历”,“非著名野生程序员”等。但我一直都在小公司里苟且偷生,出来没在开发人员超过20的公司工作过。我的工资最高是2016年的16000。这么多年也只能是勉强养家糊口。10年前我在博客园写了一篇文章:
五有老码农,程序人生回顾:心安也不是归处啊

。又过了10年,我感觉还是没有改变太多。
整理了一下自己的情况发现并不是有什么清新脱俗的地方。但是我已经45周岁也就是虚岁46了,这是千真万确的事。我搞过收银软件,供应链管理,MES,现在在实施ERP。我曾经也编码20多年。很多人说我很正力量,我身上的标签有:“自学成才”,“中专学历”,“非著名野生程序员”等。但我一直都在小公司里苟且偷生,出来没在开发人员超过20的公司工作过。我的工资最高是2016年的16000。这么多年也只能是勉强养家糊口。10年前我在博客园写了一篇文章:
五有老码农,程序人生回顾:心安也不是归处啊

在国外这一年多我还完了欠亲戚的十几万外账,这是目前最欣慰的结果。但是我已经几个月都没有在写代码了,刚开始公司想让我做全职开发,让我一个一个子系统的上线。所以我开发了POS系统,并成功通过(萨摩亚和斐济的税务平台TaxCore)的备案。但后来公司选择使用开源ERP----ODOO并在广州选择了实施和二次开发的软件公司,这样我的开发工作就终止了,现在在仓库工作,参与系统维护。

我写这篇文章的原因是在博客园看到一篇文章“
41岁的大龄程序员,苟着苟着,要为以后做打算了
”,若有所思,有一点感慨不吐不快。

我其实是想对各位大龄码农说:世界很大,不要把自己局限在中国,有条件出国来看看吧。我朋友圈里最光鲜的案例应该是杨中科老师,他去新西兰读了硕士,目前已经全家移民了,得到了新西兰的永居身份。后来进了新西兰一国家银行工作,一周可以三天在家远程工作,资本主义国家都信奉“工作只是生活的辅助”,真是羡煞旁人啊。

我最近在努力提高英语能力,虽然学了那么多年的英语,但我还是不能达到流利和老外交流的水平。我发现自己口齿不清,词汇量也达不到。随便说一句杨中科老师也开发了一个学英语的网站,也有微信小程序端,大家请搜索youzack。因为上面的很多资源我其实还听不懂,所以我目前还没有用他这个。我目前在用多邻国App,已经连续坚持了230天。目前只是觉得听力有进步。最近准备多看一些美剧,我收集了一些高频使用的口语,大家听过没有?

None of this...

音标:/nʌn ʌv ðɪs/

中文注释:这些都不(不适用、不成立等)

使用场景:你在讨论一个话题,觉得所说的内容都不相关时可以说。例如:“None of this is relevant to the main issue.”

There is no shame in...

音标:/ðɛr ɪz noʊ ʃeɪm ɪn/

中文注释:...没有什么可羞愧的

场景: 你看到一个朋友因为失业而感到羞愧,你想要安慰他。

你可以说:“There is no shame in losing your job. Many people go through it, and it's just a part of life. What’s important is how you move forward.”

You'd never guess who...

音标:/juːd ˈnɛvər ɡɛs huː/

中文注释:你永远猜不到是谁

使用场景:你要揭示一个令人惊讶的人物时可以使用。例如:“You’d never guess who showed up at the party.”

All the way across town...

音标:/ɔːl ðə weɪ əˈkrɔːs taʊn/

中文注释:整个城市的另一边

使用场景:当你谈论一个地点很远时可以使用。例如:“I had to drive all the way across town to get there.”

I am not cut out for...

音标:/aɪ æm nɒt kʌt aʊt fɔːr/

中文注释:我不适合

使用场景:你在表示自己不适合某项工作或任务时。例如:“I’m not cut out for this kind of high-pressure job.”

Mooching off of...

音标:/ˈmuːtʃɪŋ ɒf ʌv/

中文注释:依赖于(别人的钱或资源)

使用场景:当你觉得某人依赖别人而不自食其力时。例如:“He’s just mooching off of his parents.”

Wanna grab...

音标:/ˈwɑːnə ɡræb/

中文注释:想去拿(或去吃、喝)

使用场景:你提议去吃饭或喝东西时。例如:“Wanna grab a coffee later?”

Beat around the bush...

音标:/biːt əˈraʊnd ðə bʊʃ/

中文注释:拐弯抹角

使用场景:当你要说某人说话不直接时可以用。例如:“Stop beating around the bush and tell me what you really think.”

Why is this so...

音标:/waɪ ɪz ðɪs səʊ/

中文注释:为什么会这样

使用场景:你对某事的原因感到困惑时可以使用。例如:“Why is this so difficult to understand?”

Patch things up...

音标:/pætʃ θɪŋz ʌp/

中文注释:修补关系

使用场景:当你要修复破裂的关系或解决争端时。例如:“Let’s patch things up and move on.”

Take the word of...

音标:/teɪk ðə wɜːrd ʌv/

中文注释:相信某人的话

使用场景:当你决定相信某人所说的话时。例如:“I’ll take your word for it.”

I wouldn't even know...

音标:/aɪ ˈwʊdnt ˈiːvn noʊ/

中文注释:我甚至不知道

使用场景:当你对某事一无所知时可以使用。例如:“I wouldn’t even know how to start fixing that.”

I should probably...

音标:/aɪ ʃʊd ˈprɒbəbli/

中文注释:我可能应该

使用场景:当你考虑某个动作是对的时可以使用。例如:“I should probably call her and apologize.”

I bid you adieu...

音标:/aɪ bɪd juː əˈdjuː/

中文注释:我向你道别

使用场景:你在正式告别或结束交流时可以使用。例如:“I bid you adieu and wish you safe travels.”

Have a knack for...

音标:/hæv ə næk fɔːr/

中文注释:有天赋

使用场景:当你形容某人在某方面有特别的才能时可以使用。例如:“She has a knack for solving difficult problems.”

Wanna go grab...

音标:/ˈwɒnə ɡoʊ ɡræb/

中文注释:想去吃(或喝)

使用场景:当你想提议去外面吃东西时可以使用。例如:“Wanna go grab dinner after work?”

I could really go for...

音标:/aɪ kʊd ˈrɪəli ɡoʊ fɔːr/

中文注释:我真的很想要

使用场景:当你特别想要某样食物或饮品时可以使用。例如:“I could really go for a slice of pizza right now.”

what are you gonna...

音标: /wɑːt ɑːr juː ˈɡənə dʊ/ 或 /wɑːt ɑːr juː ˈɡənə seɪ/

中文注释: 你打算做什么?/你打算说什么?

使用场景: 你和朋友在商量周末的安排,你可以问:“What are you gonna do this weekend?” (你这个周末打算做什么?)

询问意图: 当对方做了一些让你困惑的事情时,你可能会问:“What are you gonna say about this?” (你对此打算说什么?)

你愿意和我一起提高自己的英语水平吗,记得我是你的伙伴哟!苦于想找人对练口语好久了,本来我用AI也就是chatGPT来练的,但是一般情况他都听不懂我说的,是的我口齿不清,我估计他会太无聊,于是就取消了。