2026年2月

在当今网络环境中,SSL证书已成为网站安全标配,它不仅保护用户数据传输安全,还能提升搜索引擎排名和用户信任度。对于大多数网站运营者来说,一年期SSL证书是性价比最高且管理相对便捷的选择。本文将为您详细解析一年期SSL证书的申请流程。

第一步:理解SSL证书类型

在申请前,您需要了解几种常见的SSL证书类型:

  1. 域名验证型(DV SSL) :仅验证域名所有权,适合个人网站和小型企业
  2. 组织验证型(OV SSL) :验证组织真实性,适合企业官网
  3. 扩展验证型(EV SSL) :提供最高级别的验证,适合金融和电商平台

一年期证书通常是DV或OV类型,EV证书多为两年期。

第二步:申请前的准备工作

  1. 确认域名所有权:确保您拥有要申请证书的域名管理权限
  2. 服务器环境检查:了解您的服务器类型(Apache、Nginx、IIS等)
  3. 生成CSR文件:这是证书签名请求文件,包含您的公钥和网站信息

生成CSR的方法:

  • 通过服务器控制面板(如cPanel)的SSL/TLS管理工具
  • 使用OpenSSL命令行工具
  • 部分证书提供商可在申请过程中帮助生成

第三步:申请SSL证书的具体步骤

免费SSL证书的获取入口

1、注册账号并登录

首先,您需要在JoySSL的官方网站上注册一个账号。访问JoySSL官网,点击右上角的“注册”按钮,进入注册页面。在注册过程中填写相关信息,最后一栏务必填写注册码230970,这是获取免费一年期证书的关键步骤。

2、选择并申请证书

登录成功后,您将看到JoySSL的用户界面。找到免费一年期单域名SSL证书,0元下单支付。

3、申请证书并验证域名所有权

提交申请后,您将进入域名验证环节。选择域名DNS解析或者服务器文件验证,根据系统提示完成相应验证步骤。然后点击“验证”按钮。如果一切正常,系统将提示您验证成功。

4、下载与安装证书

一般10分钟左右签发,您可以在用户后台的“我的证书”或找到新签发的证书,并点击下载,然后把证书文件部署到相应服务器上。

5、验证证书安装效果

证书安装完成后,通过浏览器访问您的网站,查看地址栏是否出现绿色的安全锁图标以及HTTPS访问。

大家好,我是 Java陈序员

在 AI 大模型应用爆发的当下,对接多平台接口、管理 API 密钥、控制调用权限......这些繁琐的操作是否让你头疼?

今天,给大家介绍一款开源的大模型网关神器,一站式解决大模型接口管理的所有痛点!

关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。

项目介绍

new-api —— 一款开源的大模型网关与 AI 资产管系统,定位为一站式 AI 资产管理网关,核心目标是提供大模型相关的网关管理与资产统筹能力。

功能特色

  • 适配全品类模型:一个 API 端点接入所有 AI 服务,支持 OpenAI、Moonshot、Zhipu、Anthropic Claude、Google Gemini、百度文心一言、讯飞星火等主流大模型,一套网关打通全品类 AI 服务
  • 智能调度渠道:支持渠道加权随机分配,可设置默认供应商,结合令牌分组、模型限制能力,灵活管控接口调用优先级与范围
  • 精细化资产管理:集成在线充值功能,可自定义充值金额选项、折扣规则,支持模型按次数收费,可批量设置模型固定价格、模型倍率、补全倍率
  • 细粒度权限控制:支持 GitHub OAuth、LinuxDO、Telegram、OIDC 等多方式授权登录,集成双因素认证,支持令牌分组管理,限制令牌可调用的模型范围,适配团队协作场景
  • 全新 UI:现代化的用户界面设计,支持深色/浅色主题,自动适配系统偏好,支持中文、英文、法语、日语多语言,提供可视化控制台与统计分析的数据看板

快速上手

new-api 支持 Docker 部署,可使用 Docker 快速部署。

Docker 部署

1、拉取镜像

docker pull calciumion/new-api

2、创建挂载目录

mkdir -p /data/software/new-api

3、启动容器

  • 使用 SQLite 数据库

    docker run -d --name new-api \
    --restart always \
    -p 3000:3000 \
    -e TZ=Asia/Shanghai \
    -v /data/software/new-api:/data \
    calciumion/new-api
  • 使用 MySQL 数据库(推荐)

    docker run -d --name new-api \
    --restart always \
    -p 3000:3000 \
    -e TZ=Asia/Shanghai \
    -e SQL_DSN="用户名:密码@tcp(数据库地址:3306)/数据库名" \
    -v /data/software/new-api:/data \
    calciumion/new-api
环境变量中的数据库连接信息需要传入对应的值。

4、容器运行成功后,浏览器访问

http://{IP/域名}:3000
首次安装需要按照页面指引手动设置管理员账号和密码,完成初始化后即可使用所设置的管理员账号登录系统。

Docker Compose 部署、

1、创建一个目录用于部署

mkdir new-api
cd new-api

2、在该目录下创建 docker-compose.yml 文件

  • 标准配置(生产环境)
# New-API Docker Compose Configuration
#
# Quick Start:
#   1. docker-compose up -d
#   2. Access at http://localhost:3000
#
# Using MySQL instead of PostgreSQL:
#   1. Comment out the postgres service and SQL_DSN line 15
#   2. Uncomment the mysql service and SQL_DSN line 16
#   3. Uncomment mysql in depends_on (line 28)
#   4. Uncomment mysql_data in volumes section (line 64)
#
# ⚠️  IMPORTANT: Change all default passwords before deploying to production!

version: '3.4' # For compatibility with older Docker versions

services:
  new-api:
    image: calciumion/new-api:latest
    container_name: new-api
    restart: always
    command: --log-dir /app/logs
    ports:
      - '3000:3000'
    volumes:
      - ./data:/data
      - ./logs:/app/logs
    environment:
      - SQL_DSN=postgresql://root:123456@postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
      #      - SQL_DSN=root:123456@tcp(mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL
      - REDIS_CONN_STRING=redis://redis
      - TZ=Asia/Shanghai
      - ERROR_LOG_ENABLED=true # 是否启用错误日志记录
      - BATCH_UPDATE_ENABLED=true # 是否启用批量更新 batch update enabled
    #      - STREAMING_TIMEOUT=300  # 流模式无响应超时时间,单位秒,默认120秒,如果出现空补全可以尝试改为更大值 Streaming timeout in seconds, default is 120s. Increase if experiencing empty completions
    #      - SESSION_SECRET=random_string  # 多机部署时设置,必须修改这个随机字符串!! multi-node deployment, set this to a random string!!!!!!!
    #      - SYNC_FREQUENCY=60  # Uncomment if regular database syncing is needed

    depends_on:
      - redis
      - postgres
    #      - mysql  # Uncomment if using MySQL
    healthcheck:
      test:
        [
          'CMD-SHELL',
          "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' || exit 1",
        ]
      interval: 30s
      timeout: 10s
      retries: 3

  redis:
    image: redis:latest
    container_name: redis
    restart: always

  postgres:
    image: postgres:15
    container_name: postgres
    restart: always
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: 123456 # ⚠️ IMPORTANT: Change this password in production!
      POSTGRES_DB: new-api
    volumes:
      - pg_data:/var/lib/postgresql/data
#    ports:
#      - "5432:5432"  # Uncomment if you need to access PostgreSQL from outside Docker

#  mysql:
#    image: mysql:8.2
#    container_name: mysql
#    restart: always
#    environment:
#      MYSQL_ROOT_PASSWORD: 123456  # ⚠️ IMPORTANT: Change this password in production!
#      MYSQL_DATABASE: new-api
#    volumes:
#      - mysql_data:/var/lib/mysql
#    ports:
#      - "3306:3306"  # Uncomment if you need to access MySQL from outside Docker

volumes:
  pg_data:
#  mysql_data:
生产环境下,请务必修改数据库密码。
  • 简化配置(测试环境)

    services:
    new-api:
      image: calciumion/new-api:latest
      container_name: new-api
      restart: always
      ports:
        - '3000:3000'
      environment:
        - TZ=Asia/Shanghai
      volumes:
        - ./data:/data

3、启动服务

docker compose up -d

4、容器运行成功后,浏览器访问

http://{IP/域名}:3000
首次安装需要按照页面指引手动设置管理员账号和密码,完成初始化后即可使用所设置的管理员账号登录系统。

功能体验

  • 操练场

  • 数据看板

  • 令牌管理

  • 使用日志

  • 钱包管理

  • 个人设置

  • 渠道管理

  • 兑换码管理

  • 用户管理

  • 系统设置

如果你正在被多平台 AI 接口管理困扰,想要一套轻量化、高拓展的大模型网关系统,new-api 值得一试!无论是个人学习、团队协作还是小型企业使用,都能满足你的需求。快去部署体验吧~

项目地址:https://github.com/QuantumNous/new-api

最后

推荐的开源项目已经收录到 GitHub 项目,欢迎 Star

https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:

https://chencoding.top:8090/#/

我创建了一个开源项目交流群,方便大家在群里交流、讨论开源项目

但是任何人在群里打任何广告,都会被 T 掉

如果你对这个交流群感兴趣或者在使用开源项目中遇到问题,可以通过如下方式进群

关注微信公众号:【Java陈序员】,回复【开源项目交流群】进群,或者通过公众号下方的菜单添加个人微信,并备注【开源项目交流群】,通过后拉你进群

大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!

formatTime.js

复制
import getDayStartTime from './getDayStartTime';
import formatToDate from './formatToDate';

/**
 * 获取事件发生到现在的可读时间
 * @param {number} time 事件发生时时间戳
 * @param {{
 * justNow: string;
 * minutesAgo: string;
 * hoursAgo: string;
 * yesterday : string;
 * future: string;
 * }} timeNames
 * @returns
 * 1 分钟以内 => 刚刚
 * [1 分钟,60 分钟) => n 分钟前(例:8 分钟前)
 * [1 小时,24 小时) => n 小时前(例: 8 小时前)
 * 其他 => 年-月-日(例: 2016-08-05)
 */
function formatTime(time, timeNames) {
  var date = formatToDate(time);
  if (!date) return '';
  if (!timeNames) timeNames = {};
  var justNow = timeNames.justNow || '刚刚';
  var minutesAgo = timeNames.minutesAgo || '分钟前';
  var hoursAgo = timeNames.hoursAgo || '小时前';
  var yesterday = timeNames.yesterday || '昨天';
  var future = timeNames.future || '';
  var now = Date.now();
  time = date.valueOf();
  if (now < time) return future;
  if (now - time <= 6e4) return justNow;

  if (now - time < 36e5) {
    return Math.floor((now - time) / 6e4) + minutesAgo;
  }

  if (now - time < 864e5) {
    return Math.floor((now - time) / 36e5) + hoursAgo;
  }

  if (getDayStartTime(new Date()) - time < 864e5) {
    return yesterday;
  }

  var y = date.getFullYear();
  var m = "0".concat(date.getMonth() + 1).substr(-2);
  var d = "0".concat(date.getDate()).substr(-2);

  if (y === new Date().getFullYear()) {
    return "".concat(m, "-").concat(d);
  }

  return "".concat(y, "-").concat(m, "-").concat(d);
}

export default formatTime;

isUdf.js

复制
/**
 * 判断一个变量是否为 undefined
 * @param {*} value
 * @returns {boolean}
 */
function isUdf(value) {
  return value === undefined;
}

export default isUdf;

getDayStartTime.js

复制
import isUdf from './isUdf';
import formatToDate from './formatToDate';

/**
 * 获取一天的开始时间,00 点 00 分 00 秒时间戳
 * @param {Date|number|string} time
 * @returns {number}
 *
 * getDayStartTime('2021-10-01'); // => 1633017600000
 */
function getDayStartTime(time) {
  if (isUdf(time)) time = new Date();
  var date = formatToDate(time);
  if (!date) return NaN;
  return date.setHours(0, 0, 0, 0);
}

export default getDayStartTime;

formatToDate.js

复制
/**
 * 格式化时间参数为日期对象
 * @param {Date|number|string} time
 * @returns {Date}
 *
 * formatToDate(new Date); // => Date
 * formatToDate('2021-10'); // => Date
 * formatToDate(1636222480480); // => Date
 */
function formatToDate(time) {
  var date = time instanceof Date ? time : new Date(time);
  var dateTime = date.getTime();
  if (!dateTime && dateTime !== 0) return null;
  return date;
}

export default formatToDate;

PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南

暴风雨前的宁静

想象一下,黑色星期五或者某个大促活动。你用 PHP 搭建的电商平台正在迎接前所未有的流量,订单源源不断,用户热情高涨,PHP 应用拼尽全力在扛。然后——啪——网站突然崩了。

你查日志,到底怎么了?流量确实飙了,但这次不是因为用户太多,而是一次 DDoS(分布式拒绝服务)攻击。

DDoS 攻击就像一场人造洪水,用大量伪造的请求把你的服务器淹掉。但具体到一个 PHP 应用,被打的时候到底发生了什么?怎么判断自己是不是正在被攻击?更重要的是——怎么防?

这篇文章会带你搞清楚 PHP 应用遭遇 DDoS 时的全过程:从识别攻击到保护你的应用不被打趴。

什么是 DDoS 攻击

DDoS 攻击有点像互联网上的交通堵塞。想象你要进一家热门店铺,结果突然冒出成百上千个"假顾客"堵在门口,真正的顾客根本挤不进去。店铺(你的 PHP 应用)被挤爆了,最终只能关门。

用技术语言说,DDoS 攻击是攻击者(或僵尸网络)向目标网站发送海量流量,耗尽其资源。目的很简单:让网站变慢或者直接打瘫。

对 PHP 应用来说,攻击会冲击以下几个环节:

  • Web 服务器:PHP 需要处理每一个请求,短时间内涌入大量请求会耗尽服务器资源。
  • 数据库:过多的查询会拖慢甚至打崩数据库。
  • 带宽:流量太大会吃满网络带宽,导致整体性能下降。

DDoS 攻击如何影响你的 PHP 应用

PHP 应用被 DDoS 打中时,背后发生了这些事情:

Web 服务器负载飙升

用户发起请求后,Web 服务器(比如 Apache 或 Nginx)会运行 PHP 脚本、查数据库、返回动态内容。正常情况下这没什么问题,但当成千上万(甚至上百万)的请求同时涌入,服务器很快就扛不住了。

  • CPU 打满:PHP 需要处理每个请求,大量请求会让 CPU 使用率直接拉满。
  • 内存吃紧:PHP 应用通常会在内存中保存会话数据或缓存,请求太多会导致内存耗尽,轻则变慢,重则崩溃。

数据库过载

PHP 应用通常依赖数据库来获取和展示动态内容。一个典型的请求可能涉及查库存、处理登录、渲染页面等操作。DDoS 攻击时,每个请求都可能触发开销很大的数据库查询,结果就是:

  • 数据库瓶颈:数据库扛不住这种量级的负载,查询开始变慢、超时甚至直接失败。
  • 响应迟钝:数据库服务器变得无响应,内容分发被严重延迟。

带宽打满

每个 DDoS 请求都会消耗带宽。当恶意流量大到一定程度,会把你的网络带宽全部吃掉,真实用户的请求根本进不来。

  • 连接数上限:网络连接被打满后,正常用户访问你的网站要么极慢,要么完全打不开。

PHP 脚本超时

PHP 脚本的执行时间是有上限的。服务器被大量请求淹没时,PHP 脚本可能来不及在规定时间内跑完,结果就是:

  • 500 错误:服务器因资源耗尽无法处理请求。
  • 连接超时:PHP 脚本执行时间过长,连接直接断掉。

如何判断你的 PHP 应用正在被 DDoS

及时识别 DDoS 攻击至关重要。以下是一些关键的技术指标:

流量突然飙升

流量在短时间内暴涨——尤其来源异常(比如来自不常见的地区或 IP 段)——就要警惕了。可以查看服务器日志来排查异常流量模式。

用 Apache 或 Nginx 日志检查是否有大量请求来自同一个 IP 或一批可疑地址:

# Apache:检查访问日志中的 IP 请求频次
cat /var/log/apache2/access.log | awk '{print $1}' | sort | uniq -c | sort -n

性能下降和超时

如果网站突然变慢或者频繁出现超时错误,可能就是 DDoS 在搞鬼。PHP 脚本处理不过来涌入的请求,开始报 500 错误或者超时。

资源占用异常

如果服务器的 CPU 和内存使用率突然飙高,说明 PHP 正在苦苦支撑。可以用 htoptop 实时监控资源使用情况:

# 实时监控 CPU 和内存使用情况
top -d 1

如果 CPU 或内存长时间处于高位,就该进一步排查了。

PHP 应用的 DDoS 防护策略

完全杜绝 DDoS 攻击很难,但有不少手段可以大幅降低其影响。下面是一些保护 PHP 应用的实用方案。

限流:第一道防线

限流就是限制每个用户在一段时间内能发起的请求数量。方法简单但很有效,能挡住大部分机器人和恶意请求。

用 Redis 实现限流

可以用 Redis 追踪每个用户的请求次数,超过阈值就拒绝:

$redis = new Redis();
$redis->connect('localhost', 6379);
$ip = $_SERVER['REMOTE_ADDR'];
$key = "request_count:{$ip}";
$limit = 100;  // Max requests per minute
$window = 60;  // 1 minute time window
$request_count = $redis->get($key);
if ($request_count && $request_count >= $limit) {
    // Too many requests, reject the user
    header('HTTP/1.1 429 Too Many Requests');
    exit('Rate limit exceeded');
}
$redis->incr($key);
$redis->expire($key, $window);  // Reset the count after 1 minute

这个基础限流方案可以有效节流那些试图用大量请求淹没你服务器的用户或机器人。

CDN:分流恶意流量

CDN(内容分发网络)会缓存静态资源(图片、CSS、JavaScript),通过分布在全球的边缘节点提供服务。DDoS 攻击时,CDN 可以吸收大量流量,让你的 PHP 服务器专心处理动态请求(比如用户登录、订单处理)。

通过 CDN 分发静态资源

<!-- 通过 CDN 提供静态资源 -->
<link rel="stylesheet" href="https://cdn.yoursite.com/styles.css">
<script src="https://cdn.yoursite.com/app.js"></script>
<img src="https://cdn.yoursite.com/images/product.jpg" alt="Product">

把静态资源交给 CDN,既能减轻 PHP 应用的负载,也能让 DDoS 流量更难直接打到你的应用核心。

WAF:应用层防护

WAF(Web 应用防火墙)是一种高级工具,专门检查和过滤发往 PHP 应用的 HTTP 流量。WAF 可以根据预设规则检测并拦截恶意请求,比如封禁可疑 IP 或屏蔽特定地区的流量。

以 AWS WAF 为例

  1. 创建 Web ACL(访问控制列表),定义流量过滤规则。
  2. 添加规则来拦截 HTTP 洪水攻击、SQL 注入、IP 信誉过滤等。
aws wafv2 create-web-acl --name "MyWAF" --scope "REGIONAL" --default-action "ALLOW" --rules ...

配置完成后,PHP 应用就有了一层专门的防护,恶意流量会被拦截,正常用户不受影响。

借助第三方 DDoS 防护服务

Cloudflare、AWS Shield 这类服务是专业做 DDoS 防护的。它们提供的高级防护能自动过滤恶意流量,保证你的 PHP 应用持续在线。

接入方式很简单:

  1. 注册 Cloudflare 或 AWS Shield。
  2. 把域名的流量路由到它们的服务。
  3. 它们会自动检测并拦截 DDoS 流量。

通过第三方服务,绝大部分攻击流量在到达你的 PHP 应用之前就已经被挡掉了。

实时监控和日志记录

持续监控流量和服务器性能有助于实时发现 DDoS 攻击。Datadog、New Relic、AWS CloudWatch 这类工具可以帮你捕捉异常流量、性能下降等问题。

记录可疑 IP

// Example: Log suspicious IPs for later analysis
$suspicious_ip = $_SERVER['REMOTE_ADDR'];
$log_file = '/path/to/your/log/file.log';
file_put_contents($log_file, "Suspicious IP: {$suspicious_ip}\n", FILE_APPEND);
// Optionally, block IP if it exceeds request limit
if ($request_count > $limit) {
    // Block the IP
    $blocked_ips[] = $suspicious_ip;
}

通过记录可疑活动,你可以事后封禁恶意用户,也能不断优化自己的防护策略。

总结

DDoS 攻击听起来可怕,但只要用对工具和策略,你完全可以保护好自己的 PHP 应用。从限流、CDN,到 WAF 和第三方防护服务,可选的方案并不少。

别慌——主动防御比被动应对强得多。今天就把这些防线搭起来,等攻击真来的时候你才不会手忙脚乱。持续监控、实时告警、遵循最佳实践,即使面对 DDoS,你的 PHP 应用照样能稳稳地跑着。
PHP 应用遭遇 DDoS 攻击时会发生什么:从入门到进阶的防护指南

各位 V 友好,

之前分享过我的富文本编辑器项目,今天正式完全开源了!

项目名:Tiptap UI Kit
协议:MIT - 商业项目也可以免费使用

为什么开源

原本是想做商业化,但后来想通了:

  • 好工具应该让更多人用上
  • 社区的力量能让项目更强大
  • 开源能获得更大的影响力

所以决定完全开源,回馈社区。

核心特性

🎨 5 种精美主题

  • Default - 简洁清爽
  • Word - 专业文档风格,A4 纸张布局
  • Notion - 现代化笔记体验
  • GitHub - 开发者友好的 Markdown 风格
  • Typora - 优雅的阅读写作

每个主题都有完善的暗黑模式支持。

🤖 AI 智能辅助

  • 续写 - AI 帮你继续写作
  • 润色 - 自动优化文字表达
  • 翻译 - 支持 14+ 语言
  • 总结 - 提取要点
  • 自定义命令 - 自己定义 AI 功能

支持多种 AI 提供商:

  • OpenAI (GPT-4, GPT-3.5)
  • 阿里云通义千问
  • DeepSeek
  • Ollama (本地部署)

📝 Word 模式

  • A4 纸张布局
  • 自动分页
  • 适合正式文档编辑

🤝 实时协作

  • 基于 Yjs 的多人协作
  • 支持在线编辑
  • 显示协作者光标

其他特性

  • 🌍 国际化(简体中文、繁体中文、英文)
  • 📱 响应式设计
  • ⌨️ 丰富的快捷键
  • 🔍 搜索替换
  • 🎨 格式刷
  • 📊 表格支持
  • 🖼️ 图片上传调整
  • 🔗 链接编辑
  • 💯 版本历史

技术栈

  • Vue 3.5 + Composition API
  • Tiptap 3 (基于 ProseMirror)
  • TypeScript
  • Ant Design Vue
  • Vite

Cisco NX-OS Software Release 10.6(2)F - 数据中心网络操作系统

NX-OS 网络操作系统

请访问原文链接:https://sysin.org/blog/cisco-nx-os-10/ 查看最新版。原创作品,转载请保留出处。

作者主页:sysin.org


Cisco NX-OS
Cisco NX-OS 操作系统助力网络紧跟业务发展步伐。

Nexus 9300 Family Marquee

功能和优势

NX-OS 网络操作系统为现代数据中心提供支持。

架构灵活性

架构灵活性

NX-OS 助力数据中心随工作负载和应用的发展而不断扩展。

  • 借助 VXLAN EVPN 为可扩展工作负载提供支持
  • 通过多租户有效利用网络资源 (sysin)
  • 自由选用支持分段路由和 VXLAN 的 Overlay 网络技术
  • 借助 VXLAN 多站点实现灾难恢复和业务连续性

