阿里的大模型感觉除了在开源猛猛发力,商业应用营收大吗
我平常去理发,听理发师跟街坊聊天都会提到豆包和用豆包教小孩子。
感觉字节在应用上比千问要走的更靠前啊。
xiaohack博客专注前沿科技动态与实用技术干货分享,涵盖 AI 代理、大模型应用、编程工具、文档解析、SEO 实战、自动化部署等内容,提供开源项目教程、科技资讯日报、工具使用指南,助力开发者、AI 爱好者获取前沿技术与实战经验。
在当今网络环境中,SSL证书已成为网站安全标配,它不仅保护用户数据传输安全,还能提升搜索引擎排名和用户信任度。对于大多数网站运营者来说,一年期SSL证书是性价比最高且管理相对便捷的选择。本文将为您详细解析一年期SSL证书的申请流程。 在申请前,您需要了解几种常见的SSL证书类型: 一年期证书通常是DV或OV类型,EV证书多为两年期。 生成CSR的方法: 1、注册账号并登录 首先,您需要在JoySSL的官方网站上注册一个账号。访问JoySSL官网,点击右上角的“注册”按钮,进入注册页面。在注册过程中填写相关信息,最后一栏务必填写注册码230970,这是获取免费一年期证书的关键步骤。 2、选择并申请证书 登录成功后,您将看到JoySSL的用户界面。找到免费一年期单域名SSL证书,0元下单支付。 3、申请证书并验证域名所有权 提交申请后,您将进入域名验证环节。选择域名DNS解析或者服务器文件验证,根据系统提示完成相应验证步骤。然后点击“验证”按钮。如果一切正常,系统将提示您验证成功。 4、下载与安装证书 一般10分钟左右签发,您可以在用户后台的“我的证书”或找到新签发的证书,并点击下载,然后把证书文件部署到相应服务器上。 5、验证证书安装效果 证书安装完成后,通过浏览器访问您的网站,查看地址栏是否出现绿色的安全锁图标以及HTTPS访问。第一步:理解SSL证书类型
第二步:申请前的准备工作

第三步:申请SSL证书的具体步骤
免费SSL证书的获取入口
大家好,我是 在 AI 大模型应用爆发的当下,对接多平台接口、管理 API 密钥、控制调用权限......这些繁琐的操作是否让你头疼? 今天,给大家介绍一款开源的大模型网关神器,一站式解决大模型接口管理的所有痛点! 功能特色: 1、拉取镜像 2、创建挂载目录 3、启动容器 使用 SQLite 数据库 使用 MySQL 数据库(推荐) 4、容器运行成功后,浏览器访问 1、创建一个目录用于部署 2、在该目录下创建 简化配置(测试环境) 3、启动服务 4、容器运行成功后,浏览器访问 如果你正在被多平台 AI 接口管理困扰,想要一套轻量化、高拓展的大模型网关系统, 推荐的开源项目已经收录到 或者访问网站,进行在线浏览: 我创建了一个开源项目交流群,方便大家在群里交流、讨论开源项目。 但是任何人在群里打任何广告,都会被 T 掉。 如果你对这个交流群感兴趣或者在使用开源项目中遇到问题,可以通过如下方式进群: 关注微信公众号:【Java陈序员】,回复【开源项目交流群】进群,或者通过公众号下方的菜单添加个人微信,并备注【开源项目交流群】,通过后拉你进群。Java陈序员。关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。
项目介绍
new-api —— 一款开源的大模型网关与 AI 资产管系统,定位为一站式 AI 资产管理网关,核心目标是提供大模型相关的网关管理与资产统筹能力。快速上手
new-api 支持 Docker 部署,可使用 Docker 快速部署。Docker 部署
docker pull calciumion/new-apimkdir -p /data/software/new-apidocker run -d --name new-api \
--restart always \
-p 3000:3000 \
-e TZ=Asia/Shanghai \
-v /data/software/new-api:/data \
calciumion/new-apidocker 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环境变量中的数据库连接信息需要传入对应的值。
http://{IP/域名}:3000首次安装需要按照页面指引手动设置管理员账号和密码,完成初始化后即可使用所设置的管理员账号登录系统。
Docker Compose 部署、
mkdir new-api
cd new-apidocker-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:/datadocker compose up -dhttp://{IP/域名}:3000首次安装需要按照页面指引手动设置管理员账号和密码,完成初始化后即可使用所设置的管理员账号登录系统。
功能体验










