Java并发编程核心:线程安全、锁机制与内存可见性实战
1. 线程安全到底在解决什么问题很多刚接触Java服务端开发的朋友写单线程业务逻辑时顺风顺水一旦引入多线程或者部署到并发环境就开始遇到各种“玄学”问题明明代码逻辑没问题跑出来的结果却对不上某个变量在A线程改了B线程死活读不到新值加了个标志位想优雅停止线程结果循环死活不退出。这些现象背后基本都指向同一个核心议题——线程安全。线程安全说白了就是当多个线程同时访问同一份共享资源时程序还能按照我们预期的逻辑正确执行不会因为调度顺序的随机性而产生错误结果。这里的关键词是“共享”和“同时”。如果每个线程只碰自己的私有变量那永远不会有线程安全问题如果所有操作都是串行执行的也不会有问题。问题恰恰出在多个线程读写同一块内存区域且执行顺序不可控的场景。这篇文章我会围绕线程安全这条主线把锁机制、线程间通信、内存可见性、CAS无锁编程以及线程状态流转这几个核心知识点串起来讲。适合已经写过一些Java代码、但对并发编程还停留在“知道synchronized但说不清原理”阶段的开发者。我会尽量用实际场景和类比把原理讲透同时给出可以直接参考的代码示例和排查思路。读完之后你至少能做到看到一段并发代码能判断它有没有线程安全问题遇到并发bug知道从哪些方向去排查自己写并发代码时知道该选哪种同步手段。2. 锁机制从互斥到协作的完整拆解2.1 为什么需要锁一个计数器引发的血案先看一个最经典的例子。假设我们有一个计数器多个线程同时对它做自增操作public class Counter { private int count 0; public void increment() { count; } public int getCount() { return count; } }单线程跑一万次结果就是一万没问题。但开十个线程各跑一千次最终结果大概率小于一万。原因在于count这个操作看起来是一行代码实际上在底层至少分三步读取count的当前值到寄存器、将值加一、把新值写回内存。两个线程如果同时读到相同的旧值各自加一后写回就会丢失一次自增。这就是典型的竞态条件。解决方式就是加锁让这三步操作变成原子操作同一时刻只有一个线程能执行。2.2 synchronized的三种用法与底层原理Java内置的锁就是synchronized关键字它可以修饰三种东西修饰实例方法锁的是当前对象实例this修饰静态方法锁的是当前类的Class对象修饰代码块锁的是括号里指定的对象// 修饰实例方法 public synchronized void increment() { count; } // 修饰代码块锁指定对象 public void increment() { synchronized (this) { count; } }底层实现上synchronized在JVM层面依赖对象头中的Mark Word来记录锁状态。锁的状态会经历无锁、偏向锁、轻量级锁、重量级锁的升级过程。偏向锁适用于只有一个线程反复进入同步块的场景此时不需要真正的互斥操作只是在对象头里记一下线程ID。当出现第二个线程竞争时升级为轻量级锁通过CAS自旋尝试获取锁。如果自旋失败或者竞争激烈最终升级为重量级锁依赖操作系统的互斥量实现线程会进入阻塞状态。注意偏向锁在JDK 15之后默认被禁用了因为它在高并发场景下撤销偏向锁的开销反而成了负担。如果你用的是较新的JDK版本不用太纠结偏向锁的细节。2.3 ReentrantLock更灵活的显式锁synchronized用起来简单但灵活性有限。比如它不支持超时获取锁、不支持中断响应、不支持公平锁。这些场景下就需要ReentrantLock。private final ReentrantLock lock new ReentrantLock(); public void increment() { lock.lock(); try { count; } finally { lock.unlock(); } }ReentrantLock的核心优势体现在几个方面。第一它可以指定公平性构造函数传入true就变成公平锁线程按等待顺序获取锁避免饥饿。第二它提供tryLock(long timeout, TimeUnit unit)方法在指定时间内拿不到锁就返回false不会无限等待。第三它支持lockInterruptibly()等待锁的过程中可以响应中断。底层实现上ReentrantLock基于AQSAbstractQueuedSynchronizer框架内部维护一个volatile的state变量表示锁的持有计数以及一个CLH双向队列存放等待的线程。每次加锁就是对state做CAS操作成功则获取锁失败则入队等待。2.4 读写锁读多写少场景的优化利器互斥锁的问题在于多个读操作之间其实并不需要互斥它们不会修改数据。如果读操作远多于写操作用互斥锁就浪费了大量并发能力。ReentrantReadWriteLock解决了这个问题读锁之间共享写锁与其他所有锁互斥。private final ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); private final Lock readLock rwLock.readLock(); private final Lock writeLock rwLock.writeLock(); public int getCount() { readLock.lock(); try { return count; } finally { readLock.unlock(); } } public void increment() { writeLock.lock(); try { count; } finally { writeLock.unlock(); } }这里有个容易踩的坑读锁不能升级为写锁。也就是说一个线程持有读锁的同时去获取写锁会导致死锁。反过来写锁可以降级为读锁但一般也不推荐这么用因为降级后的读锁可能被其他等待写锁的线程阻塞。2.5 锁的选择与性能考量实际开发中怎么选锁我的经验是这样的场景推荐方案理由简单互斥代码块小synchronizedJVM内置优化好代码简洁不易出错需要超时/中断/公平锁ReentrantLock功能更灵活读多写少ReentrantReadWriteLock提升读并发度无锁并发简单计数AtomicInteger等原子类CAS无锁性能更好高并发复杂状态考虑分段锁或LongAdder减少竞争热点实操心得不要一上来就追求“高性能锁”。大部分业务场景下synchronized经过JVM优化后性能已经足够好。过早引入ReentrantLock反而增加了代码复杂度和出错概率。先用最简单的方案把功能做对压测发现锁竞争确实是瓶颈了再考虑优化。3. 线程间通信等待与唤醒的正确姿势3.1 wait/notify机制的本质多个线程操作共享资源时经常需要协调生产者发现队列满了要等消费者消费消费者发现队列空了要等生产者生产。这种协调就是线程间通信。Java的Object类提供了wait()、notify()、notifyAll()三个方法来实现这种协调。它们必须在synchronized块中调用否则会抛IllegalMonitorStateException。public class BlockingQueueT { private final QueueT queue new LinkedList(); private final int capacity; public BlockingQueue(int capacity) { this.capacity capacity; } public synchronized void put(T item) throws InterruptedException { while (queue.size() capacity) { wait(); // 队列满释放锁并等待 } queue.offer(item); notifyAll(); // 唤醒等待的消费者 } public synchronized T take() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空释放锁并等待 } T item queue.poll(); notifyAll(); // 唤醒等待的生产者 return item; } }这段代码有几个关键点值得展开。第一wait()会释放锁这是它和sleep()最本质的区别。sleep()只是让线程暂停不释放任何锁。第二wait()被唤醒后需要重新竞争锁拿到锁之后从wait()下一行继续执行。第三判断条件必须用while而不是if因为线程被唤醒后条件可能又变了需要重新检查。3.2 为什么用while而不是if这是面试高频题也是实际写代码最容易出错的地方。假设用if判断if (queue.isEmpty()) { wait(); } // 被唤醒后直接往下执行 T item queue.poll();问题在于线程被notifyAll唤醒后它需要重新获取锁。在它拿到锁之前可能有另一个消费者先拿到锁并把唯一的元素取走了。等它拿到锁继续执行时队列又空了poll()返回null程序出错。用while的话被唤醒后会重新检查条件发现队列还是空的就继续wait不会出错。注意notify()只唤醒一个等待线程notifyAll()唤醒所有。大多数情况下应该用notifyAll()因为notify()可能唤醒一个不满足条件的线程导致信号丢失。只有在确定只有一个等待线程且条件明确时才用notify()。3.3 Condition更精细的等待通知ReentrantLock配合Condition可以实现更精细的控制。一个锁可以创建多个Condition不同条件上等待的线程互不干扰。private final ReentrantLock lock new ReentrantLock(); private final Condition notFull lock.newCondition(); private final Condition notEmpty lock.newCondition(); public void put(T item) throws InterruptedException { lock.lock(); try { while (queue.size() capacity) { notFull.await(); } queue.offer(item); notEmpty.signal(); } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } T item queue.poll(); notFull.signal(); return item; } finally { lock.unlock(); } }这样生产者只在notFull上等待消费者只在notEmpty上等待生产者唤醒的是消费者消费者唤醒的是生产者避免了notifyAll唤醒所有线程带来的无效竞争。3.4 管道通信与BlockingQueue除了wait/notifyJava还提供了管道流PipedInputStream/PipedOutputStream用于线程间字节流通信以及java.util.concurrent包下的BlockingQueue系列。实际开发中BlockingQueue是最常用的线程间通信方式它内部已经封装好了等待通知逻辑用起来非常省心。BlockingQueueTask queue new LinkedBlockingQueue(100); // 生产者线程 queue.put(task); // 队列满时自动阻塞 // 消费者线程 Task task queue.take(); // 队列空时自动阻塞ArrayBlockingQueue基于数组实现容量固定支持公平锁LinkedBlockingQueue基于链表默认容量是Integer.MAX_VALUE吞吐量通常更高SynchronousQueue不存储元素每个put必须等待一个take适合传递性场景PriorityBlockingQueue支持优先级排序。选哪个取决于你的具体需求。4. 内存可见性看不见的并发陷阱4.1 一个永远停不下来的循环先看一段代码public class VisibilityDemo { private static boolean running true; public static void main(String[] args) throws InterruptedException { new Thread(() - { while (running) { // do something } System.out.println(线程结束); }).start(); Thread.sleep(1000); running false; System.out.println(已设置running为false); } }直觉上主线程把running设为false后子线程的循环应该退出。但实际运行中子线程可能永远看不到这个变化循环一直执行下去。原因在于Java内存模型JMM允许线程把共享变量缓存到自己的工作内存中子线程一直读的是自己工作内存里的副本主线程的修改对它不可见。4.2 JMM与happens-before原则JMM定义了线程和主内存之间的抽象关系每个线程有自己的工作内存共享变量存储在主内存中。线程对变量的操作必须先在主内存中读取到工作内存修改后再写回主内存。不同线程之间无法直接访问对方的工作内存必须通过主内存传递。happens-before是JMM的核心规则它定义了哪些操作的结果对后续操作可见。几条关键规则程序顺序规则一个线程内前面的操作happens-before后面的操作监视器锁规则解锁操作happens-before后续对同一锁的加锁操作volatile变量规则对volatile变量的写happens-before后续对该变量的读传递性A happens-before BB happens-before C则A happens-before C4.3 volatile轻量级的可见性保证给running加上volatile修饰循环就能正常退出了private static volatile boolean running true;volatile保证了两件事第一写volatile变量时会立即刷新到主内存第二读volatile变量时会从主内存读取最新值。这样就保证了可见性。但volatile不保证原子性。volatile int count; count;这样的操作依然有线程安全问题因为自增不是原子操作。volatile适合用在状态标志位、一次性安全发布等场景。实操心得很多并发bug的根源就是可见性问题但它的表现往往很隐蔽——在开发机上跑没问题一到生产环境多核服务器就出问题。因为单核CPU下线程切换时缓存会同步多核下各核缓存独立可见性问题才暴露出来。所以并发代码一定要在多核环境测试。4.4 final与安全发布final字段也有特殊的可见性保证。被final修饰的字段在构造函数中初始化完成后其他线程就能看到正确的值不需要额外同步。这被称为安全发布。public class SafeObject { private final int value; private final String name; public SafeObject(int value, String name) { this.value value; this.name name; } }只要对象在构造过程中没有让this引用逸出其他线程拿到这个对象引用时就能看到final字段的正确值。这也是为什么推荐尽量把字段声明为final的原因之一。5. CAS与无锁编程乐观并发的艺术5.1 CAS的基本原理CAS全称Compare-And-Swap翻译过来就是“比较并交换”。它包含三个操作数内存位置V、预期旧值A、新值B。当且仅当V的当前值等于A时才把V更新为B否则不做任何操作。整个比较和交换是一个原子操作由CPU指令直接支持。Java中通过Unsafe类提供CAS能力但普通开发者一般用java.util.concurrent.atomic包下的原子类。AtomicInteger count new AtomicInteger(0); count.incrementAndGet(); // 原子自增incrementAndGet的底层就是一个CAS循环先读取当前值加一然后CAS尝试写回。如果失败说明有其他线程改了就重新读取再试直到成功。5.2 ABA问题与解决方案CAS有一个经典问题叫ABA线程1读取到值A准备改成C此时线程2把值从A改成B又改回A线程1的CAS发现值还是A就成功改成了C。但中间状态B被忽略了某些场景下这会导致问题。解决方案是加版本号每次修改版本号递增。Java提供了AtomicStampedReferenceAtomicStampedReferenceInteger ref new AtomicStampedReference(100, 0); int stamp ref.getStamp(); ref.compareAndSet(100, 200, stamp, stamp 1);只有当引用和版本号都匹配时才更新避免了ABA问题。5.3 LongAdder高并发计数的终极方案AtomicLong在并发极高时所有线程都CAS同一个变量失败重试的次数会急剧增加性能下降。LongAdder的思路是分散热点内部维护一个Cell数组不同线程更新不同的Cell最后求和时把所有Cell的值加起来。LongAdder adder new LongAdder(); adder.increment(); long sum adder.sum();在写多读少的计数场景下LongAdder的吞吐量可以比AtomicLong高一个数量级。代价是sum()方法需要遍历所有Cell不是常量时间。所以如果读操作也很频繁AtomicLong可能更合适。5.4 CAS的适用场景与局限CAS适合竞争不太激烈、操作简单的场景。它的优点是无需加锁不会引起线程阻塞和上下文切换。缺点是竞争激烈时自旋次数多CPU开销大只能保证单个变量的原子性多个变量需要额外处理ABA问题需要版本号解决代码复杂度比直接加锁高我的经验是能用原子类解决的简单计数、状态标记优先用原子类涉及多个变量需要保持一致性的复合操作老老实实用锁。不要为了追求“无锁”而把代码写得晦涩难懂。6. 线程状态与生命周期看清线程的每一步6.1 六种线程状态Java中Thread.State枚举定义了六种线程状态状态含义进入方式NEW新建new Thread()后未调用start()RUNNABLE可运行调用start()后包括正在运行和就绪等待CPUBLOCKED阻塞等待获取synchronized锁WAITING无限等待调用wait()、join()、park()TIMED_WAITING限时等待调用sleep(ms)、wait(ms)、join(ms)TERMINATED终止run()方法执行完毕或异常退出注意RUNNABLE包含了操作系统层面的就绪和运行两种状态因为JVM不区分这两种都交给操作系统调度。6.2 状态转换的实际场景理解状态转换对排查问题很有帮助。比如你发现线程dump里大量线程处于BLOCKED状态说明锁竞争严重需要检查是不是有线程持有锁时间过长。如果大量线程处于WAITING状态可能是wait()后没有被正确唤醒或者线程池任务提交后没有执行。Thread t new Thread(() - { synchronized (lock) { try { lock.wait(); // WAITING } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); t.start(); // 此时t的状态是WAITING6.3 如何优雅地停止线程Thread.stop()已经被废弃因为它会强制释放锁导致数据不一致。正确的做法是使用中断机制public class StoppableTask implements Runnable { private volatile boolean running true; public void shutdown() { running false; } Override public void run() { while (running !Thread.currentThread().isInterrupted()) { try { // 阻塞操作响应中断 Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断标志 break; } // 执行任务 } } }这里有两个关键点第一用volatile标志位配合中断检查双重保险第二捕获InterruptedException后要恢复中断标志让上层调用者知道发生了中断。6.4 线程dump的实战分析线上环境排查并发问题线程dump是最重要的工具之一。通过jstack pid或者kill -3 pid可以导出所有线程的堆栈信息。拿到dump后重点看几个东西有没有死锁会明确提示Found one Java-level deadlock哪些线程处于BLOCKED且等待同一个锁线程池的线程是不是都在WAITING且任务队列堆积。我遇到过一次线上接口超时dump出来发现所有业务线程都BLOCKED在一个数据库连接池的锁上原因是连接池配置太小大量线程排队等连接。这种问题看dump一眼就能定位。7. 常见问题与排查技巧实录7.1 并发问题速查表现象可能原因排查方向计数结果偏小竞态条件自增非原子检查是否加锁或用原子类循环不退出内存可见性问题检查标志位是否volatile线程卡死不动死锁或锁顺序不一致jstack看BLOCKED和死锁提示数据读到旧值缺少happens-before保证检查同步措施是否覆盖读写性能急剧下降锁竞争激烈或CAS自旋过多考虑分段锁、LongAdder或减小锁粒度wait后不醒信号丢失或notify用错检查是否用while、是否notifyAll7.2 死锁的四个必要条件与预防死锁需要同时满足四个条件互斥、持有并等待、不可剥夺、循环等待。破坏其中任意一个就能避免死锁。最常用的是破坏循环等待给所有锁定义全局顺序所有线程按相同顺序获取锁。// 错误示范不同线程以不同顺序获取锁 // 线程1: lockA - lockB // 线程2: lockB - lockA // 正确做法统一按id顺序获取 if (lockA.getId() lockB.getId()) { lockA.lock(); lockB.lock(); } else { lockB.lock(); lockA.lock(); }7.3 伪共享一个容易被忽视的性能杀手CPU缓存以缓存行为单位通常是64字节。如果两个volatile变量位于同一个缓存行一个线程修改变量A会导致另一个线程的变量B缓存失效这就是伪共享。解决方案是用填充字段把变量隔开public class PaddedAtomicLong { public volatile long value; public long p1, p2, p3, p4, p5, p6, p7; // 填充 }JDK 8之后可以用sun.misc.Contended注解但需要开启JVM参数-XX:-RestrictContended。实际业务开发中很少需要手动处理伪共享但了解这个概念有助于理解为什么某些高性能框架要这么设计。7.4 并发测试的正确姿势并发bug很难复现因为涉及线程调度顺序。写并发测试时不要只跑一两次就下结论。可以用CountDownLatch让所有线程同时开始增加竞争概率int threadCount 100; CountDownLatch startGate new CountDownLatch(1); CountDownLatch endGate new CountDownLatch(threadCount); for (int i 0; i threadCount; i) { new Thread(() - { try { startGate.await(); // 所有线程在此等待 // 执行并发操作 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { endGate.countDown(); } }).start(); } startGate.countDown(); // 同时放行 endGate.await(); // 等待全部完成另外可以借助一些压力测试工具反复跑或者用Thread.sleep()在关键位置插入随机延迟来打乱执行顺序提高bug复现概率。7.5 我踩过的几个坑第一个坑是过度同步。早期写代码时为了“保险”把整个方法都加上synchronized结果性能极差。后来学会只锁必要的代码块把不需要同步的操作移出同步区域。第二个坑是锁对象选错。用字符串常量或者Integer做锁对象因为常量池和缓存的原因不同地方的锁可能意外地是同一个对象导致无关的线程互相阻塞。正确做法是用专门的Object实例或者类对象做锁。第三个坑是忽略了线程池的队列容量。用Executors.newFixedThreadPool创建线程池时任务队列是无界的任务堆积过多会导致内存溢出。后来都改成手动创建ThreadPoolExecutor指定有界队列和拒绝策略。第四个坑是在synchronized块中调用外部方法。外部方法可能获取其他锁或者执行耗时操作导致锁持有时间过长甚至死锁。原则是同步块里只做必要的共享变量操作不要调用不可控的外部代码。8. 从理论到落地构建线程安全代码的检查清单写并发代码时我习惯按这个清单过一遍共享变量是否都用private修饰通过方法暴露访问复合操作检查再执行、读改写是否保证了原子性状态标志位是否用了volatile锁的粒度是否足够小同步块里有没有耗时操作多个锁的获取顺序是否全局一致是否优先使用了java.util.concurrent包下的现成工具线程池是否配置了合理的队列容量和拒绝策略是否考虑了中断响应和优雅停止这套清单不能保证百分之百不出问题但能覆盖大部分常见陷阱。并发编程的难点不在于记住API而在于理解背后的内存模型和调度机制。多写多调试遇到问题用工具去分析而不是靠猜慢慢就能建立起对并发代码的直觉。