操作简便性

操作简便性

NX-OS 具备可编程性,可将调配时间从几天缩短到几分钟,从而简化部署。

  • 与 Cisco DCNM 完全集成,实现全面管理
  • 通过行业标准 API 实现轻松配置
  • 与 DevOps 自动化工具完全集成
  • 支持原生 Docker 的简化工具 (sysin)

端到端可视性

端到端可视性

数据和控制平面具有深度可视性,有助于保护数据中心,并针对问题快速进行补救。

  • 借助精细流和 ASIC 遥测,优化网络 (sysin)
  • 利用控制平面可视性和模型驱动的遥测,预防网络故障
  • 通过 MACSEC 加密,防御监听和网络攻击
  • 借助 802.1X 验证,保护终端和工作负载
  • 查看产品手册

借助 Cisco NX-OS 管理您的数据中心网络

遥测技术助力网络运维

建立可靠的数据中心网络,防止停机并快速解决网络问题。

分段路由

使用 SRv6 架构构建可扩展的网络,从而降低网络运维的复杂性,并实现与核心/WAN 的数据中心互联 (DCI)

自由定制、安全可靠

灵活、安全地管理工作负载。NX-OS 支持您根据业务需求定制网络,让 DevOps 在创新时只需几分钟即可打开交换矩阵。

轻松扩展

为媒体构建具有成本效益且易于扩展的生产应用和网络。

NX-OS 配图

下载地址

for Cisco Nexus 9000 Series Switches:

for Cisco Nexus 9000v Switch:


数据中心网络相关产品:

更多:Cisco 产品下载链接汇总

Sophos Firewall (SFOS) v22 GA re-release - 下一代防火墙

Sophos Firewall | Next-gen firewall

请访问原文链接:https://sysin.org/blog/sfos-22/ 查看最新版。原创作品,转载请保留出处。

作者主页:sysin.org


Sophos Firewall

2026 年 1 月 20 日

Sophos Firewall v22 GA re-release (Build 411) is Now Available

Sophos Firewall v22 GA 重新发布(Build 411)现已提供

随着 SFOS v22 GA(Build 411)的重新发布,我们修复了一些罕见且孤立的问题(详见下文)。如果你已经在运行 v22 GA,你将在防火墙中看到该重新发布版本作为产品内固件升级提供。建议你在方便的时候升级到 v22 GA 重新发布版本(Build 411)。

🐞 v22 GA 重新发布(Build 411)中已解决的问题

  • 当包含超过 256 个主机的主机组被分配到规则或策略时,防火墙无法将这些主机与流量进行匹配(NC-171031)。
  • 无法通过配置了 VLAN 过滤的桥接接口访问 Web 管理控制台(NC-171003)。
  • CLI 中出现如下日志:“Invalid rule id or family for update”(无效的规则 ID 或更新族)(NC-170987)。
  • 当 DNAT 规则中包含指定的出站接口时,DNAT 流量失败(NC-170970)。
  • 当终端设备从不健康状态恢复为健康状态后 (sysin),Ping 无法继续(NC-170527)。
  • 在规则重新评估期间,防火墙规则标志未被重置(NC-166150)。
  • 当默认 SNAT 规则配置为静态 IP 地址而非 MASQ 时,基于策略的 IPsec VPN 流量失败(NC-170917)。
  • 当 Sophos Central Reporting 插件中发生 mmap 损坏时,Garner 进程会重启(NC-169540)。
  • 当防火墙为同步应用控制(Synchronized Application Control)执行内部清理任务时,会生成一条“Security global configuration”的日志事件(NC-171885)。
  • 由于存在控制字符,导致无法正确获取桥接成员接口,进而引发区域误判,策略测试器因此无法找到对应规则(NC-171933)。
  • SSL/TLS 小组件和会话图表数据无法正确显示(NC-171600)。
  • 无法添加任何 SNMP 配置(NC-172197)。

2025 年 12 月 9 日

Sophos Firewall v22 现已发布

产品团队很高兴宣布 Sophos Firewall v22 已正式发布。此次更新带来了多项 “Secure by Design” 安全设计增强,以及许多用户最期待的功能。

Secure By Design(安全设计)

在过去几周里,Sophos 已经介绍了 Secure by Design 原则的重要性,以及为何需要安全产品与安全设计同样重要。Sophos Firewall v22 在前几版的安全和加固增强基础上,将 Secure by Design 提升到了全新水平。

新功能概览

Sophos 防火墙健康检查

强大的安全态势依赖于防火墙的最佳配置。Sophos 防火墙 v22 通过新增的健康检查功能,使评估和优化防火墙配置更加容易。该功能评估防火墙的数十项配置设置,并将其与 CIS 基准及其他最佳实践进行对比,即时提供潜在风险的洞察 (sysin)。它会标识所有高风险设置,并提供快速定位和处理的建议。

健康检查状态将在新的控制中心小部件中显示,完整报告可在“Firewall health check”主菜单中查看。

Sophos Firewall v22

Sophos Firewall v22

其他 Secure By Design 增强功能

  • 下一代 Xstream 架构:

    • 引入全新控制平面架构,最大化安全性和可扩展性。
    • 新控制平面实现服务模块化、隔离和容器化,例如 IPS 可像“应用”一样运行在防火墙平台上。
    • 完全分离权限以增强安全性 (sysin)。
    • SFOS 现具备自愈能力,可持续监控系统状态并自动修复偏差。

Sophos Firewall v22

  • 加固内核:

    • Sophos 防火墙 OS 的下一代 Xstream 架构基于新加固内核(v6.6+),提供增强的安全性、性能和可扩展性,充分利用当前及未来硬件。
    • 新内核提供更严格的进程隔离,更好地防范侧信道攻击及 CPU 漏洞(Spectre、Meltdown、L1TF、MDS、Retbleed、ZenBleed、Downfall)。
    • 提供加固的 usercopy、堆栈保护 (stack canaries) 和内核地址空间布局随机化 (KASLR)。
  • 远程完整性监控:

    • v22 集成 Sophos XDR Linux 传感器 (sysin),实现系统完整性实时监控,包括未授权配置、规则导出、恶意程序执行尝试、文件篡改等。
    • 帮助安全团队更快速识别、调查和响应攻击。
    • 提供其他防火墙厂商未提供的额外安全能力。
  • 全新反恶意软件引擎:

    • 集成最新 Sophos 反恶意软件引擎,通过全球声誉查询实现对新威胁的零日实时检测。
    • 利用 SophosLabs 海量云数据库,文件每 5 分钟或更短更新一次。
    • 引入 AI/ML 模型检测,并提供增强的遥测数据以加速新威胁分析。

其他安全性与可扩展性增强

  • 主动威胁响应日志改进:为入站和出站流量增加细粒度日志控制,减少暴力破解等重复事件噪声;支持将入站转发流量与第三方威胁源、NDR Essentials 和 MDR 威胁源匹配,提高外部威胁检测。
  • XML API 访问控制增强:API 配置移动到“Administration”主菜单,可按 IP、IP 范围和网络对象定义访问,支持最多 64 个对象(之前为 10 个 IP)。
  • NDR Essentials 改进

    • 日志威胁评分 - 主动威胁响应日志中包含分配的威胁评分,便于可视化、报告和分析。
    • 数据中心选择 - 可选择 NDR Essentials 流分析的数据中心区域,满足地域或数据驻留需求,默认选择最低延迟区域。
  • 即时网页类别与搜索关键词告警

    • 可根据浏览意图或行为触发即时告警。
    • 帮助学校从被动报告转向主动防护,保护学生安全,并符合最新数字标准。
  • 设备访问支持 TLS1.3:Web 管理控制台、VPN 门户和用户门户现支持 TLS 1.3,加强加密。

