news 2026/9/9 10:54:42

Java并发编程核心要点解析:从JMM到线程池与锁实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java并发编程核心要点解析:从JMM到线程池与锁实践

1. Java并发要解决的本质问题

1.1 为什么并发问题这么难:先聊聊JMM

先说一个很多新人容易踩的误区:并发问题不是“多线程同时跑”这么简单,真正的难点在于共享内存的可见性操作的有序性。Java为了解决跨平台的内存访问差异,在语言层面定义了一套规范,叫Java内存模型(Java Memory Model,JMM)。这个模型规定了什么时候一个线程对变量的修改,另一个线程“一定能看到”。

我一般给团队新人举一个生活化例子:公司会议室的方案白板是主内存,每个员工的笔记本是线程的工作内存。你在笔记本上写了一版方案,别人手里拿的是旧版本,除非你把内容同步回白板,或者对方主动来看白板,否则你们俩做出来的东西一定不一致。多线程环境下,每个线程会把共享变量复制一份到自己的“工作内存”,然后在工作内存里读写,刷回主内存的时机并不确定。

JMM还规定了两个线程之间通信的底层机制是共享主内存,但对应用开发者来说,真正要遵守的是几条核心规则:

  • 一个线程解锁(释放锁)之前对共享变量的修改,对之后获取到同一个锁的线程可见。
  • 对volatile变量的写操作,会立即刷回主内存,读操作则会强制从主内存读取。
  • 线程启动、终止、中断等操作,都有对应的内存可见性语义。

如果违反这些规则,程序就会表现出“看起来完全没逻辑”的诡异现象。比如两个线程同时对同一个int变量做一万次自增,最后结果不是20000,而可能是10000多,甚至更少。这是我在培训时最爱用的开场案例。

1.2 三个核心特性与volatile的边界

把并发问题拆到底,就三个维度:原子性、可见性、有序性

原子性指的是一个操作要么全部执行,要么全部不执行,中间不能被其他线程打断。比如i++,它看起来是一行代码,但字节码层面其实是“读变量、加一、写回”三步,在并发环境下这三步可能被打散。

可见性指的是一个线程修改了共享变量,其他线程能不能立刻看到。刚才说的JMM工作内存机制,就是导致可见性问题的主因。

有序性指的是代码执行的顺序。JVM和CPU为了提高性能,会做指令重排(编译期重排、处理器乱序执行),只要最终结果在单线程内保持一致,重排就被允许。但在多线程环境下,重排可能让另一个线程读到“还没初始化完成的中间状态”。

很多初学者以为加个volatile就能解决所有并发问题,这是大忌。volatile能保证可见性,能禁止部分指令重排,但它不保证原子性。经典的例子是volatile int count,多个线程同时执行count++,最终结果依然不对。

我见过一个比较经典的代码,双重检查锁单例(DCL)在JDK 5之前因为指令重排会出现拿到半初始化对象的情况,后来加了volatile禁止重排才算修复。这是面试高频考点,也说明了有序性问题在实际工程里的杀伤力。

1.3 从一段代码看并发“翻车”

来看一个非常典型的并发问题:

public class RaceConditionDemo { private static int count = 0; private static final int THREADS = 4; private static final int TIMES = 10000; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[THREADS]; for (int i = 0; i < THREADS; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < TIMES; j++) { count++; } }); threads[i].start(); } for (Thread t : threads) { t.join(); } System.out.println("count = " + count); // 期望值是 40000,但实际几乎不可能等于 40000 } }

这段代码在大多数机器上跑出来的结果都会小于40000。问题就出在count++不是原子操作。单纯给count加volatile,问题仍然存在,因为volatile不解决原子性。

正确的修复方式有几种:

  • 给自增操作加synchronized(串行化修改)。
  • 使用AtomicInteger(CAS自旋修改)。
  • 使用LongAdder(高并发下分段累加)。

顺带提一个小细节:很多人分不清StringBuffer是不是线程安全的。是的,它内部方法加了synchronized,同一时刻只有一个线程能修改某个StringBuffer实例。但因为锁竞争有开销,单线程下StringBuilder反而更快。这其实引出了一个通用原则:并发安全是有代价的,不要为一个根本不存在竞争的场景,付出锁的代价。

2. 从线程到线程池:并发实现的第一层演进

2.1 线程的创建与生命周期:从Thread到Callable