相关新闻

SSIMLoss在CoRRN图像复原中的原理与实战集成

SSIMLoss在CoRRN图像复原中的原理与实战集成

1. 什么是SSIMLoss,它为什么在CoRRN这类图像复原模型里成了“隐形指挥官”SSIMLoss不是某个新出的网红框架,也不是某家大厂刚开源的黑科技库——它本质上是一种结构相似性(Structural Similarity Index Measure)驱动的损失函数设计…

2026/10/15 12:18:53 阅读更多 →
前端注释规范与工程化实践指南

前端注释规范与工程化实践指南

1. 前端注释的本质与价值2008年我刚入行时接手过一个jQuery时代的遗留项目,那个没有注释的3万行代码库让我深刻理解了"程序员最恨两件事:写注释和不写注释的人"。前端注释远非简单的代码说明,而是工程化开发中不可或缺的协作工具链…

2026/10/15 12:18:53 阅读更多 →
Halcon相机标定本质:重建几何关系而非调参

Halcon相机标定本质:重建几何关系而非调参

简介:本资源是Halcon官方参考手册第六章“Calibration(标定)”的完整英文直译中文版PDF,面向计算机视觉工程师、工业视觉检测算法开发者及高校相关方向研究者,系统解决相机模型构建、畸变补偿与多场景标定实施等核心问…