new-api 值得一试!无论是个人学习、团队协作还是小型企业使用,都能满足你的需求。快去部署体验吧~项目地址:https://github.com/QuantumNous/new-api最后
GitHub 项目,欢迎 Star:https://github.com/chenyl8848/great-open-source-projecthttps://chencoding.top:8090/#/
大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!
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 搭建的电商平台正在迎接前所未有的流量,订单源源不断,用户热情高涨,PHP 应用拼尽全力在扛。然后——啪——网站突然崩了。 你查日志,到底怎么了?流量确实飙了,但这次不是因为用户太多,而是一次 DDoS(分布式拒绝服务)攻击。 DDoS 攻击就像一场人造洪水,用大量伪造的请求把你的服务器淹掉。但具体到一个 PHP 应用,被打的时候到底发生了什么?怎么判断自己是不是正在被攻击?更重要的是——怎么防? 这篇文章会带你搞清楚 PHP 应用遭遇 DDoS 时的全过程:从识别攻击到保护你的应用不被打趴。 DDoS 攻击有点像互联网上的交通堵塞。想象你要进一家热门店铺,结果突然冒出成百上千个"假顾客"堵在门口,真正的顾客根本挤不进去。店铺(你的 PHP 应用)被挤爆了,最终只能关门。 用技术语言说,DDoS 攻击是攻击者(或僵尸网络)向目标网站发送海量流量,耗尽其资源。目的很简单:让网站变慢或者直接打瘫。 对 PHP 应用来说,攻击会冲击以下几个环节: PHP 应用被 DDoS 打中时,背后发生了这些事情: 用户发起请求后,Web 服务器(比如 Apache 或 Nginx)会运行 PHP 脚本、查数据库、返回动态内容。正常情况下这没什么问题,但当成千上万(甚至上百万)的请求同时涌入,服务器很快就扛不住了。 PHP 应用通常依赖数据库来获取和展示动态内容。一个典型的请求可能涉及查库存、处理登录、渲染页面等操作。DDoS 攻击时,每个请求都可能触发开销很大的数据库查询,结果就是: 每个 DDoS 请求都会消耗带宽。当恶意流量大到一定程度,会把你的网络带宽全部吃掉,真实用户的请求根本进不来。 PHP 脚本的执行时间是有上限的。服务器被大量请求淹没时,PHP 脚本可能来不及在规定时间内跑完,结果就是: 及时识别 DDoS 攻击至关重要。以下是一些关键的技术指标: 流量在短时间内暴涨——尤其来源异常(比如来自不常见的地区或 IP 段)——就要警惕了。可以查看服务器日志来排查异常流量模式。 用 Apache 或 Nginx 日志检查是否有大量请求来自同一个 IP 或一批可疑地址: 如果网站突然变慢或者频繁出现超时错误,可能就是 DDoS 在搞鬼。PHP 脚本处理不过来涌入的请求,开始报 500 错误或者超时。 如果服务器的 CPU 和内存使用率突然飙高,说明 PHP 正在苦苦支撑。可以用 如果 CPU 或内存长时间处于高位,就该进一步排查了。 完全杜绝 DDoS 攻击很难,但有不少手段可以大幅降低其影响。下面是一些保护 PHP 应用的实用方案。 限流就是限制每个用户在一段时间内能发起的请求数量。方法简单但很有效,能挡住大部分机器人和恶意请求。 用 Redis 实现限流 可以用 Redis 追踪每个用户的请求次数,超过阈值就拒绝: 这个基础限流方案可以有效节流那些试图用大量请求淹没你服务器的用户或机器人。 CDN(内容分发网络)会缓存静态资源(图片、CSS、JavaScript),通过分布在全球的边缘节点提供服务。DDoS 攻击时,CDN 可以吸收大量流量,让你的 PHP 服务器专心处理动态请求(比如用户登录、订单处理)。 通过 CDN 分发静态资源 把静态资源交给 CDN,既能减轻 PHP 应用的负载,也能让 DDoS 流量更难直接打到你的应用核心。 WAF(Web 应用防火墙)是一种高级工具,专门检查和过滤发往 PHP 应用的 HTTP 流量。WAF 可以根据预设规则检测并拦截恶意请求,比如封禁可疑 IP 或屏蔽特定地区的流量。 以 AWS WAF 为例 配置完成后,PHP 应用就有了一层专门的防护,恶意流量会被拦截,正常用户不受影响。 Cloudflare、AWS Shield 这类服务是专业做 DDoS 防护的。它们提供的高级防护能自动过滤恶意流量,保证你的 PHP 应用持续在线。 接入方式很简单: 通过第三方服务,绝大部分攻击流量在到达你的 PHP 应用之前就已经被挡掉了。 持续监控流量和服务器性能有助于实时发现 DDoS 攻击。Datadog、New Relic、AWS CloudWatch 这类工具可以帮你捕捉异常流量、性能下降等问题。 记录可疑 IP 通过记录可疑活动,你可以事后封禁恶意用户,也能不断优化自己的防护策略。 DDoS 攻击听起来可怕,但只要用对工具和策略,你完全可以保护好自己的 PHP 应用。从限流、CDN,到 WAF 和第三方防护服务,可选的方案并不少。 别慌——主动防御比被动应对强得多。今天就把这些防线搭起来,等攻击真来的时候你才不会手忙脚乱。持续监控、实时告警、遵循最佳实践,即使面对 DDoS,你的 PHP 应用照样能稳稳地跑着。PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
暴风雨前的宁静
什么是 DDoS 攻击
DDoS 攻击如何影响你的 PHP 应用
Web 服务器负载飙升
数据库过载
带宽打满
PHP 脚本超时
如何判断你的 PHP 应用正在被 DDoS
流量突然飙升
# Apache:检查访问日志中的 IP 请求频次
cat /var/log/apache2/access.log | awk '{print $1}' | sort | uniq -c | sort -n性能下降和超时
资源占用异常
htop 或 top 实时监控资源使用情况:# 实时监控 CPU 和内存使用情况
top -d 1PHP 应用的 DDoS 防护策略
限流:第一道防线
$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 minuteCDN:分流恶意流量
<!-- 通过 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">WAF:应用层防护
aws wafv2 create-web-acl --name "MyWAF" --scope "REGIONAL" --default-action "ALLOW" --rules ...借助第三方 DDoS 防护服务
实时监控和日志记录
// 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;
}总结
PHP 应用遭遇 DDoS 攻击时会发生什么:从入门到进阶的防护指南
各位 V 友好,
之前分享过我的富文本编辑器项目,今天正式完全开源了!
项目名:Tiptap UI Kit
协议:MIT - 商业项目也可以免费使用
原本是想做商业化,但后来想通了:
所以决定完全开源,回馈社区。
每个主题都有完善的暗黑模式支持。
支持多种 AI 提供商:
Cisco NX-OS Software Release 10.6(2)F - 数据中心网络操作系统 NX-OS 网络操作系统 请访问原文链接:https://sysin.org/blog/cisco-nx-os-10/ 查看最新版。原创作品,转载请保留出处。 作者主页:sysin.org Cisco NX-OS NX-OS 网络操作系统为现代数据中心提供支持。 NX-OS 助力数据中心随工作负载和应用的发展而不断扩展。 NX-OS 具备可编程性,可将调配时间从几天缩短到几分钟,从而简化部署。 数据和控制平面具有深度可视性,有助于保护数据中心,并针对问题快速进行补救。 建立可靠的数据中心网络,防止停机并快速解决网络问题。 使用 SRv6 架构构建可扩展的网络,从而降低网络运维的复杂性,并实现与核心/WAN 的数据中心互联 (DCI) 灵活、安全地管理工作负载。NX-OS 支持您根据业务需求定制网络,让 DevOps 在创新时只需几分钟即可打开交换矩阵。 为媒体构建具有成本效益且易于扩展的生产应用和网络。 for Cisco Nexus 9000 Series Switches: for Cisco Nexus 9000v Switch: 数据中心网络相关产品:
Cisco NX-OS 操作系统助力网络紧跟业务发展步伐。
功能和优势
架构灵活性

操作简便性

端到端可视性

借助 Cisco NX-OS 管理您的数据中心网络
遥测技术助力网络运维
分段路由
自由定制、安全可靠
轻松扩展

下载地址
seedance 各类组词都没了,连带横杠的,比如 seedance-2.ai 类的都抢光了。大家都在等着 24 号发布。
我还有最后两天,工作是不可能工作的,摸下鱼,争取在大 A 回个本
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 重新发布(Build 411)现已提供! 随着 SFOS v22 GA(Build 411)的重新发布,我们修复了一些罕见且孤立的问题(详见下文)。如果你已经在运行 v22 GA,你将在防火墙中看到该重新发布版本作为产品内固件升级提供。建议你在方便的时候升级到 v22 GA 重新发布版本(Build 411)。 🐞 v22 GA 重新发布(Build 411)中已解决的问题: 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 防火墙 v22 通过新增的健康检查功能,使评估和优化防火墙配置更加容易。该功能评估防火墙的数十项配置设置,并将其与 CIS 基准及其他最佳实践进行对比,即时提供潜在风险的洞察 (sysin)。它会标识所有高风险设置,并提供快速定位和处理的建议。 健康检查状态将在新的控制中心小部件中显示,完整报告可在“Firewall health check”主菜单中查看。 下一代 Xstream 架构: 加固内核: 远程完整性监控: 全新反恶意软件引擎: NDR Essentials 改进: 即时网页类别与搜索关键词告警: 随着 Sophos UTM 即将于 2026 年 7 月 30 日退役,一些迁移客户可受益于以下新增功能: 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/
新功能概览
Sophos 防火墙健康检查


其他 Secure By Design 增强功能

其他安全性与可扩展性增强
简化管理与使用体验增强
system cellular_wan show 检查信号强度。SG UTM9 功能
下载地址
开源 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),并拥有数百位代码贡献者。 原文链接:
可以拼车的那种
这是不打算修复了吗
AQS ( Abstract Queued Synchronizer )是一个抽象的队列同步器,通过维护一个共享资源状态( Volatile Int State )来表示同步状态 和一个先进先出( FIFO )的线程等待队列来完成资源获取的排队工作,通过CAS完成对State值的修改。 AQS整体框架如下: 当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层 AQS 为每个共享资源都设置一个共享资源锁,线程在需要访问共享资源时首先需要获取共享资源锁,如果获取到了共享资源锁,便可以在当前线程中使用该共享资源,如果获取不到,则将该线程放入线程等待队列,等待下一次资源调度,流程图如下所示: Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。 Abstract Queued Synchronizer 维护了 volatile int 类型的变量,用于表示当前的同步状态。volatile虽然不能保证操作的原子性,但是能保证当前变量state的可见性。 state的访问方式有三种: getState()、setState()和 compareAndSetState(),均是原子操作,其中,compareAndSetState的实现依赖于 Unsafe的compareAndSwaplnt() Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。 AQS使用一个Volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。 AQS只是一个框架 ,只定义了一个接口,具体资源的获取、释放都由自定义同步器去实现。不同的自定义同步器争用共享资源的方式也不同,自定义同步器在实现时只需实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护,如获取资源失败入队、唤醒出队等, AQS 已经在顶层实现好(就是模板方法模式),不需要具体的同步器再做处理。自定义同步器实现时主要实现以下几种方法: 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。 Node即为上面CLH变体队列中的节点。 Node结点是每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态waitStatus Node中几个方法和属性值的含义: waitStatus有下面几个枚举值:如是否被阻塞、是否等待唤醒、是否已经被取消等。共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。 注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。 以ReentrantLock的非公平锁为例,将加锁和解锁的交互流程单独拎出来强调一下 加锁: 解锁: 此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。 函数流程如下: 关于整个函数流程详解,可以往下看 此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。这也正是tryLock()的语义,当然不仅仅只限于tryLock()。 这里是AQS的方法,所以直接throw异常,而没有具体的实现。原因就在于AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现。 这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。 ReentrantLock实现公平锁非公平锁则主要体现在tryAcquire的实现上: 公平锁中实现的tryAcquire: 非公平锁中实现的tryAcquire: 这里的判断 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(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进等待队列。 此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。 主要的流程如下: 从AQS的静态代码块可以看出,都是获取一个对象的属性相对于该对象在内存当中的偏移量,这样我们就可以根据这个偏移量在对象内存当中找到这个属性。tailOffset指的是tail对应的偏移量,所以这个时候会将new出来的Node置为当前队列的尾节点。同时,由于是双向链表,也需要将前一个节点指向尾节点。 如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改),就需要enq入队 如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。 通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了。addWaiter()返回的是一个包含该线程的Node。而这个Node会作为参数,进入到acquireQueued方法中。acquireQueued方法可以对排队中的线程进行“获锁”操作。那么下一步就是:如果获取不到锁,那么就进入阻塞状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源,然后就可以去干自己想干的事了。 acquireQueued:在等待队列中排队拿号(中间没其它事干可以阻塞休息),直到拿到号后再返回。 setHead方法是把当前节点置为虚节点,但并没有修改waitStatus,因为它是一直需要用的数据。 acquireQueued函数的具体流程: 从上图可以看出,跳出当前循环的条件是当“前置节点是头结点,且当前线程获取锁成功”。为了防止因死循环导致CPU资源被浪费,我们会判断前置节点的状态来决定是否要将当前线程挂起,shouldParkAfterFailedAcquire代码: parkAndCheckInterrupt主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。 具体挂起流程用流程图表示如下(shouldParkAfterFailedAcquire流程): 整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。 park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。 那么shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1? 是在什么时间释放节点通知到被挂起的线程呢? 回看acquireQueued方法中的Finally代码: 显然,当failed为true时才会执行方法cancelAcquire,那什么情况下failed为true呢?try代码段执行过程中出现异常。 通过cancelAcquire方法,将Node的状态标记为CANCELLED。 cancelAcquire方法的流程: 根据当前节点的位置,考虑以下三种情况: 当前节点是尾节点: 当前节点是Head的后继节点: 当前节点不是Head的后继节点,也不是尾节点: 通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作? 执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过Try代码块中的shouldParkAfterFailedAcquire方法了),如果此时修改Prev指针,有可能会导致Prev指向另一个已经移除队列的Node,因此这块变化Prev指针不安全。 shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理Prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更Prev指针比较安全。 此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。 根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自定义同步器在设计tryRelease() 这里的判断条件为什么是h != null && h.waitStatus != 0? 跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。 此方法用于唤醒等待队列中下一个线程。 这个函数并不复杂。一句话概括:用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链不一致: 关于并发问题,addWaiter()入队操作和cancelAcquire()取消排队操作都会造成next链的不一致,而prev链是强一致的,所以这时从后往前找是最安全的。 为什么prev链是强一致的? 因为addWaiter()里每次compareAndSetTail(pred, node)之前都有node.prev = pred,即使compareAndSetTail失败,enq()会反复尝试,直到成功。一旦compareAndSetTail成功,该node.prev就成功挂在之前的tail结点上了,而且是唯一的,这时其他新结点的prev只能尝试往新tail结点上挂。这里的组合用法非常巧妙,能保证CAS之前的prev链强一致,但不能保证CAS后的next链强一致。 此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。 这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是: 此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。 这里跟acquireQueued()的流程并没有太大区别。只不过这里将补中断的selfInterrupt()放到doAcquireShared()里了,而独占模式是放到acquireQueued()之外,但实际上都一样。 跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。 那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。 setHeadAndPropagate(Node, int):此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式! private void setHeadAndPropagate(Node node, int propagate) { 此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。 此方法的流程也比较简单,一句话:释放掉资源后,唤醒后继。跟独占模式下的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()的返回值 此方法主要用于唤醒后继 Mutex是一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。核心源码: 除了Mutex,ReentrantLock/CountDownLatch/Semphore这些同步类的实现方式都差不多,不同的地方就在获取-释放资源的方式tryAcquire-tryRelelase。 ReentrantLock 的使用方式与 synchronized 关键字类似,都是通过加锁和释放锁来实现同步的。我们来看看 ReentrantLock 的使用方式,以非公平锁为例: 代码很简单,两个线程分别对 count 变量进行 10000 次累加操作,最后输出 count 的值。我们来看看运行结果: 可以看到,两个线程对 count 变量进行了 20000 次累加操作,说明 ReentrantLock 是支持重入性的。再来看看公平锁的使用方式,只需要将 ReentrantLock 的构造方法改为公平锁即可: 运行结果为: 可以看到,公平锁的运行结果与非公平锁的运行结果一致,这是因为公平锁的实现方式与非公平锁的实现方式基本一致,只是在获取锁时增加了判断当前节点是否有前驱节点的逻辑判断。 需要注意的是,使用 ReentrantLock 时,锁必须在 try 代码块开始之前获取,并且加锁之前不能有异常抛出,否则在 finally 块中就无法释放锁(ReentrantLock 的锁必须在 finally 中手动释放)。 错误示例: 正确示例:概述


原理

底层结构
state:状态
// 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队列

AQS的独占式和共享式

Node节点

等待状态waitStatus
源码

acquire(int)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}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;
}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;
}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());
}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;
}// 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);
}
}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;
}
}
}
}
acquireQueued(Node, int)
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);
}
}// java.util.concurrent.locks.AbstractQueuedSynchronizer
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
// 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;
}// java.util.concurrent.locks.AbstractQueuedSynchronizer
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
CANCELLED状态节点生成
// 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);
}
}这里不知道哪里会出现异常?假设tryAcquire出现的异常,那么acquire方法就已经不会往后执行,也就不会执行到acquireQueued
// 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
}
}


do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);release(int)
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(int)
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}// 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);//唤醒
}

acquireShared(int)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}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);
}
}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;
}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;
}
}应用
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();
}
}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);
}
}20000private static final ReentrantLock lock = new ReentrantLock(true);20000Lock 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();
}
大家用 AI,是用官网,还是第三方客户端?
还有就是,有什么好用的第三方 AI 客户端推荐的么
摘要:在Android复杂的组件通信机制中,漏洞往往隐藏在看似合法的业务逻辑之下。本文将深度解析一种名为NIV(Next-Intent Vulnerability)的跨域漏洞。该漏洞不仅挑战了Android默认的访问控制屏障,更由于其广泛存在于头部应用、开源项目和第三方SDK中,具有极高的实战研究价值。本文将从底层机制、漏洞建模、大规模统计及全方位防御方案四个维度阐述这一安全隐患。
随着Android占据移动市场的大半江山,应用的复杂程度呈指数级增长。Android系统的基石之一是其基于组件的开发模式,而Intent(意图)则是链接这些孤岛的纽带。
然而,正是这种灵活性带来了致命的攻击面。开发者往往认为,只要将核心组件声明为exported="false"(私有组件),就能高枕无忧。但NIV漏洞的出现,彻底打破了这一“安全错觉”:它利用不安全的Intent重定向逻辑,借道公共组件,“合法”地启动私有组件。

Android组件(Activity, Service, Broadcast Receiver, Content Provider)的访问权限通过AndroidManifest.xml定义。
exported="true"。接收来自任何应用的请求,是攻击者最直接的接触点。exported="false"。原则上仅由应用自身调用。Intent是一个消息传递对象,除了基本的Action、Data外,最值得安全研究员关注的是Extras。 Extras是一个Bundle对象,可以存储几乎任何数据,包括另一个Intent。

NIV漏洞本质上是由应用内部的“代理转发”机制引起的。
典型业务场景:
LoginActivity)接收外部Intent。Next-Intent)。Next-Intent并执行。攻击建模: 攻击者向公共组件发送一个精心构造的Intent,将其中的Next-Intent指向目标应用的某个私有组件。当代理组件执行startActivity(nextIntent)时,系统校验的是代理组件的权限——由于代理组件属于应用自身,它拥有启动自家所有私有组件的最高权限。

在逆向工程中,我们可以通过定位以下调用链路来寻找NIV漏洞点:
getIntent()。getParcelableExtra()并伴随check-cast Landroid/content/Intent;。startActivity(), startService()或sendBroadcast()。Android系统基于Linux内核,每个应用都运行在独立的Dalvik/ART虚拟机和独立的系统进程中。这种隔离机制构成了Android安全体系的沙箱边界。为了实现跨进程的功能复用(如应用调用系统相机、第三方支付),系统必须提供一套既高效又安全的通信协议,这便是Binder机制以及在其之上封装的Intent系统。
在Android开发中,启动组件有两种主要方式:
NIV漏洞之所以广泛存在,不是因为开发者故意制造后门,而是由于现代复杂APP的架构需求:
RouterActivity。该组件负责解析URL,并根据参数决定后续跳转的具体业务页面。这种“解析参数 -> 动态构造目标 -> 二次分发”的模式,本质上是将原本由Android系统内核接管的组件启动流程,下放到了应用层的业务代码中。如果此时缺乏对“二次分发”目标的严格校验,系统级的权限边界就会在应用层被轻易击穿。
历史上,Android安全研究多聚焦于Intent Spoofing(伪造广播或启动)或Fragment Injection(注入非法片段)。NIV(Next-Intent Vulnerability)则更具隐蔽性和威力。它利用了Intent对象作为Parcelable可以被嵌套的特性,使得攻击者不仅能突破访问控制(Activity劫持),还能通过构造恶意的嵌套Extra数据,对受侵害的私有组件进行深层次的“参数污染”。

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