Java里创建线程有几种方式,但本质上都在说同样的事:让一段代码跑在一个独立的执行路径上。

  • 继承Thread类,重写run()
  • 实现Runnable接口,传给Thread
  • 实现Callable接口,配合FutureTask,可以拿到返回值。

很多面试官会问start()run()的区别。start()才是真正创建一个新线程并进入就绪状态,由JVM调用run();直接调用run()只是在当前线程里执行一个普通方法,根本没有新线程。

线程的生命周期状态包括:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。我见过不少新人以为Thread.sleep()会让线程不占用CPU,实际它只是让线程进入TIMED_WAITING,让出CPU调度,但锁资源不会释放。如果拿着锁去sleep,其他线程依然会被卡住,这在写代码时经常被忽略。

2.2 为什么不推荐裸new线程:线程池的必要性

直接new Thread去执行异步任务,在小Demo里没问题,但在生产环境就是灾难。

第一个问题是创建和销毁线程有开销,包括操作系统分配内核资源、JVM分配线程栈等。频繁创建销毁线程,耗时可能比业务执行本身还大。

第二个问题是无边界创建线程会拖垮系统。每个线程默认要占用几百KB到1MB的栈内存,如果某个接口被大量并发请求打进来,系统可能直接OOM。JVM进程能创建的线程数不是无限的,除非你显式调过-Xss,否则一个进程撑到几千个线程时系统就已经很危险了。

线程池的价值在于:复用固定数量的工作线程,把任务放到队列里排队,线程吃饱了继续从队列取任务。这样既能控制并发度,又能避免频繁创建线程的开销。这也是Java并发实现里最实用、最常用的一块。

2.3 ThreadPoolExecutor核心参数与拒绝策略

我们在生产环境里用的最多的还是ThreadPoolExecutor,它的核心参数有七个,每一个都可能成为性能瓶颈:

参数作用
corePoolSize核心线程数,即使空闲也保留
maximumPoolSize线程池允许的最大线程数
keepAliveTime非核心线程空闲存活时间
unitkeepAliveTime的时间单位
workQueue任务等待队列(BlockingQueue)
threadFactory创建线程的工厂,建议自定义线程名前缀
handler线程池和队列都满时,任务拒绝策略

最简单的执行流程可以这样记:任务提交后,如果当前线程数少于corePoolSize,直接创建核心线程执行;如果核心线程都在忙,任务进队列排队;如果队列满了,开始创建非核心线程执行;如果线程数已经到maximumPoolSize,队列也满了,触发拒绝策略。

四种拒绝策略里,ThreadPoolExecutor.AbortPolicy是默认的,直接抛RejectedExecutionException。我在工作里反而更喜欢CallerRunsPolicy,它会让提交任务的线程自己来执行任务。这样好处是任务不会丢,而且相当于一种天然的背压机制:如果线程池忙不过来,调用方也会被拖住,整体系统就不会疯狂堆积。

关于线程池参数怎么设置,没有一个万能公式,但可以按场景粗估。CPU密集型任务,线程数可以设为CPU核心数+1;IO密集型任务,比如大量查询数据库、调用远程接口,线程数可以设成CPU核心数乘2左右,再根据实际压测结果调整。我的经验是:先保守设一个值,再通过监控指标动态调整,比任何公式都靠谱。

3. 锁与同步机制:并发实现的核心武器

3.1 synchronized的底层故事

synchronized是Java里最基础的同步手段,但从JDK 1.6之后,它的实现已经非常复杂,不能简单理解成“重量级锁”。

synchronized在字节码层面是通过monitorentermonitorexit指令实现的。锁对象会有一个Monitor对象,持有monitor的线程才能进入临界区。JVM对synchronized做了多级优化,按竞争激烈程度,锁会从无锁膨胀到偏向锁、轻量级锁、重量级锁。

  • 无锁:没有线程竞争时,普通对象直接无锁访问。
  • 偏向锁:同一个线程反复进入同步块时,锁会偏向这个线程,记录线程ID,后续再进就不用重新竞争。
  • 轻量级锁:一旦有第二个线程来竞争,偏向锁会撤销,改用CAS在对象头上记录锁记录,线程通过自旋等待。
  • 重量级锁:自旋也搞不定,就升级为依赖操作系统互斥量的重量级锁,未获得锁的线程会进入阻塞状态。

