2026年2月

当您满心期待地打开自己的网站,却看到浏览器地址栏那个醒目的红色“不安全”警告时,想必心情瞬间跌入谷底。更糟糕的是,这个警告正在悄无声息地赶走您的潜在客户。

别担心,今天这篇文章就为您详细梳理:网站提示“不安全”的常见原因是什么?如何一步步解决?

第一步:对症下药——四种常见问题的解决方案

1、未安装SSL证书

如果确认您的网站根本没有SSL证书,解决方案最简单:立即申请并安装SSL证书

对于个人博客、小型展示网站,可以选择免费SSL证书;对于企业官网、电商平台,建议选择付费OV或EV证书,提供更高级别的身份验证和用户信任。

2、SSL证书过期或无效

证书过期是最常见的问题之一,往往出现在长期无人维护的网站上。

解决方案

  • 若证书过期,登录证书平台找到过期证书,点击“续期”,完成域名验证后重新安装
  • 若使用自签名证书,浏览器默认不信任,需废弃并重新申请正规证书

3、域名与证书不匹配

如果您为www.example.com申请了证书,但用户访问的是example.com,就会触发此警告。

解决方案

  • 检查证书绑定的域名是否与访问域名一致
  • 若需支持多个子域名,可将单域名证书升级为通配符证书(如*.example.com

第二步: 通过JoySSL解决“不安全”问题的实操步骤

免费SSL证书申请入口

1、 注册JoySSL账号

访问JoySSL官方网站,点击“注册”创建新账户。**在注册过程中填写注册码 230970,可确保享受永久免费SSL证书及相关技术支持。

2、 选择证书类型

登录后,在SSL证书页面找到“免费体验版”,选择适合您的证书类型:

  • 单域名证书:保护单个域名
  • 通配符证书:保护一个域名及其所有子域名(强烈推荐有子域名的网站)
  • 多域名证书:保护多个不同域名

3、验证域名所有权

按照提示完成域名验证,通常是通过在域名DNS设置中添加特定的TXT记录,这一过程一般几分钟内即可完成审核。

4、下载与安装证书

验证成功后,从JoySSL平台下载证书文件。根据您的服务器环境(如Nginx、Apache、IIS等),参照官方文档进行配置和部署。

5、 设置强制HTTPS跳转

在服务器配置中添加重写规则,将所有HTTP流量强制重定向到HTTPS。

过年期间,装 openclaw,以及衍生的 picoclaw、nanoclaw。当然我重点不是对比。是再找安装需求中发生。
因为我用 tg 作为 channel,所以我一开始想到就是做消息聚合,让 claw 处理后给我。因为我 tg 订阅很多频道,于是我让 claw 想办法获取那些消息然后处理推送给我。
他给我两个方案:

  1. 把 bot 添加到每个频道(我觉得不方便也不一定可以)
  2. 根据 api 去获取
    于是让他用 pyhon 写一个脚本,然后他需要 session,我从一个开源网站获取(给二维码,tg 授权),然后把获取 session string 给脚本,然后一直失败,正调试中,发现我 tg 账号被冻,然后我申诉(唯一就是注册年份忘了,不知道影响大不大)

我 tg 很早前用+86 注册,当时收不到短信,通过发邮件解决,到现在也看看信息,没有深度使用
现在 tg 被冻结,发现不看那些订阅频道对我影响不大,反正我做好失去 tg 号准备,后续用 rss 来代替他作用,怎么说我没把这个当坏事,我觉得看这么多频道消息,对我来说大部分无用信息,现在思考把这些时间用需要地方
对了同时把所有装 claw 都卸了= =

下面顺便分享我之前看 tg 频道(挑经常看)

本期让我们来了解下云流管理平台中【服务器管理】和【用户管理】的具体功能吧!

一、服务器管理

当实时云渲染用户并发量很大,或分布在不同地区或国家时,为给用户更好的实时渲染体验,点量云流系统支持动态扩容,可设置不同国家或地区的机房区域,每个区域下支持多台服务器自动负载调度,以保证每个用户都有更好的使用体验。

【01机房区域】

1、页面汇总当前已创建的机房区域,可进行编辑、删除等操作。不同机房区域用于负载均衡、测速时使用。
2、一般用户访问某个云流化的内容,会先从多个机房进行测速,选一个最近的机房使用,因此请正确填写机房区域。

3、点击“创建机房区域”按钮可创建新的机房区域,在服务器列表页面可对服务器所属的区域进行编辑。

【02服务器列表】

页面对已有服务器进行汇总,展示了服务器的名称、地址、渲染服务版本、机房区域、上架状态、在线状态、网页端口、渲染管理端口、创建时间等信息。
支持通过服务器名称、上架状态等字段对服务器进行搜索,用户可以对服务器进行编辑、删除、下架、查看资源使用等操作。

若想要创建服务器,有以下两种方式:
方法一:自动加入(推荐)
安装点量云流渲染服务,并填写授权地址提交认证,该服务器自动加入组织,由平台统一管理。

方法二:手动添加
1、在【服务器列表】页面,点击“创建服务器”按钮,打开创建弹窗,输入服务器基本信息。

  • 服务器名称:自定义服务器名称
  • 服务器地址:服务器IP地址
  • 局域网IP地址:服务器所在局域网IP地址,可用于服务器与CMS管理端在局域网内通信
  • 物理地址:服务器的MAC地址
  • CPU最大使用率:CPU超过该设置限制将会启动新流路,默认为0,0代表不限制
  • GPU最大使用率:GPU超过该设置限制将会启动新流路,默认为0,0代表不限制
  • 机房区域:服务器所属的机房区域
  • 服务器信息:对服务器的备注信息

2、除填写服务器基本信息之外,也可在高级设置中对服务器相关端口参数进行细节修改。如果没有特殊需求,保持默认值即可。

  • 守护进程服务端口号:渲染服务和管理平台后台通信端口(TCP)
  • web服务端口号:Web服务端口(TCP)
  • 流路起始端口号:当前服务器的流路从哪个端口号开始使用
  • 会话连接端口号:设置rdp服务的端口号

3、信息填写完成之后点击“确定”即可保存成功。

【03服务器预警设置】

为了帮助用户更好地监测服务器状态、及时了解实时渲染系统服务器和显卡等硬件资源的使用情况并预防突发故障,点量云流管理平台提供了[服务器预警设置]与[服务器预警记录】功能。
页面对已有预警规则进行汇总,展示了预警规则的名称、状态、CPU预警值、GPU预警值、内存预警值、磁盘预警值、预警沉默间隔等信息。支持通过名称、状态对预警规则进行搜索,用户可以对预警规则进行禁用、编辑、删除等操作。

若想创建预警规则:
1、点击“添加预警规则”按钮,打开创建窗口。
2、设置规则名称、CPU预警值、GPU预警值、内存预警值、磁盘预警值、预警沉默间隔,磁盘预警值将在服务器的任一磁盘满足条件即触发报警,预警值设置为0代表当前指标不设置预警,预警沉默间隔指的一定时间段内连续触发预警条件不重复生成预警记录。

3、点击“确定”即可保存成功,预警规则创建总数不可超过10条。

【04服务器预警记录】

1、页面对服务器预警记录进行汇总,支持通过规则名称、所属机器、指标类型、报警时间等字段进行搜索。
2、列表展示了预警记录所对应的规则名称、服务器名称、报警信息、报警时间,用户可以对预警规则进行删除操作。

二、用户管理

为保证特定用户随时可访问实时云渲染网页,点量云流系统支持用户管理,可在这里实现对特定用户账号、允许访问云应用的管理。

该页面对已有用户进行汇总,展示了用户的账号、姓名、授权应用、状态等信息,支持通过账号、状态对用户进行搜索,可以对用户进行禁用、编辑、删除等操作。

若想创建用户:
1、点击“创建用户”按钮,打开创建弹窗。输入用户信息,创建用户登录的账号和密码,填写姓名及备注。基本信息填写完成之后,选择该用户可查看的云应用,支持多选。选择完成之后,点击“保存”即可保存成功。

2、复制或者直接打开“用户登录链接”,终端用户可通过设置的账号、密码进行登录。

3、用户登录成功之后即可查看所属的云应用,点击即可直接打开。

注意:

  • 禁用:用户禁用之后,该账号状态变为“禁用”状态,账号可登录,但是无法打开个人中心中的云应用。
  • 编辑:参考“创建用户”的步骤,可以对用户的详细信息进行修改。
  • 删除:删除该用户,账号无法再登录

昨天,公司的 Front Office Quants 团队裁员 20%,Desk Quant 、Strategist 和 E-Trading 都被波及,欧洲办公室是重灾区,剩下的年底继续裁 30%,因为 AI 工具极大的提高了团队的效率,不需要这么多人了,当然留下来的人薪资肯定会更高,因为分蛋糕的人少了

本来 quants 就够卷的了,现在 AI 也来插一脚,真的卷上加卷,等 AI 发展程度更高了,中台,后台肯定也要受到波及,真的感受到人的智力开始快速贬值了

我在 buy-side ,感觉 sell-side 受到的冲击会小一点,但是赚的肯定要少很多

不知道互联网公司是不是也受到冲击,几年前,我们公司的 IT 团队,外包了一部分到印度,今年也全部取消了,自己的团队加上 AI 就能满足需求了,并且年底估计也要裁掉一部分

大家好,我是良许。

在当今这个万物互联的时代,网络安全和数据保护已经成为每个技术从业者都无法回避的话题。

作为一名嵌入式开发者,我深刻体会到,无论是智能家居设备、车载系统,还是工业控制设备,一旦联网,就面临着各种安全威胁。

今天我想和大家聊聊这个话题,从嵌入式开发者的视角,看看我们应该如何构建更安全的系统。

1. 为什么嵌入式设备的安全如此重要

1.1 嵌入式设备面临的安全威胁

很多人可能觉得,嵌入式设备功能简单,不会成为黑客的目标。

但事实恰恰相反。我在汽车电子领域工作这些年,见过太多因为安全问题导致的严重后果。

嵌入式设备往往直接控制物理世界,比如汽车的刹车系统、工厂的生产线、医疗设备等。

一旦被攻击,后果可能是灾难性的。

2015 年,两名安全研究人员远程入侵了一辆 Jeep 切诺基,控制了方向盘、刹车和变速器,最终导致该车型召回 140 万辆。

这个案例给整个行业敲响了警钟。

1.2 常见的攻击方式

在嵌入式领域,攻击者常用的手段包括:固件逆向工程、调试接口利用(比如 JTAG、UART)、网络协议漏洞利用、侧信道攻击等。

我曾经参与过一个项目的安全审计,发现开发板上的 UART 调试接口没有任何保护,直接连上就能获取 root 权限,这在生产环境中是非常危险的。

2. 嵌入式系统的安全防护策略

2.1 安全启动(Secure Boot)

安全启动是保护设备的第一道防线。

它确保设备只运行经过验证的可信代码。

在 STM32 等 MCU 上,我们可以利用芯片内置的安全特性来实现。

以 STM32H7 系列为例,可以这样配置安全启动:

// 配置安全启动
void ConfigureSecureBoot(void)
{
    // 使能Flash读保护
    FLASH_OBProgramInitTypeDef OBInit;
    
    HAL_FLASHEx_OBGetConfig(&OBInit);
    
    // 设置读保护级别为Level 2(最高级别,不可逆)
    // 注意:Level 2设置后无法回退,请谨慎使用
    OBInit.RDPLevel = OB_RDP_LEVEL_1; // 先用Level 1测试
    OBInit.OptionType = OPTIONBYTE_RDP;
    
    HAL_FLASH_Unlock();
    HAL_FLASH_OB_Unlock();
    
    if(HAL_FLASHEx_OBProgram(&OBInit) != HAL_OK)
    {
        // 配置失败处理
        Error_Handler();
    }
    
    HAL_FLASH_OB_Launch(); // 加载新的选项字节
    HAL_FLASH_OB_Lock();
    HAL_FLASH_Lock();
}

这段代码配置了 Flash 读保护,防止攻击者通过调试接口读取固件。

在实际项目中,我们还需要配合数字签名验证,确保固件的完整性。

2.2 数据加密存储

对于敏感数据,必须进行加密存储。

我在做车载系统时,用户的个人信息、车辆识别码等数据都需要加密保存。

可以使用 AES 算法,STM32 的硬件加密模块可以大大提高加密效率。

// 使用STM32硬件AES加密数据
HAL_StatusTypeDef EncryptData(uint8_t *plaintext, uint8_t *key, 
                               uint8_t *iv, uint8_t *ciphertext, uint32_t length)
{
    CRYP_HandleTypeDef hcryp;
    
    // 配置AES-128-CBC模式
    hcryp.Instance = AES;
    hcryp.Init.DataType = CRYP_DATATYPE_8B;
    hcryp.Init.KeySize = CRYP_KEYSIZE_128B;
    hcryp.Init.pKey = (uint32_t *)key;
    hcryp.Init.pInitVect = (uint32_t *)iv;
    hcryp.Init.Algorithm = CRYP_AES_CBC;
    
    if(HAL_CRYP_Init(&hcryp) != HAL_OK)
    {
        return HAL_ERROR;
    }
    
    // 执行加密
    if(HAL_CRYP_Encrypt(&hcryp, (uint32_t *)plaintext, length, 
                        (uint32_t *)ciphertext, HAL_MAX_DELAY) != HAL_OK)
    {
        return HAL_ERROR;
    }
    
    return HAL_OK;
}

密钥管理也很关键。

千万不要把密钥硬编码在代码里,这是新手最容易犯的错误。

可以使用芯片的 OTP(一次性可编程)区域存储密钥,或者使用专门的安全芯片(如 TPM、SE)。

2.3 网络通信安全

对于联网的嵌入式设备,网络通信必须加密。

TLS/SSL 是标准做法。

在嵌入式 Linux 系统上,我们可以使用 mbedTLS 这样的轻量级库。

// mbedTLS建立安全连接示例(简化版)
int establish_secure_connection(const char *server_addr, int port)
{
    mbedtls_net_context server_fd;
    mbedtls_ssl_context ssl;
    mbedtls_ssl_config conf;
    mbedtls_x509_crt cacert;
    
    // 初始化
    mbedtls_net_init(&server_fd);
    mbedtls_ssl_init(&ssl);
    mbedtls_ssl_config_init(&conf);
    mbedtls_x509_crt_init(&cacert);
    
    // 连接服务器
    if(mbedtls_net_connect(&server_fd, server_addr, 
                           port_str, MBEDTLS_NET_PROTO_TCP) != 0)
    {
        printf("Failed to connect to server\n");
        return -1;
    }
    
    // 配置SSL/TLS
    mbedtls_ssl_config_defaults(&conf,
                                MBEDTLS_SSL_IS_CLIENT,
                                MBEDTLS_SSL_TRANSPORT_STREAM,
                                MBEDTLS_SSL_PRESET_DEFAULT);
    
    // 加载CA证书
    mbedtls_x509_crt_parse(&cacert, ca_cert_pem, ca_cert_pem_len);
    mbedtls_ssl_conf_ca_chain(&conf, &cacert, NULL);
    
    // 设置验证模式
    mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED);
    
    mbedtls_ssl_setup(&ssl, &conf);
    mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, 
                        mbedtls_net_recv, NULL);
    
    // 执行SSL握手
    while((ret = mbedtls_ssl_handshake(&ssl)) != 0)
    {
        if(ret != MBEDTLS_ERR_SSL_WANT_READ && 
           ret != MBEDTLS_ERR_SSL_WANT_WRITE)
        {
            printf("SSL handshake failed\n");
            return -1;
        }
    }
    
    printf("SSL connection established\n");
    return 0;
}