android:exported="false"。这是防御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);
}
}
}
利用getCallingPackage()来判断请求的发送方。如果发现发起跳转的不是本应用自身,则应拒绝处理嵌套的Intent。
NIV漏洞是典型的“功能与安全”冲突的产物。在追求流畅的用户体验(如自动跳转)时,开发者往往牺牲了对Intent边界的把控。通过白名单校验、收敛组件权限以及利用PendingIntent等更安全的对象,我们完全可以规避此类风险。
声明:本文旨在技术交流,相关技术严禁用于非法渗透。
在 Bash 中,您可以使用 例如,要将 或者,您可以使用 例如,要将 然后可以使用 请记住,
2>&1 操作符和 $() 命令替换语法将命令的标准错误输出存储到一个变量中。这里 2>&1 将错误消息重定向到 &1 (标准输出)。默认情况下,shell 作为标准输出设备。ls 命令的标准错误输出存储到名为 errors 的变量中,可以使用以下命令:errors=$(ls non-existent-file 2>&1)
$? 特殊参数,将命令的退出状态存储到一个变量中。退出状态是一个数字值,指示命令是否成功。值“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 标准输出流,这允许您捕获命令的标准输出和标准错误输出到变量中。我的开源项目
今年春节 deepseek 的 deepseek 时刻会不会再次上演?
大家好,分享一下最近做的项目。
新人感受:新手做网站就是做的快,做的多。没有太多历史包袱,想到就做,做完就上。
这次做的是 Seedance 2 AI 视频生成器: https://seedance2ai.one
简单说就是一个在线 AI 视频工具,输入文字描述或者上传图片,30-90 秒就能生成视频。
作为新手,踩了不少坑,但也验证了一件事:别想太多,先上线再说。
功能不完美没关系,有人用了、有反馈了,再迭代就行。