需要特别注意的是,JDK 15之后偏向锁已经被标记为废弃,在高版本JDK里默认被禁用。所以网上很多讲偏向锁的文章,在新版本环境下已经不太适用,看源码时要注意版本。

synchronized可以加在实例方法、静态方法、代码块上,锁对象各不相同:实例方法锁的是this,静态方法锁的是Class对象,代码块锁的是括号里的对象。如果不小心用了不同的锁对象,根本起不到互斥效果。我也犯过这类低级错误。

3.2 Lock接口与AQS

synchronized虽然方便,但功能上有短板:不能响应中断、不能设置超时、无法尝试非阻塞获取锁。从JDK 5开始,java.util.concurrent.locks.Lock接口提供了一套更灵活的锁方案。

ReentrantLock是最常用的实现,它支持公平锁和非公平锁。非公平锁性能更好,因为刚释放锁的线程立刻再竞争,不需要做线程切换;公平锁则按申请顺序分配,避免线程饥饿,但吞吐量会低一些。

谈到ReentrantLock就绕不开AQS(AbstractQueuedSynchronizer),它可以说是整个juc包的基石。AQS内部维护一个volatile int state变量和一组等待线程队列。像ReentrantLock只重写了tryAcquire和tryRelease,用来决定state如何变化;Semaphore、CountDownLatch、ReentrantReadWriteLock等都是基于AQS改出来的,只是对state的语义不同。

用ReentrantLock时,我建议在finally块里释放锁,否则一旦业务代码抛异常,锁不会被释放,线程会一直卡住。最基本的模板是:

Lock lock = new ReentrantLock(); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }

3.3 原子类与CAS

除了加锁,还有一种更轻量的并发实现思路:CAS(Compare And Swap)。它的核心逻辑是“先比较再交换”,只有当内存中的值和预期值一致时,才把新值写进去,否则就一直重试。

java.util.concurrent.atomic包下面的AtomicIntegerAtomicLongAtomicReference都是基于CAS实现的。比如AtomicInteger.incrementAndGet(),底层是Unsafe类提供的compareAndSwapInt方法。CAS避免了线程上下文切换的开销,但高并发下如果竞争很激烈,自旋重试会大量消耗CPU。

CAS有个经典问题叫ABA问题:一个线程把变量从A改成B再改回A,另一个线程执行CAS时发现值还是A,就以为没有任何变化。只关心值的情况一般没问题,但如果要关心“是否被改过”,就需要用AtomicStampedReference加上版本号。

在高并发的计数场景,LongAdderAtomicLong更好用。LongAdder内部维护了一个基础计数器和若干个Cell,多个线程各自累加到不同的Cell,最后再求和,相当于分片缩减了竞争。我们团队之前做抢券系统的计数,压力测试显示LongAdder的提升比AtomicLong明显不少。

4. 并发容器:多线程下的数据基础设施

4.1 ConcurrentHashMap是怎么做到高性能并发的

并发场景下最常用的容器,非ConcurrentHashMap莫属。

在JDK 7时代,ConcurrentHashMap使用分段锁机制,把整个Map分成一段一段,每个Segment自己有一把锁,多线程访问不同段时互不干扰。到了JDK 8,实现改成了CAS + synchronized:数组里的每个桶第一个节点加锁,锁粒度更细,并发度也更高。put流程大概是:

  1. 计算key的hash,定位到桶。
  2. 如果桶为空,用CAS直接放入节点。
  3. 如果桶不为空,对桶头节点加synchronized,再插入链表或红黑树。
  4. 如果链表长度超过阈值(8),且数组长度达标,转成红黑树。

ConcurrentHashMap有个设计上的限制:不允许null key和null value。原因是它不能区分“key不存在”和“key对应的value是null”,在多线程环境下避免了一次额外的查询。网上有专门的面试题问这个,值得注意。

和它形成对比的是Hashtable,它把所有方法直接加synchronized,锁的粒度是整个表,并发效率很低。还有Collections.synchronizedMap,同样是把所有操作串行化,适合并发量极小的场景。

4.2 处理“读多写少”场景:CopyOnWriteArrayList和ConcurrentLinkedQueue

有一种并发场景是读操作远多于写操作,比如配置缓存、白名单列表,这时候用CopyOnWriteArrayList很合适。