这段代码建立了一个 TLS 加密连接,并验证服务器证书。

在实际项目中,还需要考虑证书更新、证书吊销列表检查等问题。

3. 软件层面的安全实践

3.1 输入验证和边界检查

缓冲区溢出是最常见的安全漏洞之一。

我刚工作那会儿,写代码经常忽略边界检查,后来在代码审查中被前辈狠狠批评了一顿。

// 不安全的代码
void unsafe_copy(char *dest, char *src)
{
    strcpy(dest, src); // 危险!没有检查长度
}
​
// 安全的代码
void safe_copy(char *dest, char *src, size_t dest_size)
{
    if(dest == NULL || src == NULL || dest_size == 0)
    {
        return;
    }
    
    // 使用安全的字符串函数
    strncpy(dest, src, dest_size - 1);
    dest[dest_size - 1] = '\0'; // 确保字符串终止
    
    // 或者使用更安全的函数(C11标准)
    // strncpy_s(dest, dest_size, src, dest_size - 1);
}
​
// 处理网络数据时的验证
HAL_StatusTypeDef ProcessNetworkData(uint8_t *data, uint32_t length)
{
    // 验证数据长度
    if(length < MIN_PACKET_SIZE || length > MAX_PACKET_SIZE)
    {
        printf("Invalid packet length: %lu\n", length);
        return HAL_ERROR;
    }
    
    // 验证数据包头
    PacketHeader *header = (PacketHeader *)data;
    if(header->magic != PACKET_MAGIC)
    {
        printf("Invalid packet magic\n");
        return HAL_ERROR;
    }
    
    // 验证校验和
    uint32_t calculated_crc = CalculateCRC32(data, length - 4);
    uint32_t received_crc = *(uint32_t *)(data + length - 4);
    if(calculated_crc != received_crc)
    {
        printf("CRC check failed\n");
        return HAL_ERROR;
    }
    
    // 数据验证通过,继续处理
    return HAL_OK;
}

所有外部输入都应该被视为不可信的,包括网络数据、用户输入、传感器数据等。

永远不要假设输入是合法的。

3.2 最小权限原则

在嵌入式 Linux 系统中,应该遵循最小权限原则。

不要让所有进程都以 root 权限运行。

我在做项目时,会为不同的功能模块创建专门的用户,限制其权限。

// 降低进程权限
void drop_privileges(const char *username)
{
    struct passwd *pw = getpwnam(username);
    if(pw == NULL)
    {
        fprintf(stderr, "User %s not found\n", username);
        exit(1);
    }
    
    // 先设置组ID
    if(setgid(pw->pw_gid) != 0)
    {
        perror("setgid failed");
        exit(1);
    }
    
    // 再设置用户ID
    if(setuid(pw->pw_uid) != 0)
    {
        perror("setuid failed");
        exit(1);
    }
    
    printf("Dropped privileges to user %s\n", username);
}
​
int main(int argc, char *argv[])
{
    // 初始化需要root权限的操作
    init_hardware();
    bind_privileged_port();
    
    // 完成后立即降低权限
    drop_privileges("app_user");
    
    // 后续代码以普通用户权限运行
    run_application();
    
    return 0;
}

3.3 安全的 OTA 升级

远程升级功能很方便,但也带来了安全风险。

必须确保升级包的完整性和来源可信。

// OTA升级安全验证流程
typedef struct {
    uint32_t version;
    uint32_t size;
    uint8_t signature[256]; // RSA-2048签名
    uint8_t hash[32];       // SHA-256哈希
} FirmwareHeader;
​
HAL_StatusTypeDef VerifyAndInstallFirmware(uint8_t *firmware_data, 
                                           uint32_t data_size)
{
    FirmwareHeader *header = (FirmwareHeader *)firmware_data;
    uint8_t *firmware_body = firmware_data + sizeof(FirmwareHeader);
    uint32_t body_size = data_size - sizeof(FirmwareHeader);
    
    // 1. 验证版本号(防止降级攻击)
    if(header->version <= GetCurrentFirmwareVersion())
    {
        printf("Firmware version too old, rejecting\n");
        return HAL_ERROR;
    }
    
    // 2. 验证哈希值
    uint8_t calculated_hash[32];
    SHA256_Calculate(firmware_body, body_size, calculated_hash);
    if(memcmp(calculated_hash, header->hash, 32) != 0)
    {
        printf("Hash verification failed\n");
        return HAL_ERROR;
    }
    
    // 3. 验证数字签名
    if(RSA_Verify(header->hash, 32, header->signature, 
                  public_key) != HAL_OK)
    {
        printf("Signature verification failed\n");
        return HAL_ERROR;
    }
    
    // 4. 所有验证通过,开始安装
    printf("Firmware verified, installing...\n");
    if(InstallFirmware(firmware_body, body_size) != HAL_OK)
    {
        printf("Installation failed\n");
        return HAL_ERROR;
    }
    
    printf("Firmware installed successfully\n");
    return HAL_OK;
}

这个流程包含了版本检查、完整性验证和签名验证三重保护。

在实际项目中,我还会加上回滚机制,确保升级失败时能恢复到旧版本。

4. 硬件层面的安全措施

4.1 禁用调试接口

生产环境的设备应该禁用或保护调试接口。

我见过有些产品为了方便售后调试,保留了 JTAG 接口,结果被人利用来提取固件。

// 在生产固件中禁用调试功能
void DisableDebugInterfaces(void)
{
    #ifdef PRODUCTION_BUILD
    
    // 禁用JTAG和SWD
    __HAL_AFIO_REMAP_SWJ_DISABLE();
    
    // 禁用调试模式下的功能
    DBGMCU->CR = 0x00000000;
    
    // 关闭调试串口
    #ifdef DEBUG_UART
    HAL_UART_DeInit(&huart_debug);
    #endif
    
    #endif
}

4.2 使用安全芯片

对于高安全要求的应用,可以使用专门的安全芯片(如 ATECC608A)来存储密钥和执行加密运算。

这些芯片具有防篡改特性,即使物理攻击也很难提取密钥。

// 使用安全芯片进行认证
HAL_StatusTypeDef AuthenticateWithSecureElement(void)
{
    uint8_t challenge[32];
    uint8_t response[64];
    
    // 生成随机挑战
    GenerateRandomChallenge(challenge, 32);
    
    // 发送挑战到安全芯片,获取签名响应
    if(SecureElement_Sign(challenge, 32, response) != HAL_OK)
    {
        return HAL_ERROR;
    }
    
    // 验证响应
    if(VerifySignature(challenge, 32, response, 64) != HAL_OK)
    {
        return HAL_ERROR;
    }
    
    return HAL_OK;
}

5. 数据保护的法律合规

5.1 遵守数据保护法规

现在各国都有严格的数据保护法规,比如欧盟的 GDPR、中国的《个人信息保护法》等。

作为开发者,我们必须了解并遵守这些法规。

在我做车载系统时,需要收集用户的位置信息、驾驶习惯等数据。

我们必须做到:明确告知用户收集哪些数据、用于什么目的;获得用户明确同意;提供数据删除功能;数据传输和存储必须加密;定期进行安全审计。

5.2 数据最小化原则

只收集必要的数据,不要贪多。

我见过有些产品恨不得把用户所有信息都收集起来,结果不仅增加了安全风险,还可能违反法规。

// 数据收集示例:只收集必要信息
typedef struct {
    uint32_t device_id;        // 设备ID(匿名化)
    uint32_t timestamp;        // 时间戳
    float temperature;         // 温度数据
    uint8_t error_code;        // 错误码
    // 不收集用户个人身份信息
} TelemetryData;
​
void CollectTelemetry(void)
{
    TelemetryData data;
    
    data.device_id = GetAnonymizedDeviceID(); // 使用匿名化ID
    data.timestamp = GetCurrentTimestamp();
    data.temperature = ReadTemperature();
    data.error_code = GetLastError();
    
    // 加密后上传
    EncryptAndUpload(&data, sizeof(data));
}

6. 安全开发流程

6.1 威胁建模

在项目开始阶段就应该进行威胁建模,识别潜在的安全风险。

我现在做新项目时,会专门组织团队进行威胁分析会议,列出所有可能的攻击面和攻击方式,然后针对性地设计防护措施。

6.2 代码审查和安全测试

代码审查不仅要关注功能实现,更要关注安全问题。

我在公司推行的代码审查 checklist 中,专门有一个安全检查部分,包括:是否有缓冲区溢出风险;是否正确处理错误;是否有硬编码的密钥或密码;是否验证了所有输入;是否使用了安全的 API 等。

定期进行渗透测试也很重要。

可以请专业的安全团队来测试,或者使用自动化工具扫描漏洞。

6.3 安全事件响应计划

即使做了充分的防护,也不能保证 100% 安全。

必须有应急响应计划。

我们公司的应急预案包括:漏洞发现后的上报流程;紧急补丁的发布机制;用户通知方案;事后分析和改进措施等。

7. 总结与建议

网络安全和数据保护是一个系统工程,需要从硬件、软件、流程等多个层面综合考虑。

作为嵌入式开发者,我们不能只关注功能实现,更要把安全放在首位。

我的建议是:第一,从项目一开始就考虑安全,不要等到出问题才补救。

第二,持续学习最新的安全技术和攻击手段,安全是一个动态的过程。

第三,建立安全意识,让团队每个人都重视安全;第四,遵守法律法规,保护用户隐私。

在我创业做咨询的这些年,接触了很多嵌入式项目,发现安全问题往往不是技术难题,而是意识问题。

很多团队觉得自己的产品不会成为攻击目标,或者为了赶进度忽略安全。

但一旦出事,代价是巨大的,不仅是经济损失,更是品牌信誉的损害。

希望这篇文章能给大家一些启发,让我们一起构建更安全的嵌入式系统。

如果你在项目中遇到安全相关的问题,欢迎和我交流讨论。

更多编程学习资源

注册即送 5 刀,邀请再送 5 刀,有点虚假了,是送 1 刀,实际上 0.2 倍率,可以达到官网 5 刀的效果

  • 为什么选我们家?多种渠道混合选择,富哥用 ccmax ,小老弟用 kiro ,由于中转经常抽风,不耽误大家工作,我们接入了多个 kiro 跟 ccmax 供大家选择
  • 极致的低价,kiro 渠道 0.2 倍率,套餐 ccmax 渠道低至 8 毛,基本上在赔本卖
  • 近期我们在考虑组建 ccmax 4-5 人小车队的事宜,欢迎老哥们加入详聊

https://terminal.pub

注册后留下自己的 id 进行抽奖

下周 5 ,上证指数 hash 取模,让 gemini 抽 5 个套餐

欢迎加入我们的 tg 跟微信,交流 vibecoding 创业想法

tg:

https://t.me/+4D5U2msTRshkZmFl

wechat:

现状就是在郊区长大,30 岁了,没有能力在城区买同等面积的房,还差两三百万,而且就算移居到城六区,也是一样的差空气一样的拥挤。
郊区很腐败,很官僚,我不清楚城区比如朝阳是不是稍微好一点,但至少看小红书,城区办事效率高很多。
郊区修一条路从去年 7 月开始干,埋地下管线,现在施工退场了,路还是破破烂烂的,这是国道主路,还不如农村柏油路平整。
我之前发了个帖子,说老旧小区改造,未经全体业主同意,把我家住了二十多年的蓝色外立面商品房染成了红色的。北京市都在改造,朝阳我去看房见到一个小区人家直接把效果图贴在小区大门的告示栏上,人人路过都知道会变成什么样子,而我这郊区几乎每个老旧小区都是开盲盒,施工质量非常差。
再就是我感觉这里人都非常有攻击性,侵略性。上下班路上竟是打架的,地铁也挤,开车也挤,不知道这些人挣不了多少钱天天瞎挤什么。
上班 8 年了,累了,刚好老婆公司是苏州的,社保也是苏州的,我们生孩子,和苏州医保局打了不少电话,感觉那边人更温和,就事论事,我也想过是不是换个城市生活了。
再就是气候太差了,前几天是沙尘暴,今天又下雪,北京人每年得有 1/3 的日子在对抗暴雨大风大雪严寒酷暑极端天气和干燥灰尘的空气。
父母肯定不走了,就是我们一家三口,杭州,苏州,上海压力大,但是可能收入也高,只要环境能改善,租房能上学,也没什么大困难,每天开开心心最重要。。。

概述

Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。

  • 在使用Lock之前,使用的最多的同步方式应该是synchronized关键字来实现同步方式了。配合Object的wait()、notify()系列方法可以实现等待/通知模式。
  • Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。

Object和Condition接口的一些对比。

对比项Object 监视器方法Condition
前置条件获取对象的监视器锁调用 Lock.lock() 获取锁调用 Lock.newCondition() 获取 Condition 对象
调用方法直接调用如:object.wait()直接调用如:condition.await()
等待队列个数一个多个
当前线程释放锁并进入等待队列支持支持
当前线程释放锁并进入等待队列,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态到将来的某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持

接口的介绍与示例

首先需要明白condition对象是依赖于lock对象的,意思就是说condition对象需要通过lock对象进行创建出来(调用Lock对象的newCondition()方法)。condition的使用方式非常的简单。但是需要注意在调用方法前获取锁。

/**
 * condition使用示例:
 * 1、condition的使用必须要配合锁使用,调用方法时必须要获取锁
 * 2、condition的创建依赖于Lock lock.newCondition();
 */
public class ConditionUseCase {
    /**
     * 创建锁
     */
    public Lock readLock = new ReentrantLock();
    /**
     * 创建条件
     */
    public Condition condition = readLock.newCondition();

    public static void main(String[] args) {
        ConditionUseCase useCase = new ConditionUseCase();
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.execute(() -> {
            //获取锁进行等待
            useCase.conditionWait();
        });
        executorService.execute(() -> {
            //获取锁进行唤起读锁
            useCase.conditionSignal();
        });
    }

    /**
     * 等待线程
     */
    public void conditionWait() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            System.out.println(Thread.currentThread().getName() + "等待信号");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "拿到信号");
        } catch (Exception e) {

        } finally {
            readLock.unlock();
        }
    }

    /**
     * 唤起线程
     */
    public void conditionSignal() {
        readLock.lock();
        try {
            //睡眠5s 线程1启动
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            condition.signal();
            System.out.println(Thread.currentThread().getName() + "发出信号");
        } catch (Exception e) {

        } finally {
            //释放锁
            readLock.unlock();
        }
    }

}

//执行结果
1 pool-1-thread-1拿到锁了
 2 pool-1-thread-1等待信号 ---释放锁-线程等待 t1
 3 pool-1-thread-2拿到锁了
 4 pool-1-thread-2发出信号 --- 唤起线程t2释放锁
 5 pool-1-thread-1拿到信号---t1继续执行

如示例所示,一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

接口常用方法

condition可以通俗的理解为条件队列。当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

  1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
  2. boolean await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态---》是否超时,超时异常
  3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
  4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
  5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
  6. signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
  7. signalAll() :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

这里顺便回顾一下 Object 类的主要方法:

  1. wait():线程等待直到被通知或者中断。
  2. wait(long timeout):线程等待指定的时间,或被通知,或被中断。
  3. wait(long timeout, int nanos):线程等待指定的时间,或被通知,或被中断。
  4. notify():唤醒一个等待的线程。
  5. notifyAll():唤醒所有等待的线程。

原理解析

Condition是AQS的内部类。可以通过 Lock.newCondition() 方法获取 Condition 对象,而 Lock 对于同步状态的实现都是通过内部的自定义同步器实现的,newCondition() 方法也不例外,所以,Condition 接口的唯一实现类是同步器 AQS 的内部类 ConditionObject,因为 Condition 的操作需要获取相关的锁,所以作为同步器的内部类也比较合理,该类定义如下:

public class ConditionObject implements Condition, java.io.Serializable

等待队列

前面我们学过,AQS 内部维护了一个先进先出(FIFO)的双端队列,并使用了两个引用 head 和 tail 用于标识队列的头部和尾部。

Condition 内部也使用了同样的方式,内部维护了一个先进先出(FIFO)的单向队列,我们把它称为等待队列。该队列是 Condition 对象实现等待 / 通知功能的关键。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。

事实上,节点的定义复用了 AQS 中 Node 节点的定义,也就是说,同步队列和等待队列中节点类型都是 AQS 的静态内部类 AbstractQueuedSynchronized.Node。一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用 Condition.await() 方法之后,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下所示。

  • 等待队列分为首节点和尾节点。当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。
  • 新增节点就是将尾部节点指向新增的节点。节点引用更新本来就是在获取锁以后的操作,所以不需要CAS保证。同时也是线程安全的操作。
public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;
}

在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列

等待 await 方法

  • 当线程调用了await方法以后。线程就作为队列中的一个节点被加入到等待队列中去了。同时会释放锁的拥有。
  • 当从await方法返回的时候。一定会获取condition相关联的锁。当等待队列中的节点被唤醒的时候,则唤醒节点的线程开始尝试获取同步状态。

    • 如果不是通过 其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常信息。
    • 通知调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到同步队列中。

当前线程加入到等待队列中如图所示:

源码如下:

public final void await() throws InterruptedException {
    // 检测线程中断状态
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程包装为Node节点加入等待队列
    Node node = addConditionWaiter();
    // 释放同步状态,也就是释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 检测该节点是否在同步队中,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
    while (!isOnSyncQueue(node)) {
        // 挂起线程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 竞争同步状态
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 清理条件队列中的不是在等待条件的节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

调用该方法的线程是成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

可能会有这样几个问题:

  1. 怎样将当前线程添加到等待队列中?
  2. 释放锁的过程是?
  3. 怎样才能从 await 方法中退出?

问题1:怎样将当前线程添加到等待队列中?

调用 addConditionWaiter 方法会将当前线程添加到等待队列中,源码如下:

private Node addConditionWaiter() {
    // 尾节点
    Node t = lastWaiter;
    // 尾节点如果不是CONDITION状态,则表示该节点不处于等待状态,需要清理节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 根据当前线程创建Node节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 将该节点加入等待队列的末尾
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

首先将 t 指向尾节点,如果尾节点不为空并且它的waitStatus!=-2(-2 为 CONDITION,表示正在等待 Condition 条件),则将不处于等待状态的节点从等待队列中移除,并且将 t 指向新的尾节点。

然后将当前线程封装成 waitStatus 为-2 的节点追加到等待队列尾部。

如果尾节点为空,则表明队列为空,将首尾节点都指向当前节点。

img

如果尾节点不为空,表明队列中有其他节点,则将当前尾节点的 nextWaiter 指向当前节点,将当前节点置为尾节点。

img

简单总结一下,这段代码的作用就是通过尾插入的方式将当前线程封装的 Node 插入到等待队列中,同时可以看出,Condtion 的等待队列是一个不带头节点的链式队列,之前我们学习 AQS 时知道同步队列是一个带头节点的链式队列,这是两者的一个区别。

关于头节点的作用,我们这里简单说明一下。

不带头节点是指在链表数据结构中,链表的第一个节点就是实际存储的第一个数据元素,而不是一个特定的"头"节点,该节点不包含实际的数据。

1)不带头节点的链表:

  • 链表的第一个节点就是第一个实际的数据节点。
  • 当链表为空时,头引用(通常称为 head)指向 null。

2)带头节点的链表:

  • 链表有一个特殊的节点作为链表的开头,这个特殊的节点称为头节点。
  • 头节点通常不存储任何实际数据,或者它的数据字段不被使用。
  • 无论链表是否为空,头节点总是存在的。当链表为空时,头节点的下一个节点指向 null。
  • 使用头节点可以简化某些链表操作,因为你不必特殊处理第一个元素的插入和删除。

为了更好地解释这两种链表结构,我将为每种结构提供一个简单的整数链表插入方法的示例。

1)不带头节点的链表

public class Node {
    public int data;
    public Node next;

    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}

public class LinkedListWithoutHead {
    public Node head;

    public void insert(int value) {
        Node newNode = new Node(value);
        if (head == null) {
            head = newNode;
        } else {
            Node temp = head;
            while (temp.next != null) {
                temp = temp.next;
            }
            temp.next = newNode;
        }
    }
}

2)带头节点的链表

public class NodeWithHead {
    public int data;
    public NodeWithHead next;

    public NodeWithHead(int data) {
        this.data = data;
        this.next = null;
    }
}

public class LinkedListWithHead {
    private NodeWithHead head;

    public LinkedListWithHead() {
        head = new NodeWithHead(-1);  // 初始化头节点
    }

    public void insert(int value) {
        NodeWithHead newNode = new NodeWithHead(value);
        NodeWithHead temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = newNode;
    }
}

这下是不是就彻底明白了?说明白了头节点,我们再回到 Condition 的 await 方法。

问题 2:释放锁的过程是?

将当前线程加入到等待队列之后,需要释放同步状态,该操作通过 fullyRelease(Node) 方法来完成:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 获取同步状态
        int savedState = getState();
        // 释放锁
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

这段代码也很容易理解,调用 AQS 的模板方法 release 释放 AQS 的同步状态并且唤醒在同步队列中头节点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。

问题3:怎样才能从 await 方法中退出?

怎样从 await 方法退出呢?现在回过头再来看 await 方法,其中有这样一段逻辑:

while (!isOnSyncQueue(node)) {
    // 3. 当前线程进入到等待状态
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

isOnSyncQueue 方法用于判断当前线程所在的 Node 是否在同步队列中:

final boolean isOnSyncQueue(Node node) {
    // 节点状态为CONDITION,或者前驱节点为null,返回false
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 后继节点不为null,那么肯定在同步队列中
    if (node.next != null) // If has successor, it must be on queue
        return true;
    
    return findNodeFromTail(node);
}

如果当前节点的 waitStatus=-2,说明它在等待队列中,返回 false;如果当前节点有前驱节点,则证明它在 AQS 队列中,但是前驱节点为空,说明它是头节点,而头节点是不参与锁竞争的,也返回 false。

如果当前节点既不在等待队列中,又不是 AQS 中的头节点且存在 next 节点,说明它存在于 AQS 中,直接返回 true。

这里有必要给大家看一下同步队列与等待队列的关系图了。

img

当线程第一次调用 condition.await 方法时,会进入到这个 while 循环,然后通过 LockSupport.park(this) 使当前线程进入等待状态,那么要想退出 await,第一个前提条件就是要先退出这个 while 循环,出口就只两个地方:

  1. 走到 break 退出 while 循环;
  2. while 循环中的逻辑判断为 false。

出现第 1 种情况的条件是,当前等待的线程被中断后代码会走到 break 退出,第 2 种情况是当前节点被移动到了同步队列中(即另外一个线程调用了 condition 的 signal 或者 signalAll 方法),while 中逻辑判断为 false 后结束 while 循环。

总结一下,退出 await 方法的前提条件是当前线程被中断或者调用 condition.signal 或者 condition.signalAll 使当前节点移动到同步队列后

当退出 while 循环后会调用acquireQueued(node, savedState),该方法的作用是在自旋过程中线程不断尝试获取同步状态,直到成功(线程获取到 lock)。这样也说明了退出 await 方法必须是已经获得了 condition 引用(关联)的 lock

到目前为止,上文提到的三个问题,我们都通过阅读源码的方式找到了答案,也加深了对 await 方法的理解。await 方法示意图如下:

await方法示意图

如图,调用 condition.await 方法的线程必须是已经获得了 lock 的线程,也就是当前线程是同步队列中的头节点。调用该方法后会使得当前线程所封装的 Node 尾插入到等待队列中。

超时机制的支持

condition 还额外支持超时机制,使用者可调用 awaitNanos、awaitUtil 这两个方法,实现原理基本上与 AQS 中的 tryAcquire 方法如出一辙。

不响应中断的支持

要想不响应中断可以调用 condition.awaitUninterruptibly() 方法,该方法的源码如下:

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if (Thread.interrupted())
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        selfInterrupt();
}

这段方法与上面的 await 方法基本一致,只不过减少了对中断的处理。

通知-signal/signalAll 实现原理

调用 condition 的 signal 或者 signalAll 方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得 lock。等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用 condition 的 signal 方法都会将头节点移动到同步队列中。

在调用signal()方法之前必须先判断是否获取到了锁。接着获取等待队列的首节点,将其移动到同步队列并且利用LockSupport唤醒节点中的线程。节点从等待队列移动到同步队列如下图所示:

被唤醒的线程将从await方法中的while循环中退出。随后加入到同步状态的竞争当中去。成功获取到竞争的线程则会返回到await方法之前的状态。

源码如下:

调用 Condition 的 signal() 方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。Condition 的 signal() 方法如下所示:

public final void signal() {
    // 判断是否是当前线程获取了锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 唤醒等待队列的首节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

该方法最终调用 doSignal(Node) 方法来唤醒节点:

private void doSignal(Node first) {
    do {
        // 把等待队列的首节点移除之后,要修改首结点
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
}

真正对头节点做处理的逻辑,将节点移动到同步队列是通过 transferForSignal(Node) 方法完成的:

final boolean transferForSignal(Node node) {
    // 尝试将该节点的状态从CONDITION修改为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    
    // 将节点加入到同步队列尾部,返回该节点的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 如果前驱节点的状态为CANCEL或者修改waitStatus失败,则直接唤醒当前线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

这段代码主要做了两件事情:

  1. 将头节点的状态更改为 CONDITION;
  2. 调用 enq 方法,将该节点尾插入到同步队列中,关于 enq 方法请看 AQS 的底层实现这篇文章

节点从等待队列移动到同步队列的过程如下图所示:

被唤醒后的线程,将从 await() 方法中的 while 循环中退出(因为此时 isOnSyncQueue(Node) 方法返回 true),进而调用 acquireQueued() 方法加入到获取同步状态的竞争中。

成功获取了锁之后,被唤醒的线程将从先前调用的 await() 方法返回,此时,该线程已经成功获取了锁。

signalAll()

sigllAll 与 sigal 方法的区别体现在 doSignalAll 方法上,前面我们已经知道 doSignal 方法只会对等待队列的头节点进行操作, signalAll() 方法相当于对等待队列的每个节点均执行一次 signal() 方法,效果就是将等待队列中的所有节点移动到同步队列中。doSignalAll 的源码如下:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

该方法会将等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用 condition.await() 方法的每一个线程。

await 与 signal/signalAll

文章开篇提到的等待/通知机制,通过 condition 的 await 和 signal/signalAll 方法就可以实现,而这种机制能够解决最经典的问题就是“生产者与消费者问题

await、signal 和 signalAll 方法就像一个开关,控制着线程 A(等待方)和线程 B(通知方)。它们之间的关系可以用下面这幅图来说明,会更贴切:

线程 awaitThread 先通过 lock.lock() 方法获取锁,成功后调用 condition.await 方法进入等待队列,而另一个线程 signalThread 通过 lock.lock() 方法获取锁成功后调用了 condition.signal 或者 signalAll 方法,使得线程 awaitThread 能够有机会移入到同步队列中,当其他线程释放 lock 后使得线程 awaitThread 能够有机会获取 lock,从而使得线程 awaitThread 能够从 await 方法中退出并执行后续操作。如果 awaitThread 获取 lock 失败会直接进入到同步队列。

总结

  • 调用await方法后,将当前线程加入Condition等待队列中。当前线程释放锁。否则别的线程就无法拿到锁而发生死锁。自旋(while)挂起,不断检测节点是否在同步队列中了,如果是则尝试获取锁,否则挂起。
  • 当线程被signal方法唤醒,被唤醒的线程将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。

Generate Random String in Bash

随机字符串是随机生成的字符序列,而不是由固定的模式或预先决定的序列。随机字符串通常用作密码、密钥或标识符。Bash 中生成随机字符串非常简单和便利,Bash 中可以使用几种内置程序和命令生成随机字符串的。在本文中,我们将探索实现这一目标的各种方法,并提供相关的示例。

1. 使用 $RANDOM 变量

Bash shell 提供了一个名为 $RANDOM 的特殊变量,它返回 0 到 32767 之间的随机数。您可以利用这个变量来生成一个随机字符串。

echo $RANDOM

但是,这只会返回一个数字。如果您想要一个包含字符的字符串,那么您可能需要更具创造性一些。

echo $(date +%s%N) | sha256sum | head -c 10

该命令使用当前时间戳(以纳秒为单位),并将其输送到 sha256sum 命令,然后取前 10 个字符。

2. 使用 /dev/urandom

/dev/urandom 是一个设备文件,它提供了一个加密安全的随机数源。

例如,生成一个长度为 10 的随机字符串

cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 10 | head -n 1

相关参数说明如下:

  • cat /dev/urandom : 从 /dev/urandom 输出随机字节
  • tr -dc 'a-zA-Z0-9' : 删除非字母数字的字符
  • fold -w 10 : 将输出封装为 10 个字符宽,实际上每行 10 个字符
  • head -n 1 : 只需要第一行

3. 使用 openssl

可以使用 openssl 命令和 base64 编码函数,例如:生成长度为 10 的随机字符串(包括字母、数字和特殊字符)

openssl rand -base64 10

你也可以使用 tr 命令删除不希望包含的任何字符。例如:生成一个长度为 10 的随机字符串(只包含大写字母和数字)

openssl rand -base64 10 | tr -dc 'A-Z0-9'

4. 使用 pwgen

pwgen 是一个专门用来生成密码的工具,也就是说可以产生随机字符串。如果你没有安装它,你可以通常通过包管理器(apt-get、 yum、 brew)获取安装。

例如,生成一个长度为 10 的随机字符串。

pwgen 10 1

5. 使用 Bash Arrays

您可以将 bash 数组与 $RANDOM 组合以生成随机字符串。

ARRAY=('a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z')
RAND_STR=""
for i in {1..10}; do
  RAND_STR+="${ARRAY[$RANDOM % ${#ARRAY[@]}]}"
done
echo $RAND_STR

我的开源项目

酷瓜云课堂-开源知识付费解决方案

我把它叫做 [举起手来] 视频播放器。

如图

你需要打开电脑摄像头,选择本地视频文件,然后举起你的双手,视频才会播放

如果你中途放下你的手,视频会自动暂停

PixPin_2026-02-26_23-13-25.png
PixPin_2026-02-26_23-18-21.png

你们一定想找这种播放器很久了吧 [doge]

哈哈哈哈哈哈哈哈

https://handsup.1link.fun/

对了,上次做的颈椎贪吃蛇游戏(摄像头识别转头方向控制贪吃蛇游戏),正在做桌面离线版本,进度大概 80% 了。

玩键盘这件事,我算是折腾了挺多年。

前前后后入手过十几把,青轴、红轴、茶轴、黑轴、矮轴都试过,Cherry、佳达隆、凯华这些主流轴体也基本体验了一遍,各种配列从 100%、75% 到 60% 也都换着用。但说到底,不管怎么换,始终都没跳出 QWERTY 标准键盘的架构。

折腾到最后,我一度觉得找到了自己的舒适区 —— NiZ 静电容键盘,手感对我最友好,甚至打算就用它「养老」,也确实安安静静用了好几年。

但最近还是没忍住,又一次开始「剁手」了,决定试试人体工学键盘。

纠结了很久,最终选了最便宜的一款,毕竟不确定自己能不能适应。布局也挑了最容易上手的 ALICE 布局,先从入门款开始体验。

640

拿到手刚用三天,感受还不错。

第一天完全不适应,键位经常按错,打一句话就要改两三个字,磕磕绊绊很不习惯。

第二天开始明显好转,感觉肌肉记忆慢慢形成了,打中文已经能比较流畅地输入 (不得不说微信输入法的自动纠错帮了不少忙)。不过输入英文,尤其是无规律的验证码,还是得低头找键。

整体适应速度比我预想的要好,网上说一般 7 天左右能恢复正常输入效率,照目前的状态,应该差不多。

还有个意外收获:强制纠正了我不标准的指法。比如我以前习惯用右手按 B 和 F6 等键位,用人体工学键盘后,不得不改回正确手法,也算一个正向改变。

640 (1)

用到现在也就两三天,输入效率在慢慢回升,但还没到平时的水平。至于大家说的,对肩膀、手臂、尤其是手腕的舒适度提升,目前还没明显感受到,应该需要更长时间才能体现出来。

另外也有个小疑问:等彻底适应人体工学布局后,再用回 MacBook 自带的标准键盘,会不会反而不习惯?这个只能后续观察了。

640 (2)

这篇就单纯是个初体验记录,等用一两个月之后,我再写一篇完整的使用报告。如果确实好用、值得推荐,到时候也会整理出来跟大家详细分享。


全文链接 人体工学键盘初体验

最近一段时间,我发现晚上翻墙网速特别卡,查来查去,问了 AI ,一直以为是我的翻墙服务器的网络对 CN2 太慢,总想着要不要换。然而,昨晚忽然发现,使用手机流量使用相同翻墙网络,速度一下子就恢复到白天的速度了,而切回和电脑一样的 Wi-Fi 就卡住了,我才想起,之前似乎有看到移动宽带使用的是小区共享网络。晚上同小区的用户多了,就会卡住。但是,国内网站访问速度并不慢,我怀疑是移动自己的国际出口太小导致的。

另外,我的手机使用的也是移动,这就很吊诡了,同一家运营商,咋就区别这么大呢?

看来还是要换到电信宽带才能保证速度了。

坐标深圳宝安区,这边工厂比较多,晚上出去逛了逛,发现很多商铺都没开门,人也少了很多。
问一下,还有公司在放假的吗?(什么时候上班的)你们开工红包多少?

我先来:

24 号上班,100

最近刷到领克 z20 语言关阅读灯结果关了全部灯光的事件,司机为啥不直接用物理开关打开大灯,从结果来看,不支持语言开大灯啊。

PS:那些天天鼓吹不需要物理安静的司机还好吗?

都说职场如戏,全靠演技。但有时候,现实剧情的荒诞程度,甚至都远超了你我的想象。

过年假期在家刷到这样一个职场帖子,看完是既觉得搞笑,又觉得无奈,原文大概是这样描述的:

“有个同事,去年年中的绩效是C,年底还是C。他主动提了离职,结果领导死活不让他走,还说后面还有机会。起初我还以为领导是真心挽留,结果他私下透露,他走了,就没人背这个绩效了。”

提到「绩效」这个事情,相信大多同学在公司都经历过。

尤其像是在年前、年后这阵,往往是和过去一年的年终奖什么的,都直接挂钩的。

本来「绩效」这个东西的推出初衷是为了激励团队进步、识别高潜人才,但到了执行层面,尤其是在一些管理能力欠缺的中层大聪明领导手中,它就异化成了一场充满算计的数字游戏。

第一种就是搞所谓的绩效锁定。

有些公司,为了所谓的优胜劣汰,实行强制分布式的绩效考核制度。

简单点来说,就是公司要求每个部门,必须按比例评出一定数量的 S、A、B,甚至 C。

初衷或许是好的,想打破大锅饭,但到了执行层面,当一个团队整体都很优秀,或者领导不想得罪人时,那个必须存在的 C,就成了一个烫手山芋。

于是,谁是那个最没有话语权、最不会向上管理、最不懂人情世故、或者最“老实”的人,谁就很有可能成为那个背锅的人。

还有一种公司也挺搞笑的,人家不搞绩效锁定,但是搞起了所谓的绩效轮转。

什么意思呢?

公司也是要求团队必须比例评出一定数量的 S、A、B、C 绩效,但是拿 C 的员工会轮着来。

这次你拿 C,下次他拿 C,然后上次拿过 C 的同事在后面的绩效评比中再通过给 S 再勾回来,以此达到一种动态的平衡。

说实话,这种比起上面那种所谓的绩效锁定,找人背 C 来说,看起来似乎还要稍微那么良心一点。

再回过头来看,像上面帖子中这位同事的遭遇,就是典型的绩效考核异化的受害者,纯纯的被当成了团队里背 C 的工具人。


那么,面对这种“被背锅”的局面,如果是你,你会怎么办呢?

这里也来聊聊我自己的几点想法。

首先,也是最重要的,就是心态的转变和调整。

我们首先要明白,绩效评价并不完全等同于你的个人实际价值。

拿了一个 C,只能代表在那个特定的时间、特定的领导、特定的游戏规则下,你被选中了。它不能定义你的技术能力,更不能否定你过去的努力。

拿到一个不公正的绩效评价,不一定是你能力的问题,有可能是公司制度的问题,或者是领导管理能力的问题,甚至有可能是某些人特意耍的心机而已。

不要轻易否定自己,不要让别人的错误,来惩罚自己。

其次,面对这种不公,沉默和忍让往往换不来尊重,只会让对方得寸进尺。

如果可能,尝试去沟通,去了解那个 C 背后的真实理由。

如果沟通无效,那么就要开始为自己谋划后路,同时注意保留好自己的工作成果、沟通记录等证据,以备不时之需。

最后,也是最重要的一点,我们要能有随时离开的勇气和能力。

这一点之前咱们这里多次提及,在技术行业,吃老本是最危险的。

当今的技术世界变化太快,而作为程序员的我们则恰好处于这一洪流之中,这既是挑战,也是机会。

还是那句话,一定要定期评估一下自己的市场价值:如果明天就离开现在的公司,你的技能和经验是否足以让你在市场上获得同等或更好的位置?

无论在公司工作多久,都要不断更新自己的技能和知识,确保自己始终具有市场竞争力。

有一说一,如果一个环境已经坏到需要你靠背锅来生存,那么这个地方,不待也罢。

虽然离开可能会面临短期的阵痛,比如空窗期、比如收入的暂时减少,但是从长远来看,未必是坏事。

因为虽说大环境寒冷,但有时候摆脱一个有毒的团队和一个扭曲的环境,才是对自己职业生涯最大的负责,大家觉得呢?

注:本文在GitHub开源仓库「编程之路」 https://github.com/rd2coding/Road2Coding 中已经收录,里面有我整理的6大编程方向(岗位)的自学路线+知识点大梳理、面试考点、我的简历、几本硬核pdf笔记,以及程序员生活和感悟,欢迎star。

我大多数情况都是跑步使用,每次使用完不太方便清理;

索性就直接放进去了也不影响听觉,但是就会越来越脏,如图。。。

是不是需要随手准备一把刷子用完刷一刷,或者有其它更方便的建议,请大佬们支招~

AirPods

AirPods

连续两天 还有谁 昨天一个老哥说 1 不是最低,0 才是最低,好家伙 今天直接喜提 0sobbing

图片

图片