简化管理与使用体验增强

  • 导航性能优化:可直接访问任意菜单或标签,无需等待当前页面加载完成,加快 UI 导航速度。
  • TLS1.3 支持:Web 管理控制台 (sysin)、VPN 门户和用户门户支持 TLS 1.3。
  • 通过 SNMP 监控硬件:新增热门功能,支持下载 MIB 文件;监控指标包括 CPU 温度、NPU 温度、风扇转速、电源状态(XGS 2100 及以上)、以及所有支持 PoE 的 XGS 型号的 PoE 测量(XGS 116(w) 除外)。
  • NTP 服务器设置:新安装默认设置为 “Use pre-defined NTP server”
  • XFRM 接口 UI 改进:增加分页支持,可搜索和筛选,大量 XFRM 接口管理更便捷。
  • sFlow 监控:基于设置的采样率提供实时数据,支持任何物理接口及子接口(Alias、VLAN 等),最多 5 个采集器。
  • 蜂窝 WAN:新增 CLI 命令 system cellular_wan show 检查信号强度。

SG UTM9 功能

随着 Sophos UTM 即将于 2026 年 7 月 30 日退役,一些迁移客户可受益于以下新增功能:

  • WAF 多因素认证支持:为集成 Web 应用防火墙提供 MFA,增强安全性和功能一致性。
  • WAF 安全性增强:会话由 SFOS 管理,而非客户端 cookie,更难被劫持;当无需认证转发时,可完全卸载到 SFOS,减少内部 WAF 服务器暴露。
  • OTP 令牌 SHA 256/512 支持:提供给 Google 和 Sophos 应用及管理员用户的选项。
  • 审计日志:提供全面审计日志,记录前后变化以满足最新 NIS2 标准。第一阶段支持防火墙规则、对象和接口的详细审计日志,可从 Diagnostics > Troubleshooting Logs > configuration-audit.log 下载,XML 用于标注前后变化。

下载地址

Sophos Firewall OS (SFOS) 22.0 GA re-release (Build 411) (2026-01-20)

v22 GA re-release 取代了之前的 v22 GA,修复了若干罕见且孤立的已知问题。

  • 请访问:https://sysin.org/blog/sfos-22/

    • SFOS v22 Hardware Devices for XGS Series
    • SFOS v22 Software Appliances for bare metal machines
    • SFOS v22 Virtual Appliances for VMware/KVM/Xen/Hyper-v

更多:Firewall 产品链接汇总

近年来发现 AI 行业的从业者普遍喜欢在朋友圈分享行业观点,包括:现在在 AI 行业应该关注什么,不应该关注什么,将来的 AI 产品可以做哪方面,未来 AI 会如何引导人类社会的变革,等等。而别的行业几乎难以在朋友圈看到。

开源 AI 编程工具 OpenCode 正式亮相,其具备原生终端界面(Terminal UI)、多会话支持,并广泛兼容包括 Claude、OpenAI、Gemini 及各类本地模型在内的 75 种以上模型。除了命令行(CLI)工具外,OpenCode 还提供桌面应用版本,并支持作为 VS Code、Cursor 等主流 IDE 的插件使用。

OpenCode 允许开发者沿用现有的付费服务订阅,如 ChatGPT Plus/Pro 和 GitHub Copilot。此外,它还内置了一系列免费模型,用户可以通过 LM Studio 在本地直接运行。

在功能集成方面,OpenCode 与包括 Rust、Swift、Terraform、TypeScript 和 PyRight 在内的多种语言服务器协议(LSP)服务器实现了深度整合。通过利用 LSP 服务器输出的反馈信息,大语言模型能够更高效地与代码库进行交互。

该智能体同时支持远程和本地的 MCP 服务器。不过,开发团队提醒道,使用 MCP 服务器会增加上下文占用,部分服务器(特别是 GitHub MCP)往往会消耗大量的 Tokens。

OpenCode 能够适配任何支持 Agent Client Protocol (ACP) 的编辑器,该协议旨在标准化编程编辑器/IDE 与 AI 智能体之间的通信。目前的兼容编辑器列表已涵盖 JetBrains 系列 IDE、Zed、Neovim 和 Emacs,针对 Eclipse 等其他编辑器的适配工作也正在进行中。

OpenCode 背后的公司 Anomaly Innovations 强调,该工具采用了“隐私优先”的架构设计,这意味着 OpenCode 不会存储任何代码或上下文数据。用户对会话共享拥有完全控制权,可以选择手动共享、自动共享或完全禁用共享。协作完成后,已共享的对话可以取消共享;对于敏感项目,团队还可以在配置层面统一禁用共享功能。

据创始人介绍,OpenCode 最适合那些追求控制力、可审计性、希望避免供应商锁定(vendor-locking)的高级用户和团队,以及对隐私敏感的工作环境。同时他们也指出,对于寻求纯粹“无代码”体验的初学者来说,这可能不是最佳解决方案。

Reddit 用户 Specialist_Garden_98 对 OpenCode 支持多种 LLM 的优势赞赏有加,他总结道:

这套工作流简直无敌。你可以灵活配置,平时构思方案用廉价模型‘跑龙套’,关键执行时刻再‘一键开大’换成昂贵模型,效率和成本拉满了。

此外,该用户还强调了其“撤销修改”功能的实用性,如果执行结果不理想,可以快速回滚。另一方面,用户 copenhagen_bram 则提出了批评,认为该工具在执行命令前似乎不会询问权限,这可能带来一定的安全风险。

目前,OpenCode 已在 GitHub 上开源,目前已斩获超过 9.5 万颗星(Stars),并拥有数百位代码贡献者。

原文链接:

https://www.infoq.com/news/2026/02/opencode-coding-agent/

概述

AQS ( Abstract Queued Synchronizer )是一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )来表示同步状态 和一个先进先出( FIFO )的线程等待队列来完成资源获取的排队工作,通过CAS完成对State值的修改。

AQS整体框架如下:

当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层

原理

AQS 为每个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程放入线程等待队列,等待下一次资源调度,流程图如下所示:

Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。

底层结构

state:状态

Abstract Queued Synchronizer 维护了 volatile int 类型的变量,用于表示当前的同步状态。volatile虽然不能保证操作的原子性,但是能保证当前变量state的可见性。

state的访问方式有三种: getState()、setState()和 compareAndSetState(),均是原子操作,其中,compareAndSetState的实现依赖于 Unsafe的compareAndSwaplnt()

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

CLH队列

Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。

AQS的独占式和共享式

  • 独占式:只有一个线程能执行,具体的 Java 实现有 ReentrantLock。
  • 共享式:多个线程可同时执行,具体的 Java 实现有 Semaphore和CountDownLatch。

AQS只是一个框架 ,只定义了一个接口,具体资源的获取、释放都由自定义同步器去实现。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等, AQS 已经在顶层实现好(就是模板方法模式),不需要具体的同步器再做处理。自定义同步器实现时主要实现以下几种方法:

  • 以ReentrantLock为例,ReentrantLock中的state初始值为0表示无锁状态。在线程执行 tryAcquire()获取该锁后ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过tryAcquire() 获取锁时均会失败,直到该线程释放锁后state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1, 因此ReentrantLock也属于可重入锁。 但获取多少次锁就要释放多少次锁,这样才能保证state最终为0。如果获取锁的次数多于释放锁的次数,则会出现该线程一直持有该锁的情况;如果获取锁的次数少于释放锁的次数,则运行中的程序会报锁异常。
  • 以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后面的动作。
  • 以Semaphore为例,state则代表可以同时访问的线程数量,也可能理解为访问的许可证(permit)数量。每个线程访问(acquire)时需要拿到对应的许可证,否则进行阻塞,访问结束则返还(release)许可证。state只能在Semaphore的构造方法中进行初始化,后续不能进行修改。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

Node节点

Node即为上面CLH变体队列中的节点。

Node结点是每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态waitStatus

Node中几个方法和属性值的含义:

  • waitStatus:当前节点在队列中的状态
  • thread:表示处于该节点的线程
  • prev:前驱指针
  • predecessor:返回前驱节点,没有的话抛出npe
  • nextWaiter:指向下一个处于CONDITION状态的节点(由于本篇文章不讲述Condition Queue队列,这个指针不多介绍)
  • next:后继指针