CopyOnWriteArrayList的核心思路是:每次修改(add、remove)都会复制一个新的底层数组,修改在新数组上完成,然后把volatile的数组引用指向新数组。读操作完全不用加锁,因为读的是不可变的旧数组快照。听着很美好,但写成本很高,如果频繁写,会反复复制整个数组,内存和GC压力都很大。所以它只适合“读多写极少”的场景,这一点我反复提醒团队,别用错了场景。

ConcurrentLinkedQueue则是基于CAS实现的一个无锁队列,适合高并发下的生产者消费者模式。因为用的是无锁算法,不会有锁竞争导致的阻塞,但实际运用时它的queue.size()是O(n)遍历,不高效,如果只是取大小做限制判断,建议用AtomicInteger自己维护一个计数器。

4.3 线程之间的“数据管道”:BlockingQueue

如果说并发容器里有一个“神器”,我觉得是阻塞队列。它不仅是一份数据集合,还天然承担了线程间的协调功能:队列为空时,消费者线程会被阻塞等待;队列满时,生产者线程会被阻塞等待。

常用的实现各有适用场景:

  • ArrayBlockingQueue:有界数组队列,适合作为线程池的任务队列,可以避免线程池在请求峰值时背太多任务。
  • LinkedBlockingQueue:链表队列,可以方便设置容量上限,默认容量是Integer.MAX_VALUE,不设的话很容易堆积。
  • SynchronousQueue:不存储元素,每次put必须等到take,适合直接交付的生产者消费者模式。
  • DelayQueue:延迟队列,可以按到期时间出队,适合做定时任务调度。

前文说的线程池workQueue,底层就是BlockingQueue,所以理解阻塞队列对配置线程池非常有帮助。我们在压测时发现,不同队列策略对线程池表现影响极大,ArrayBlockingQueue容量设得太小,任务就容易被拒绝;设得太大,高并发下又会积压大量任务,导致业务响应延迟变高,这两者要平衡。

5. 异步编程:从Future到CompletableFuture

5.1 Future的短板与CompletableFuture登场

用线程池执行任务时,我们经常需要拿到任务的执行结果。JDK 5提供的Future可以做到,但它有个很大的毛病:future.get()是一个阻塞方法,如果任务还没执行完,当前线程会一直在那儿等,效果相当于把异步又变回了同步。

另外,当你需要把多个异步任务编排起来,比如“等两个接口都返回后,再汇总数据”,用Future写起来特别别扭。你不得不逐个get,然后串行处理,期间可能还有线程被白白阻塞。

CompletableFuture解决的正是这两个痛点。它实现了CompletionStage接口,可以像流水线一样组合异步操作的阶段。常用API包括:

  • thenApply:把前一个阶段的结果做转换。
  • thenCompose:把两个有依赖关系的异步任务扁平化组合。
  • thenCombine:组合两个无依赖的异步任务,两个都完成后执行回调。
  • allOf:等待所有任务完成。
  • anyOf:任意一个任务完成就触发。
  • exceptionally:异常时的兜底处理。
  • whenComplete:阶段完成时执行,不改变结果。

5.2 基于CompletableFuture的异步实践案例

举个我在业务中实际遇到的例子:老订单列表页需要同时展示用户昵称、订单状态文案、商品缩略图,这三个信息分别来自三个不同的微服务,假设每个服务耗时100ms。

如果串行调用,总耗时至少300ms,用户会明显觉得卡。用CompletableFuture实现:

CompletableFuture<String> userFuture = CompletableFuture.supplyAsync( () -> userService.getNickname(order.getUserId()), executor); CompletableFuture<String> statusFuture = CompletableFuture.supplyAsync( () -> orderStatusService.getStatusText(order.getStatus()), executor); CompletableFuture<String> imageFuture = CompletableFuture.supplyAsync( () -> productService.getThumbnail(order.getProductId()), executor); CompletableFuture.allOf(userFuture, statusFuture, imageFuture).join(); String nickname = userFuture.join(); String statusText = statusFuture.join(); String thumbnail = imageFuture.join();

这里必须注意:supplyAsync如果不传线程池,默认用ForkJoinPool.commonPool()。这个公共池在异步任务里隐式使用,一旦某个任务里有阻塞IO,会影响所有使用commonPool的代码。我在生产上坚持用独立的业务线程池,避免公共池被拖垮。

5.3 聊聊新版本里的虚拟线程

JDK 21正式发布了虚拟线程(Virtual Threads),这个东西对Java并发实现的影响非常大。虚拟线程由JVM调度,不再是一比一绑定操作系统线程,每个虚拟线程的栈可以动态扩张,所以一个进程可以启动几十万个虚拟线程。

如果写的是IO密集型任务,虚拟线程带来的收益是肉眼可见的:你可以用同步阻塞代码风格,不用写异步回调,也能获得极高的并发吞吐。我们团队在做外部接口调用的压测时,用虚拟线程替换了传统线程池,性能提升非常明显。

但要注意,虚拟线程不是万能的。CPU密集型计算场景下,虚拟线程并不能比平台线程跑得更快,反而可能因为切换过于频繁带来额外开销。另外,在synchronized块里阻塞和锁竞争较多的场景,虚拟线程的优势会被削弱,JDK还在持续优化这一块。现阶段还是建议:新项目可以尝鲜,但核心链路要先用压测验证。

6. 并发问题排查:实战场景实录

6.1 死锁定位与jstack实战

死锁是多线程开发里最经典、也最容易“完全卡死”的问题。两个线程各自持有一把锁,又都去等对方释放锁,就会永远互相等待。

我建议在本地写一个简单死锁代码,然后用工具实际走一遍排查流程:

public class DeadLockDemo { private static final Object A = new Object(); private static final Object B = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (A) { System.out.println("thread1 get A"); try { Thread.sleep(100); } catch (InterruptedException ignored) {} synchronized (B) { System.out.println("thread1 get B"); } } }).start(); new Thread(() -> { synchronized (B) { System.out.println("thread2 get B"); try { Thread.sleep(100); } catch (InterruptedException ignored) {} synchronized (A) { System.out.println("thread2 get A"); } } }).start(); } }

运行后程序卡住不动,这是典型的死锁。排查步骤:

  1. jps找出Java进程PID。
  2. jstack PID导出线程快照。
  3. 在输出里搜“Found one Java-level deadlock”,下面会明确列出线程A持有什么锁、正在等待什么锁、线程B持有什么锁、正在等待什么锁。

我还会注意看线程栈里的调用链,确认是不是业务逻辑里把锁顺序写反了。修复死锁最常用的手段是:所有线程都按同一个顺序加锁,或者用tryLock带超时,拿不到锁就放弃,避免无限期等待。

6.2 线程池拒绝与队列堆积排查

生产环境里线程池最容易出两类问题:一类是任务被拒绝,一类是队列不断堆积。

如果线上日志频繁抛RejectedExecutionException,说明线程池已经严重过载。这时要先看线程池的几个运行指标:活跃线程数是多少、队列容量还剩多少、任务平均耗时多长。最简单的方式是用ThreadPoolExecutor自带的getPoolSize()getActiveCount()getQueue().size()定期采样,或者在监控平台上做埋点。

队列堆积是个更隐蔽的问题。看起来任务没被拒绝,但很多任务一直排队,响应时间越来越高。出现这种情况,我会优先排查是不是某个下游依赖变慢了,比如数据库慢查询、第三方接口超时重试。有时候调线程池参数只是缓解症状,真正的病根在下游。

我也踩过一次坑:为了提升吞吐,把线程池的maximumPoolSize调得很大,结果下游数据库连接池被打爆,整个服务雪崩。后来我对下游服务尽量做“连接数控制 + 熔断降级”,线程池参数反而不用往大了调。

6.3 区分“假并发问题”与真实并发问题

排查线上问题久了会发现,很多看起来像并发问题的问题,根本不是线程竞争导致的。比如项目启动时报错找不到类,热点搜索里经常出现的java.lang.NoClassDefFoundError: java/applet/Applet,大概率是JDK版本和项目依赖不一致,或者引用了新版本JDK中已删除的类。这类问题和并发毫无关系,但新人经常用并发思路去排查,走了很多弯路。

再比如Lombok报错“you aren't using a compiler supported by lombok”,常见原因是用了新版本JDK但Lombok版本太老,需要升级或者改用注解处理器配置。还有环境变量配置问题,很多人照着博客配了JAVA_HOME,但忘记配PATH,或者PATH里多个JDK版本冲突,导致运行时版本和编译版本不一样。

我给团队的建议是:先确认问题发生在“编译期”还是“运行期”,再确认是“构建环境”还是“业务代码”导致,不要一看到异常就怀疑线程安全。只有能稳定复现、并且代码路径里确实存在共享可变状态时,才值得往并发方向深挖。

6.4 并发面试高频点速查表

并发相关知识点几乎是Java后端面试的必考模块,我整理过一份高频速查,对工作也有帮助:

问题关键回答要点
为什么ConcurrentHashMap不能存null?无法区分key不存在和value为null的情况,避免多线程下再次查询产生歧义
volatile和synchronized的区别?volatile保证可见性和有序性,不保证原子性;synchronized保证互斥,兼具可见性和原子性
Thread.sleep和wait的区别?sleep不释放锁;wait释放锁并进入等待队列,需要notify唤醒
线程池核心线程会被回收吗?默认不会;设置了allowCoreThreadTimeOut(true)后,核心线程也会在空闲超时后回收
什么是CAS?比较并交换,乐观锁实现,多用于无锁并发,需要处理ABA问题
ReentrantLock和synchronized怎么选?功能复杂、需要超时/中断/公平锁时用ReentrantLock;一般场景synchronized够用且实现不断优化

这份表格看着像八股文,但每个点背后都有真实的应用场景。我平时带新人时,会让他们拿着这些问题去对应调过的代码,而不是死背结论。

写在最后的一点体会

做Java并发开发这些年,我最大的感受是:锁、线程池、并发容器这些技术,单独拎出来都不难,难的是组合使用的时候怎么保持“并发安全”和“性能”的平衡。我自己就吃过乱用锁的亏,也给线上服务填过线程池过载的坑。如果让我给一个最实用的建议,那就是写并发代码前,先问自己三个问题:这段代码真的有并发访问吗?共享状态能不能避免?临界区能不能更小一点?很多时候,减少共享、减少竞争,比掌握多少高深API都管用。数据结构和工具类的选型,也永远先考虑场景,再考虑技术。希望这篇文章能帮你少走一些弯路,至少知道从哪里入手,用什么思路去排查问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 10:54:33

Python datetime库详解:核心对象、格式化与实战技巧

我一直觉得&#xff0c;Python里最容易被低估的标准库就是 datetime 。平时写脚本、处理日志、做数据分析&#xff0c;时间处理是躲不掉的硬需求。你要是只会用 time.time() 加加减减&#xff0c;或者靠手写字符串切片去拼日期&#xff0c;那迟早会掉进各种坑里&#xff0c…

作者头像 李华
网站建设 2026/9/9 10:54:10

Vue3进化论:从Options API到Composition API的逻辑重构与工程实践

Vue3 发布这么久&#xff0c;我接触过的团队里仍然有不少人停留在"会用<script setup>写点东西"的阶段&#xff0c;说起 Options API 和 Composition API 的区别&#xff0c;只能答出"前者是选项对象、后者是函数式"这种表面话。这其实挺可惜的&…

作者头像 李华
网站建设 2026/9/9 10:54:04

Python生成器对象与enumerate全解析:理解惰性求值与迭代协议

1. 先确认一件事&#xff1a;print 出 <generator object ...> 到底是啥 1.1 别急着删代码&#xff0c;先看它是不是"错误的外观" 我记得在不少技术群里见过这样的求助截图&#xff1a;有人写完一个生成器表达式&#xff0c;print 了一下&#xff0c;终端里…

作者头像 李华
网站建设 2026/9/9 10:53:27

百考通智能化功能:AI赋能论文降重与去AI痕迹

在学术写作与论文发表的过程中&#xff0c;重复率过高、AI生成痕迹明显&#xff0c;是困扰无数学生与科研工作者的核心难题。不仅可能导致查重不通过&#xff0c;更会影响学术诚信与成果认可度。百考通&#xff08;https://www.baikaotongai.com&#xff09; 凭借智能文本优化技…

作者头像 李华
网站建设 2026/9/9 10:50:27

AI日报:轻量模型、AI编程与Agent工程化

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

作者头像 李华
网站建设 2026/9/9 10:49:59

2026年9月亨得利腕表维修官方服务|多城服务中心专业水准·维修工艺·到店维修指南docx

2026年9月亨得利腕表维修官方服务&#xff5c;多城服务中心专业水准维修工艺到店维修指南  前言  高端腕表维修的口碑核心&#xff0c;从来不靠宣传造势&#xff0c;而是依托各地线下门店的真实服务实力、稳定的维修工艺、优质的到店体验逐步积累而来。亨得利深耕全国多城高…

作者头像 李华