2026/10/15 12:18:53 阅读更多 →

最新新闻

人脸特征提取三路线:HoG、Dlib与CNN原理及选型指南

人脸特征提取三路线:HoG、Dlib与CNN原理及选型指南

人脸识别这几年从实验室走向了门禁、考勤、相册聚类、甚至手机解锁,几乎成了计算机视觉里最"接地气"的方向之一。但真到动手做项目的时候,很多人会卡在同一个地方:特征到底怎么提?我见过不少朋友一上来就堆深度学习模型…

2026/10/15 13:00:25 阅读更多 →
校园失物招领系统

校园失物招领系统

文章目录前言项目简介项目背景技术栈系统架构核心功能截图后台管理公告管理认证授权认领管理评论收藏失物招领信息消息用户管理首页失物列表发布信息我的发布个人中心核心技术实现项目亮点项目总结项目下载:前言 项目简介 面向高校校园场景的前后端分离 Web 应用&a…

2026/10/15 13:00:25 阅读更多 →
基于4D关联图的多相机实时多人动作捕捉技术解析

基于4D关联图的多相机实时多人动作捕捉技术解析

1. 从"多相机多人动捕"这个硬骨头说起第一次看到"4D Association Graph"这个名词的时候,我正蹲在一个临时搭建的动作捕捉棚里,面对八台同步相机拍出来的画面发愁。画面里三个人在互相传球,手臂交叠、身体遮挡、快速移动&…