等待状态waitStatus

waitStatus有下面几个枚举值:如是否被阻塞、是否等待唤醒、是否已经被取消等。共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度,不再想去获取资源了。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

源码

以ReentrantLock的非公平锁为例,将加锁和解锁的交互流程单独拎出来强调一下

加锁:

  1. 通过ReentrantLock的加锁方法Lock进行加锁操作。
  2. 会调用到内部类 Sync的Lock方法,由于Sync#lock是抽象方法,根据 ReentrantLock初始化选择的公平锁和非公平锁,执行相关内部类的Lock方法,本质上都会执行AQS的 Acquire 方法。
  3. AQS的 Acquire 方法会执行 tryAcquire 方法,但是由于tryAcquire需要自定义同步器实现,因此执行了ReentrantLock中的tryAcquire方法,由于ReentrantLock是通过公平锁和非公平锁内部类实现的tryAcquire方法,因此会根据锁类型不同,执行不同的tryAcquire。
  4. tryAcquire是获取锁逻辑,获取失败后,会执行框架AQS的后续逻辑,跟ReentrantLock自定义同步器无关。

解锁:

  1. 通过ReentrantLock的解锁方法Unlock进行解锁。
  2. Unlock会调用内部类Sync的Release方法,该方法继承于AQS。
  3. Release中会调用tryRelease方法,tryRelease需要自定义同步器实现,tryRelease只在ReentrantLock中的Sync实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。
  4. 释放成功后,所有处理由AQS框架完成,与自定义同步器无关。

acquire(int)

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。

public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
}

函数流程如下:

  1. tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
  2. addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

关于整个函数流程详解,可以往下看

tryAcquire(int)

此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,当然不仅仅只限于tryLock()。

protected boolean tryAcquire(int arg) {
     throw new UnsupportedOperationException();
}

这里是AQS的方法,所以直接throw异常,而没有具体的实现。原因就在于AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现。

这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。

ReentrantLock实现公平锁非公平锁则主要体现在tryAcquire的实现上:

公平锁中实现的tryAcquire:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
           if (!hasQueuedPredecessors() &&  //公平锁加锁时判断等待队列中是否存在有效节点的方法
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
           }
     }
     else if (current == getExclusiveOwnerThread()) {
           int nextc = c + acquires;
           if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
           setState(nextc);
           return true;
     }
     return false;
}

非公平锁中实现的tryAcquire:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();
     if (c == 0) {
           if (compareAndSetState(0, acquires)) {
               setExclusiveOwnerThread(current);
               return true;
           }
      }
      else if (current == getExclusiveOwnerThread()) {
           int nextc = c + acquires;
           if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
           setState(nextc);
           return true;
      }
      return false;
}
  • 公平锁中多了一层 !hasQueuedPredecessors() 的判断,这是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以获取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中。
  • 而在非公平锁中,没有这个判断,直接尝试获取锁,能获取到锁则不用加入等待队列。
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

这里的判断 h != t && ((s = h.next) == null || s.thread != Thread.currentThread());为什么要判断的头结点的下一个节点?第一个节点储存的数据是什么?

双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。当h != t时: 如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True。 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。

addWaiter(Node)

此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。

private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    Node node = new Node(Thread.currentThread(), mode);

    //尝试快速方式直接放到队尾。
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }

    //上一步失败则通过enq入队。
    enq(node);
    return node;
}

主要的流程如下:

  1. 通过当前的线程和锁模式新建一个节点。
  2. Pred指针指向尾节点Tail。
  3. 将New中Node的Prev指针指向Pred。
  4. 通过compareAndSetTail方法,完成尾节点的设置。这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值。
// java.util.concurrent.locks.AbstractQueuedSynchronizer

static {
    try {
        stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        waitStatusOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));
        nextOffset = unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));
    } catch (Exception ex) { 
    throw new Error(ex); 
  }
}

从AQS的静态代码块可以看出,都是获取一个对象的属性相对于该对象在内存当中的偏移量,这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset指的是tail对应的偏移量,所以这个时候会将new出来的Node置为当前队列的尾节点。同时,由于是双向链表,也需要将前一个节点指向尾节点。

如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改),就需要enq入队

private Node enq(final Node node) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。

acquireQueued(Node, int)

通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。addWaiter()返回的是一个包含该线程的Node。而这个Node会作为参数,进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行“获锁”操作。那么下一步就是:如果获取不到锁,那么就进入阻塞状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。

acquireQueued:在等待队列中排队拿号(中间没其它事干可以阻塞休息),直到拿到号后再返回。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过

        //CAS“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源,也就是当前节点在真实数据队列的首部,就尝试获取锁(别忘了头结点是虚节点)。
            if (p == head && tryAcquire(arg)) {
                setHead(node);// 获取锁成功,头指针移动到当前node
                p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
                failed = false; // 成功获取资源
                return interrupted;//返回等待过程中是否被中断过
            }

            // 说明p为头节点且当前没有获取到锁(可能是非公平锁被抢占了)或者 是p不为头结点,这个时候就要判断当前node是否要被阻塞(被阻塞条件:前驱节点的waitStatus为-1),防止无限循环浪费资源。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) //说明发生了意料之外的异常,将节点移除,避免影响到其他节点
            cancelAcquire(node);
    }
}

setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据。

// java.util.concurrent.locks.AbstractQueuedSynchronizer

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

acquireQueued函数的具体流程:

从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,shouldParkAfterFailedAcquire代码:

// java.util.concurrent.locks.AbstractQueuedSynchronizer

// 靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取头结点的节点状态
        int ws = pred.waitStatus;
        // 说明头结点处于唤醒状态
        if (ws == Node.SIGNAL)
            return true; 
        // 通过枚举值我们知道waitStatus>0是取消状态
        if (ws > 0) {
            do {
                // 循环向前查找取消节点,把取消节点从队列中剔除
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 设置前任节点等待状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}

parkAndCheckInterrupt主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。

// java.util.concurrent.locks.AbstractQueuedSynchronizer

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}

具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程):

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。

那么shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1?

是在什么时间释放节点通知到被挂起的线程呢?

CANCELLED状态节点生成

回看acquireQueued方法中的Finally代码:

// java.util.concurrent.locks.AbstractQueuedSynchronizer

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        ...
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    ...
                    failed = false;
                    ...
                }
                ...
        } finally {
            if (failed)
                cancelAcquire(node);
            }
}

显然,当failed为true时才会执行方法cancelAcquire,那什么情况下failed为true呢?try代码段执行过程中出现异常。

这里不知道哪里会出现异常?假设tryAcquire出现的异常,那么acquire方法就已经不会往后执行,也就不会执行到acquireQueued

通过cancelAcquire方法,将Node的状态标记为CANCELLED。

// java.util.concurrent.locks.AbstractQueuedSynchronizer

private void cancelAcquire(Node node) {
  // 将无效节点过滤
    if (node == null)
        return;
  // 设置该节点不关联任何线程,也就是虚节点
    node.thread = null;
    Node pred = node.prev;
  // 通过前驱节点,跳过取消状态的node
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
  // 获取过滤后的前驱节点的后继节点
    Node predNext = pred.next;
  // 把当前node的状态设置为CANCELLED
    node.waitStatus = Node.CANCELLED;
  // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
  // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
    // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
    // 如果1和2中有一个为true,再判断当前节点的线程是否为null
    // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
        if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
      // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

cancelAcquire方法的流程:

  1. 获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。
  2. 根据当前节点的位置,考虑以下三种情况:

    1. 当前节点是尾节点。
    2. 当前节点是Head的后继节点。
    3. 当前节点不是Head的后继节点,也不是尾节点。

当前节点是尾节点:

当前节点是Head的后继节点:

当前节点不是Head的后继节点,也不是尾节点:

通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?

执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。

shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。

do {
    node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);

release(int)

此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到头结点
        // 头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒等待队列里的下一个线程
        return true;
    }
    return false;
}

根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease()

这里的判断条件为什么是h != null && h.waitStatus != 0?

  • h null Head还没初始化。初始情况下,head null,第一个节点入队,Head会被初始化一个虚拟节点。所以说,这里如果还没来得及入队,就会出现head == null 的情况。
  • h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
  • h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。

tryRelease(int)

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

// java.util.concurrent.locks.ReentrantLock.Sync#tryRelease

@ReservedStackAccess
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;//在未重入的情况下,getState() = 1,减去releases 1,因此c 为 0
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);//独占锁线程设置为null
    }
    setState(c);//恢复默认
    return free;
}

unparkSuccessor(Node)

此方法用于唤醒等待队列中下一个线程。

private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程s。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立了),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!

在队列中查找时是从后向前找的,为什么这么做?

从源码上看,先找到后继结点s,如果s状态正常那么直接唤醒。但有两种异常情况,会导致next链不一致:

  1. s==null,在新结点入队时可能会出现

  1. s.waitStatus > 0,中间有节点取消时会出现(如超时)

关于并发问题,addWaiter()入队操作和cancelAcquire()取消排队操作都会造成next链的不一致,而prev链是强一致的,所以这时从后往前找是最安全的。

为什么prev链是强一致的?

因为addWaiter()里每次compareAndSetTail(pred, node)之前都有node.prev = pred,即使compareAndSetTail失败,enq()会反复尝试,直到成功。一旦compareAndSetTail成功,该node.prev就成功挂在之前的tail结点上了,而且是唯一的,这时其他新结点的prev只能尝试往新tail结点上挂。这里的组合用法非常巧妙,能保证CAS之前的prev链强一致,但不能保证CAS后的next链强一致。

acquireShared(int)

此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

public final void acquireShared(int arg) {
     if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:

  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

doAcquireShared(int)

此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);//加入队列尾部
    boolean failed = true;//是否成功标志
    try {
        boolean interrupted = false;//等待过程中是否被中断过的标志
        for (;;) {
            final Node p = node.predecessor();//前驱
            if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
                int r = tryAcquireShared(arg);//尝试获取资源
                if (r >= 0) {//成功
                    setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程
                    p.next = null; // help GC
                    if (interrupted)//如果等待过程中被打断过,此时将中断补上。
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }

            //判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这里跟acquireQueued()的流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外,但实际上都一样。

跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。

那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。

setHeadAndPropagate(Node, int):此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!

private void setHeadAndPropagate(Node node, int propagate) {

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;
    setHead(node);//head指向自己
     //如果还有剩余量,继续唤醒下一个邻居线程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

releaseShared()

此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//尝试释放资源
        doReleaseShared();//唤醒后继结点
        return true;
    }
    return false;
}

此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值

doReleaseShared()

此方法主要用于唤醒后继

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))
                    continue;
                unparkSuccessor(h);//唤醒后继
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)// head发生变化
            break;
    }
}

应用

Mutex是一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。核心源码:

class Mutex implements Lock, java.io.Serializable {
    // 自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判断是否锁定状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 尝试获取资源,立即返回。成功则返回true,否则false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 这里限定只能为1个量
            if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
                return true;
            }
            return false;
        }

        // 尝试释放资源,立即返回。成功则为true,否则false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定为1个量
            if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//释放资源,放弃占有状态
            return true;
        }
    }

    // 真正同步类的实现都依赖继承于AQS的自定义同步器!
    private final Sync sync = new Sync();

    //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }

    //tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //unlock<-->release。两者语文一样:释放资源。
    public void unlock() {
        sync.release(1);
    }

    //锁是否占有状态
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

除了Mutex,ReentrantLock/CountDownLatch/Semphore这些同步类的实现方式都差不多,不同的地方就在获取-释放资源的方式tryAcquire-tryRelelase。

ReentrantLock 的使用

ReentrantLock 的使用方式与 synchronized 关键字类似,都是通过加锁和释放锁来实现同步的。我们来看看 ReentrantLock 的使用方式,以非公平锁为例:

public class ReentrantLockTest {
    private static final ReentrantLock lock = new ReentrantLock();
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                lock.lock();
                try {
                    count++;
                } finally {
                    lock.unlock();
                }
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}

代码很简单,两个线程分别对 count 变量进行 10000 次累加操作,最后输出 count 的值。我们来看看运行结果:

20000

可以看到,两个线程对 count 变量进行了 20000 次累加操作,说明 ReentrantLock 是支持重入性的。再来看看公平锁的使用方式,只需要将 ReentrantLock 的构造方法改为公平锁即可:

private static final ReentrantLock lock = new ReentrantLock(true);

运行结果为:

20000

可以看到,公平锁的运行结果与非公平锁的运行结果一致,这是因为公平锁的实现方式与非公平锁的实现方式基本一致,只是在获取锁时增加了判断当前节点是否有前驱节点的逻辑判断。

  • 公平锁: 按照线程请求锁的顺序获取锁,即先到先得。
  • 非公平锁: 线程获取锁的顺序可能与请求锁的顺序不同,可能导致某些线程获取锁的速度较快。

需要注意的是,使用 ReentrantLock 时,锁必须在 try 代码块开始之前获取,并且加锁之前不能有异常抛出,否则在 finally 块中就无法释放锁(ReentrantLock 的锁必须在 finally 中手动释放)。

错误示例:

Lock lock = new XxxLock();
// ...
try {
    // 如果在此抛出异常,会直接执行 finally 块的代码
    doSomething();
    // 不管锁是否成功,finally 块都会执行
    lock.lock();
    doOthers();

} finally {
    lock.unlock();
}

正确示例:

Lock lock = new XxxLock();
// ...
lock.lock();
try {
    doSomething();
    doOthers();
} finally {
    lock.unlock();
}

摘要:在Android复杂的组件通信机制中,漏洞往往隐藏在看似合法的业务逻辑之下。本文将深度解析一种名为NIV(Next-Intent Vulnerability)的跨域漏洞。该漏洞不仅挑战了Android默认的访问控制屏障,更由于其广泛存在于头部应用、开源项目和第三方SDK中,具有极高的实战研究价值。本文将从底层机制、漏洞建模、大规模统计及全方位防御方案四个维度阐述这一安全隐患。


0x01 引言:Android生态中的“信任”危机

随着Android占据移动市场的大半江山,应用的复杂程度呈指数级增长。Android系统的基石之一是其基于组件的开发模式,而Intent(意图)则是链接这些孤岛的纽带。

然而,正是这种灵活性带来了致命的攻击面。开发者往往认为,只要将核心组件声明为exported="false"(私有组件),就能高枕无忧。但NIV漏洞的出现,彻底打破了这一“安全错觉”:它利用不安全的Intent重定向逻辑,借道公共组件,“合法”地启动私有组件。

android_components_1768914732865.png


0x02 基础夯实:Android通信边界与Intent构造

2.1 四大组件的“围墙”

Android组件(Activity, Service, Broadcast Receiver, Content Provider)的访问权限通过AndroidManifest.xml定义。

  • 公共组件 (Public)exported="true"。接收来自任何应用的请求,是攻击者最直接的接触点。
  • 私有组件 (Private)exported="false"。原则上仅由应用自身调用。

2.2 Intent:携带“毒药”的信使

Intent是一个消息传递对象,除了基本的Action、Data外,最值得安全研究员关注的是ExtrasExtras是一个Bundle对象,可以存储几乎任何数据,包括另一个Intent。

intent_structure_1768914919077.png


0x03 漏洞建模:NIV(Next-Intent Vulnerability)原理

3.1 核心逻辑:借刀杀人

NIV漏洞本质上是由应用内部的“代理转发”机制引起的。

典型业务场景:

  1. 某公共组件(如LoginActivity)接收外部Intent。
  2. 该Intent的Extras中藏有一个预定的“下一步行动”(即Next-Intent)。
  3. 当业务完成(如登录成功或路由判断结束)后,组件取出这个嵌套的Next-Intent并执行。

攻击建模: 攻击者向公共组件发送一个精心构造的Intent,将其中的Next-Intent指向目标应用的某个私有组件。当代理组件执行startActivity(nextIntent)时,系统校验的是代理组件的权限——由于代理组件属于应用自身,它拥有启动自家所有私有组件的最高权限。

niv_attack_flow_1768914844292.png

3.2 漏洞代码特征(Smali层)

在逆向工程中,我们可以通过定位以下调用链路来寻找NIV漏洞点:

  1. Source点:调用getIntent()
  2. 传递链:调用getParcelableExtra()并伴随check-cast Landroid/content/Intent;
  3. Sink点:最终调用startActivity(), startService()sendBroadcast()


0x04 技术背景:Android组件间通信的演进与权衡

4.1 IPC机制的必要性与沙箱边界

Android系统基于Linux内核,每个应用都运行在独立的Dalvik/ART虚拟机和独立的系统进程中。这种隔离机制构成了Android安全体系的沙箱边界。为了实现跨进程的功能复用(如应用调用系统相机、第三方支付),系统必须提供一套既高效又安全的通信协议,这便是Binder机制以及在其之上封装的Intent系统。

4.2 显式与隐式的安全性博弈

在Android开发中,启动组件有两种主要方式:

  • 显式Intent (Explicitly):直接指定目标的包名和类名。从安全角度看,这是最稳固的,因为调用目标是预定义的,攻击者无法通过伪造Intent来劫持流向。
  • 隐式Intent (Implicitly):仅声明Action、Category等描述信息。系统根据匹配规则寻找接收者。隐式Intent虽然增强了系统的解耦性,但由于其目标不确定,极易引发Intent嗅探或非法拦截。

4.3 为什么重定向逻辑成了“刚需”?

NIV漏洞之所以广泛存在,不是因为开发者故意制造后门,而是由于现代复杂APP的架构需求:

  1. 统一路由分发(Deep Link Router):当用户从短信、浏览器或第三方应用通过URL点击进入APP时,应用通常需要一个中转的RouterActivity。该组件负责解析URL,并根据参数决定后续跳转的具体业务页面。
  2. 状态拦截器模式(Interceptors):最典型的场景是登录拦截。当用户在未登录状态下访问受限功能(如“我的收藏”)时,系统会强制跳转到登录界面,并动态保存用户的最初意图(即原始Intent)。在登录成功后,再取出并执行这个Intent,以实现“无缝跳转”的体验。

这种“解析参数 -> 动态构造目标 -> 二次分发”的模式,本质上是将原本由Android系统内核接管的组件启动流程,下放到了应用层的业务代码中。如果此时缺乏对“二次分发”目标的严格校验,系统级的权限边界就会在应用层被轻易击穿。

4.4 从Intent Spoofing到NIV的演进

历史上,Android安全研究多聚焦于Intent Spoofing(伪造广播或启动)或Fragment Injection(注入非法片段)。NIV(Next-Intent Vulnerability)则更具隐蔽性和威力。它利用了Intent对象作为Parcelable可以被嵌套的特性,使得攻击者不仅能突破访问控制(Activity劫持),还能通过构造恶意的嵌套Extra数据,对受侵害的私有组件进行深层次的“参数污染”。

vuln_statistics_1768915164648.png




0x05 防御体系:如何构建稳固的通信堡垒?

针对NIV漏洞,防御不能只靠某一个层面,而应构建多维防御体系:

security_defense_1768915276554.png

5.1 组件暴露面的最小化(Access Control)

  • 除非确需对外开放,否则所有组件默认应设置android:exported="false"
  • 对于必须开放的入口,尽量使用自定义权限进行过滤。

5.2 嵌套Intent的白名单校验(Whitelist Filtering)

这是防御NIV的核心。在执行Next-Intent之前,必须进行严格校验:

Intent nextIntent \= getIntent().getParcelableExtra("next_intent");
if (nextIntent != null) {
ComponentName cn \= nextIntent.getComponent();
// 1. 校验包名是否为自身应用
if (cn != null && cn.getPackageName().equals(getPackageName())) {
// 2. 校验类名是否在允许的跳转名单内
if (allowedInterals.contains(cn.getClassName())) {
startActivity(nextIntent);
}
}
}

5.3 来源判定(Source Verification)

利用getCallingPackage()来判断请求的发送方。如果发现发起跳转的不是本应用自身,则应拒绝处理嵌套的Intent。


0x06 结语

NIV漏洞是典型的“功能与安全”冲突的产物。在追求流畅的用户体验(如自动跳转)时,开发者往往牺牲了对Intent边界的把控。通过白名单校验、收敛组件权限以及利用PendingIntent等更安全的对象,我们完全可以规避此类风险。


声明:本文旨在技术交流,相关技术严禁用于非法渗透。

Store Standard Error to a Variable in Bash

在 Bash 中,您可以使用 2>&1 操作符和 $() 命令替换语法将命令的标准错误输出存储到一个变量中。这里 2>&1 将错误消息重定向到 &1 (标准输出)。默认情况下,shell 作为标准输出设备。

例如,要将 ls 命令的标准错误输出存储到名为 errors 的变量中,可以使用以下命令:

errors=$(ls non-existent-file 2>&1)

Store Standard Error in a Bash Variable

或者,您可以使用 $? 特殊参数,将命令的退出状态存储到一个变量中。退出状态是一个数字值,指示命令是否成功。值“0”表示成功,而非“0”表示错误。

例如,要将 ls 命令的退出状态存储到一个名为 status 的变量中,可以使用以下命令:

ls non-existent-file 
status=$?

然后可以使用 $status 变量检查 ls 命令的退出状态,并根据结果采取适当的操作。例如:

ls non-existent-file
status=$?

if [ $status -ne 0 ]; then
echo "Last command failed with an error."
fi

请记住,$() 命令替换语法允许您执行命令并替换其输出。 2> 操作符将命令的标准错误输出重定向到 &1 标准输出流,这允许您捕获命令的标准输出和标准错误输出到变量中。

我的开源项目

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

"年终奖不是公司有义务要给你们的,而是我决定的" -老板

"星期天不赶货,但是不能让车间的员工休息,要让他们加班,不然会养成懒惰的习惯" -老板

"你们举手表决的这个议程,无论结果怎么样还是我说的算" -老板

年终奖这事情基本是年年都有发,今年初也压根没说你们管理层做的不好会减一半或者全扣掉。也是在临近放年假的时候说年终奖全扣掉,最后变成扣一半。

全扣掉的起因:老板在会上问到有没有人年后请假,我说这边有 1 个。
老板问到:上周开会不是把请假的人报到人事部吗?
我:组员请假我正常批假,上次也问了组员有没有事情,他不请假。星期六有突发事情要请假,我正常批假有什么问题吗?组员请不请假是我能管控的吗?老板再会上瞬间情绪失控来了一句:管理层都这样不作为,你们年终奖全扣掉,今年不发了。(我的想法是本来对整体部门不满意,刚好我变成了出气筒有理由全扣掉了)

"星期天不赶货,但是不能让车间的员工休息,要让他们加班,不然会养成懒惰的习惯" 这是让我感觉到最恶心的,车间的工人哪个月不是天天加班,少这一天不少,多这一天不多,恶心的嘴脸。
"不管你们在怎么急,星期天我都要让员工休息"这是我听采购给供应商让他们加班供货对方的回答。在对比一下我们这里的老板。(星期天白嫖不给加班费,天天下班开会,pua)

"你们举手表决的这个议程,无论结果怎么样还是我说的算"
本来就是你说的算的规则为什么还需要我们来表决呢?

大家好,分享一下最近做的项目。

新人感受:新手做网站就是做的快,做的多。没有太多历史包袱,想到就做,做完就上。

这次做的是 Seedance 2 AI 视频生成器: https://seedance2ai.one

简单说就是一个在线 AI 视频工具,输入文字描述或者上传图片,30-90 秒就能生成视频。

作为新手,踩了不少坑,但也验证了一件事:别想太多,先上线再说。
功能不完美没关系,有人用了、有反馈了,再迭代就行。