2026/10/15 13:00:25 阅读更多 →
SSM在线作业管理系统:从选题到答辩的全链路解析

SSM在线作业管理系统:从选题到答辩的全链路解析

又到了一年毕业设计选题季,后台私信里反复被同一个关键词轰炸:SSM在线作业管理系统。实话说,这个题在计算机毕业设计里属于"经典中的经典",每年都有大量学生选它,但真正能讲清楚"为什么这么设计、每一步…

2026/10/15 13:00:25 阅读更多 →
Network Kit HTTP:三方库版本清单ETag重验证与离线快照保底【鸿蒙心迹】

Network Kit HTTP:三方库版本清单ETag重验证与离线快照保底【鸿蒙心迹】

场景说明: 本文 ManifestHarbor 的 MH-1421、v17、8 个依赖项、ETag "ohpm-v17"、304 响应与 HiLog 均为受控演示数据。示意图不是 DevEco Studio、真机或真实服务器的测试结果;代码依据公开 Network Kit API 设计,尚未在实际 Harm…

2026/10/15 13:00:25 阅读更多 →
Hyper-Frames:无标签视频+里程计自监督预训练,实现低成本运动预测

Hyper-Frames:无标签视频+里程计自监督预训练,实现低成本运动预测

做自动驾驶、移动机器人、无人机导航的同行,应该都有一段被数据集标注反复折磨的经历。最近 Waymo Research 放出的 Hyper-Frames 框架,让我终于看到一条不太一样的路:不用一开始就堆人天去标框、标轨迹、标地图,用无标签视频加里…

2026/10/15 12:59:25 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →