新闻详情

新闻详情

首页 / 资讯中心 / 详情

双重检查锁与volatile:从DCL到内存可见性的完整解析

发布时间:2026/9/28 18:17:23来源:尧图网络
双重检查锁与volatile:从DCL到内存可见性的完整解析
很多人在简历里写“熟悉单例模式”但问到双重检查锁DCL时只会背出那句“要加 volatile”。至于为什么加、不加会怎样、加了之后到底解决了什么问题能讲清楚的人不多。这篇东西就是围绕 DCL 和 volatile 写的把对象创建时的重排序、内存可见性、以及 Java 里的 volatile 和单片机 C 语言里的 volatile 有什么区别一次讲透。如果你是刚接触并发不久或者写过 DCL 但心里一直有个疙瘩这篇内容适合你。我会从最朴素的懒加载单例写起一步一步推导出 DCL再解释 volatile 在中间扮演的角色最后顺带聊一个很多人会混淆的点单片机里也有 volatile但它和 Java 里的 volatile 几乎不是同一个东西。1. DCL 的由来为什么需要双重检查锁1.1 从懒加载到锁单例模式的三阶段演进单例模式最朴素的需求是“整个进程里只有一个实例”。但“只有一个实例”这个结果可以用很多方式实现。早期教科书最爱写的是饿汉式public class Singleton { private static final Singleton INSTANCE new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }这种写法简单、线程安全因为类加载阶段由 JVM 保证了初始化一次。缺点也很明显只要类被加载实例就被创建了。如果这个单例很重比如要建立数据库连接池、加载配置、预热缓存而应用启动后很长时间根本没用到它内存和初始化成本就白花了。于是有了懒加载第一次调用 getInstance 时才创建。public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance null) { instance new Singleton(); } return instance; } }synchronized 保证了线程安全但每次调用 getInstance 都要经过锁的获取和释放。在 JDK 早期锁竞争激烈时这个开销可能比方法本身做的事情还贵。要知道真正需要同步的其实只有 instance null 到 instance new Singleton() 这段初始化逻辑后面的读操作根本不需要锁。DCL 的出发点就是既然大多数时候 instance 已经非空那就不应该每次都进锁只有在发现 instance 为空时才进入同步块做初始化。这就是“双重检查”的名字来源——第一重检查不加锁第二重检查加锁。public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { // 第一重检查无锁 synchronized (Singleton.class) { // 只有疑似为空才进锁 if (instance null) { // 第二重检查锁内 instance new Singleton(); } } } return instance; } }这套代码在没有 volatile 修饰 instance 的情况下看起来天衣无缝锁内的第二次检查保证了不会重复创建实例无锁的第一次检查保证了大多数情况下的高性能。但它实际上是错的。1.2 DCL 的初版写法为什么是错的经典误区如果只说“双重检查锁是错的”那是指上面这段没有 volatile 的版本。它不是逻辑上会创建出多个实例而是可能让别的线程拿到一个“半成品”对象。要理解这一点得先撕开new Singleton()这行代码。它不是一个原子操作在 JVM 层面拆开大致有三步在堆上分配一块内存调用构造函数对这块内存做初始化把 instance 引用指向这块内存。这是逻辑上的顺序。但 CPU 和 JIT 编译器为了性能在满足“不改变单线程执行结果”的前提下可能对指令重排序。第 2 步和第 3 步没有数据依赖它们就可能被调换顺序先让 instance 指向那块内存然后再执行构造函数。一旦发生这种情况就会出现一个窗口期instance 已经不是 null但对象内部的字段还没初始化完成。线程 A 在锁内执行 new Singleton()在重排序后先完成了引用赋值线程 B 同时执行第一次检查看到 instance 非 null于是直接返回拿到一个“构造函数还没跑完”的半成品对象。字段是默认值、状态是乱的程序后面怎么出错都不奇怪。这个问题的关键不在锁而在于那个不加锁的第一次检查。它给了其他线程不加锁就看到 instance 的机会。锁内再安全也管不住锁外线程的裸读取。为什么早期有经验的开发者也会踩这个坑因为 1996 年那篇关于 DCL 的经典文章里就是用 Java 写的而且没提 volatile。当时的 JMMJava 内存模型规范又比较模糊很多人以为 synchronized 块内外的可见性是一致的其实忘了锁外读没有同步保证。直到后来 JMM 明确了 happens-before 规则大家才意识到要让 DCL 成立必须用 volatile 截断重排序并且保证写线程对 volatile 变量的写入在读线程读取该变量时可见。到这儿问题的核心已经浮出水面DCL 要真正正确必须解决两件事一是禁止构造函数与引用赋值之间的重排序二是保证一个线程写入的 instance 引用对另一个线程可见。这两个需求恰好都是 volatile 的看家本领。2. 复现 DCL 的问题现场2.1 一份可以复现问题的错误代码先说清楚想在实际机器上稳定复现 DCL 不加 volatile 时的错误其实是件很困难的事。因为它依赖 JIT 编译器的重排序决策、CPU 的乱序执行、甚至具体的内存屏障策略环境不同表现完全不一样。很多老项目跑了很多年没出问题不代表代码是对的只是“坏运气还没来”。但复现不了不代表不能理解。下面这个例子是常见教学模型虽然加了辅助手段原理是真实的public class DCLSingleton { private static DCLSingleton instance; private final int value; private DCLSingleton() { // 模拟耗时的初始化给重排序留出更明显的时间窗口 value 42; } public int getValue() { return value; } public static DCLSingleton getInstance() { if (instance null) { synchronized (DCLSingleton.class) { if (instance null) { instance new DCLSingleton(); } } } return instance; } }对应的“破坏者”代码一般是多个线程一边调 getInstance一边马上调用 getValue并且断言拿到的 value 一定是 42。在 x86 上StoreLoad 重排序相对保守可能跑几十万次都不出错但在 ARM、PowerPC 这类弱内存模型平台上出错的概率会明显上升。再加上服务器端的 JIT 可能对代码做更激进的重排线上环境比本地更容易翻车。所以理解 DCL 为什么错别依赖复现。它是内存模型层面的问题不是普通的“多跑几次就能看到的逻辑 bug”。2.2 失败根源构造函数重排序与不完全对象让我们把视角从代码拉到硬件。现代 CPU 为了提高流水线效率普遍允许乱序执行。单线程里看起来没什么问题因为最终结果一致但多线程下这种“无害”的重排序会被另一个线程观察到。回到 DCL。问题代码里 instance 是一个普通的静态字段。它没有 volatile也没有 final 之类的特殊语义。于是 JIT 和 CPU 在优化 new DCLSingleton() 时完全有理由把“给 instance 赋值”和“执行构造函数”这两步调换顺序。注意这不是 bug是合法的优化因为对执行这段代码的同一个线程来说后续并不依赖构造函数执行完毕才继续。可另一个线程呢它在if (instance null)处看到 instance 非 null然后直接 return instance。它没有锁、没有 volatile 读所以它根本不知道“构造函数还没跑完”。它会拿这个引用去调 getValue()读到的 value 是默认值 0而不是 42。这就是“不完全对象”问题。DCL 的错误本质上是让一个对象在没有完成安全发布的情况下被其他线程观察到了。安全发布的意思是一个对象被发布引用被其他线程看到时它的 final 字段已经正确初始化而且构造函数期间发生的写入对这个观察线程可见。Java 里实现安全发布的手段说白了就那么几种通过 volatile 变量发布引用通过 static final 或者 final 字段这里要引入安全初始化保证通过持有锁在同一个锁内读写通过并发工具类比如 AtomicReference、ConcurrentHashMap 等。DCL 的问题恰恰在于发布引用的过程发生在锁内但观察引用这个过程在锁外。于是“锁保证可见性”的规则管不到这里。volatile 就是用来补这个洞的。很多人会问我加个 final 字段行不行比如把 value 声明为 final构造函数里给 final 赋值。这能保证 value 这个字段对其他线程可见但保证不了 instance 引用本身的发布顺序。final 字段语义管的是“这个对象内部的数据完成初始化”管不到“另一个线程什么时候能看到引用”。要想 DCL 完整成立靠 final 救不了命最终还是要 volatile 或者干脆换方案。3. volatile 在 DCL 中的关键作用Java 视角3.1 volatile 的两个核心语义可见性与有序性现在把 Java 里的 volatile 放在聚光灯下。很多人只记得“volatile 保证可见性”其实它还有一条同等重要的规矩禁止重排序。先说可见性。当一个线程写了一个 volatile 变量JMM 保证这个写入最终对其他线程的 volatile 读可见。在 x86 上volatile 写会触发一次 StoreLoad 屏障效果类似于让 CPU 把写缓冲区的数据刷到缓存一致性协议可见的范围volatile 读会保证读取到最新写入的值。简单说volatile 读写之间形成了一个天然的“交接点”我写完你马上能看到。再说有序性。JMM 给 volatile 专门定义了一套重排序规则一个 volatile 写之前的普通写不能被重排序到 volatile 写之后一个 volatile 读之后的普通读不能被重排序到 volatile 读之前volatile 写和 volatile 读之间不能互相重排序。用通俗的话说volatile 像一个栅栏它前后的操作不能随随便便跨界。这对 DCL 太关键了构造函数的写入是普通写instance 赋值是 volatile 写。因为“volatile 写之前的普通写不能跑到 volatile 写之后”所以 JIT 和 CPU 都必须保证构造函数执行完才能把 instance 赋出去。这样其他线程一旦读到 instance 非 null它看到的必然是一个已经初始化完成的对象。在 JMM 里还能换一种表述对某个 volatile 变量的写happens-before 于后续对该 volatile 变量的读。于是线程 A 在写 instance 之前做的所有操作包括构造函数里的初始化、对象内部字段的赋值对线程 B 在读到这个 volatile 后都是可见的。这不只解决了“半成品对象”还解决了“对象虽然 new 完了但内部字段在另一个线程眼里是旧值”的问题。3.2 正确 DCL 写法与内存屏障细节正确的 DCL 写起来非常短但要读懂它背后每一行都不是白写的public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }唯一的改动就是给 instance 加了 volatile。但看代码的时候建议把这个改动拆成四个作用去理解第一重检查读 volatile如果非 null说明发布已完成第二重检查读 volatile锁内确认是否真的需要创建instance new Singleton()写 volatile构造工作全部完成后才发布后续所有读 volatile确保能看到完整初始化的对象。从内存屏障角度看Java 里的 volatile 在不同平台上有不同实现。x86 下 volatile 写会插入 StoreStore StoreLoad 屏障volatile 读在 x86 上基本是普通读加上编译器屏障仅靠硬件一致性保障。在 ARM、PowerPC 上则可能插入更重的屏障指令。所以在 x86 上跑得好的代码贴在安卓或嵌入式 JVM 上不一定一样稳。这也是为什么规范层面的保证比“我实测没问题”更重要。关于 volatile 的成本也得说句公道话。很多人把 volatile 想象成慢得离谱的操作实际没那么夸张。在 x86 上volatile 写比普通写多一次内存屏障会导致后续读不能随意重排但单次开销通常在几十纳秒级别。相比 synchronized 的锁获取和释放volatile 几乎没有锁竞争问题也没有线程阻塞唤醒的开销。在 DCL 场景里绝大多数 getInstance 调用只做一次 volatile 读性能已经很接近无锁代码了。3.3 volatile 的局限与替代方案volatile 不是万能的。它解决不了“复合操作”的原子性问题。比如 count 这种读改写操作volatile 只能保证每次 read 到最新值但两个线程同时读、同时写最终还是可能丢更新。DCL 里之所以只用 volatile 就够是因为这个场景比较简单只有初始化这一条写路径且初始化完就不再改了。从“读-判断-写”这个复合流程来看第一重检查哪怕读到的是 null也没关系因为后面还有锁兜底读到非 null也没关系因为 volatile 读到的必然是发布了完整对象的状态。所以 DCL 是 volatile 的教科书场景但不是万灵药。如果你要写一个可以动态替换配置的单例比如定期刷新缓存对象那 volatile 引用搭配不可变对象也可以做到安全发布但替换逻辑本身仍要原子完成通常要配合 CAS 或 synchronized。再聊替代方案。现在很多团队已经不太写 DCL 了原因有几个静态内部类单例更简单public class Singleton { private Singleton() {} private static class Holder { static final Singleton INSTANCE new Singleton(); } public static Singleton getInstance() { return Holder.INSTANCE; } }这个写法利用 JVM 的类初始化锁由《Java 语言规范》保证当多个线程同时触发 Holder 的初始化时只有一个线程会执行clinit其他线程会阻塞等待。它既懒加载又线程安全还没有 volatile 的内存屏障开销。缺点就是类加载的初始化时机不好控制但对大多数场景足够了。枚举单例更省心public enum Singleton { INSTANCE; }枚举天然线程安全、序列化安全、反射攻击防护也更好缺点是“这不像一个正常的类”有些团队觉得代码不易读。但从功能正确性上讲它几乎是标准答案。我个人的习惯是能用枚举解决的就不折腾 DCL要控制初始化时机优先用静态内部类只有确实需要在极少数场景里自己控制发布时机才会回头写 DCL。这样对比下来你会发现DCL 在现代 Java 里更像是一个“理解并发机制的窗口”而不是“第一优先级的实现方案”。但正因为如此把 DCL 搞明白对理解 JMM、重排序、安全发布这些概念特别有帮助。4. 别再混淆Java volatile 与单片机 C 语言 volatile 的差异4.1 单片机里的 volatile 解决什么问题搜索热词里出现了“单片机 volatile 变量”这个必须单独说一下因为这是国内开发者最容易被绕晕的地方。很多人在学校和嵌入式项目里先学了 C 语言的 volatile后来学 Java看到 volatile 以为是一回事结果理解全歪了。C 语言以单片机开发为例里的 volatile 语义非常朴素告诉编译器这个变量的值可能在当前代码流之外被改变不要把它优化到寄存器里缓存每次使用都必须从内存地址重新读取。典型的场景是操作寄存器。比如单片机上某个 GPIO 输出寄存器的地址是固定内存地址程序里写PORTB 0xFF对应的物理引脚就输出高电平但如果你在 main 循环里反复判断一个外部中断修改的 flag不写 volatile编译器可能认为这个变量在循环里没被修改于是把第一次读取的值存在寄存器里后面的判断都用寄存器里的旧值。外部中断明明改了 flag程序却永远看不到。还有延时函数里的空循环volatile unsigned int i; for (i 0; i 100000; i);没有 volatile 的话编译器觉得这个循环没有副作用直接优化成空操作延时效果消失。有了 volatile编译器才乖乖地每次读、每次写循环才真正消耗时间。从本质上讲C 的 volatile 解决的核心问题是防止编译器做过度优化保证对内存地址的访问次数符合源码写法。它不涉及 CPU 缓存一致性、不涉及指令重排序语义、不涉及多线程间复杂的可见性模型。在单片机上很多场景根本没开多核缓存volatile 就是给编译器看的“你别乱动”标记。4.2 两种 volatile 的语义差异对照表为了让你看得更清楚我把 Java 和单片机 C 里的 volatile 放在一起对照对比维度Java volatile单片机 C volatile核心目的保证多线程间的可见性和禁止特定重排序防止编译器对内存访问做优化保证每次访问真实内存是否涉及内存模型是JMM 定义了一整套 happens-before 和重排序规则基本不涉及。C 标准只规定了“访问不可被消除”跨线程可见性属于其他机制如原子操作、内存屏障是否保证原子性不保证复合操作原子性不保证对一个非原子类型的变量读写也不保证原子典型场景DCL、状态标志位、缓存引用发布寄存器访问、中断服务程序共享变量、延时空循环编译器层面影响编译器优化受限同时 JIT 会生成内存屏障指令编译器优化受限通常不生成特殊指令交给 CPU 硬件保证内存访问一致性跨线程可见性volatile 写 happens-before 后续 volatile 读标准本身没有这个保证单片机上往往依赖单核特性或额外屏蔽中断等手段看到这里你可能会问那我在单片机里写多线程比如 RTOS 任务间通信用 volatile 行不行答案是不充分。RTOS 里两个任务共享一个变量A 任务写入B 任务读取除了加 volatile 防止编译器优化外通常还需要考虑原子性、临界区保护、以及任务调度时机。如果数据是多字节的更要注意因为 8 位单片机上读写 16 位、32 位变量本身可能就不是原子操作中途可能被任务切换打断。这些都不是 volatile 能管的。再说回 Java。Java 的 volatile 其实融合了多重语义它既有类似 C 里“每次从内存读、不缓存寄存器”的效果还包括了内存屏障、禁止重排序、happens-before 规则等更上层的东西。可以粗暴理解为Java 的 volatile 是“C 的 volatile 内存屏障 跨线程内存可见性规范”的集合体。所以不要拿单片机里的经验去理解 Java也不要反过来。写代码时的口诀我总结成两句话单片机里看到 volatile想编译器怕它乱优化保证访问真实地址。Java 里看到 volatile想线程怕它乱重排保证跨线程可见正确。如果把这两句话刻在脑子里以后看到任何 volatile 相关代码思路都不会偏。5. DCL 的常见问题与排查实战5.1 DCL 真出问题时什么样DCL 有问题时表现通常很隐蔽。因为它是概率性出错而且出错的路径往往跟 JIT 优化程度、CPU 架构强相关。常见症状包括单例对象内部的状态字段偶尔是默认值比如 Boolean 是 false、int 是 0偶发抛出 NullPointerException但调用栈完全看不出问题因为“null 的是字段不是引用”在高并发压测时出现低并发环境下运行几个月都不报错代码在开发机x86各种验证都通过上线到 ARM 服务器或安卓端开始偶发故障。我见过一个真实案例某个团队在网关程序里用 DCL 存一个路由配置对象对象里有几十个字段。压测时大概每百万次请求会出现一两次拿到半初始化对象导致路由表缺项。排查了整整两周最后把所有 map 操作都怀疑了一遍才发现是单例发布的问题。这类问题最麻烦的地方在于它不会给你一个明显的“并发修改”报错而是表现为脏数据、空指针、程序状态错乱。数据竞争本身就是未定义行为你越往后追越难定位。5.2 我的实操排查方法如果怀疑代码里有 DCL 或类似发布问题我一般按下面这几步走第一步审查代码里的安全发布方式。把所有静态单例字段、static Map、static Set 之类的东西全部过一遍问自己三个问题这个字段在锁内写在锁外读吗这个引用发布后对象内部字段会继续被修改吗发布这个引用的线程和读取这个引用的线程之间有没有一条明确的 happens-before 边只要有一个回答是“不确定”就当作有问题。第二步用并发压力测试工具暴露问题。Java 并发社区常用的工具是 JCStressOpenJDK 出的并发压测框架。它可以针对特定代码构造多线程压测场景自动检测违反 JMM 的后果。给 DCL 写一个简单的 stateJCStressTest Outcome(id 1, 42, expect ACCEPTABLE, desc 正常拿到完整对象) Outcome(id 1, 0, expect FORBIDDEN, desc 拿到半初始化对象) public class DCLTest { static class Box { int value; Box() { value 42; } } static volatile Box vbox; static Box plain; public void actor1() { plain new Box(); } public void actor2() { vbox plain; } }这种工具的价值在于它能系统性地制造大量并发交叉比你自己写多线程循环更容易暴露问题。第三步如果项目不方便引入压测框架就靠代码审查配合 JMM 规则推断。把关键路径画成时间线写线程做什么、读线程做什么、两者通过什么机制建立 happens-before。如果找不到这条边直接判定为有风险别指望“应该没问题”。5.3 常见问题速查表问题原因解决方案DCL 不加 volatile偶发拿到半初始化对象构造函数与引用赋值被重排序锁外读无同步instance 加 volatile单例内部字段是默认值发布对象时内部写入对读线程不可见使用 volatile 发布或改用安全发布方式第一次检查总是读到 null性能差volatile 缓存行竞争、或对象每次都被销毁重建检查是否有代码修改了 instance统计真实调用路径用了 volatile 还是复现问题可能有别的地方把 instance 重新赋值了或者对象内部字段本身在发布后被修改审计所有对 instance 的赋值点发布后保持对象不可变用静态内部类或枚举后反而遇到初始化顺序问题某些框架通过反射或自定义类加载器破坏单例特性优先采用枚举如有特殊类加载场景显式管理生命周期单片机里用 volatile 做任务间共享变量volatile 不保证原子性和临界区保护配合临界区、原子操作或 RTOS 同步原语还有一个细节值得单独提醒加了 volatile 之后DCL 的性能优势要建立在“大多数情况下 instance 非 null”这个前提上。如果代码里有人误把 instance 在运行时重新赋 null比如某些“重置单例”的操作那每次调用都走锁性能立刻退化。更为关键的是一旦发布后修改单例引用volatile 语义允许新的引用被看到但旧对象的状态变化不会自动同步。所以我的建议是单例的引用字段除了初始化时赋值任何地方都不要去改它。最后一件事看到 DCL 和 volatile 这个组合没必要过度崇拜也没有必要觉得它已经过时。它最大的价值是作为一道非常好的思维训练题一个看起来只有几行代码的单例实现背后站着的是 JMM、重排序、内存屏障、安全发布这一整片冰山。我自己在实际开发中的体会是真正让我进步的不是背下“DCL 要加 volatile”这条结论而是某一次在 ARM 服务器上跑压测亲眼看到没有 volatile 的版本暴露出半初始化对象的那一刻。从此以后我再也不会说“并发代码跑着没问题就等于正确”。如果你也想验证自己是否真的理解了这个机制可以试着不看答案写一个 DCL、再写一个非 volatile 版本然后给同事讲清楚两者在 JMM 层面差在哪里。能讲明白才算真正会用。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

电控秋招突围:用开源项目打造真实工程经历 2026/9/28 19:18:21

电控秋招突围:用开源项目打造真实工程经历

1. 为什么电控秋招简历总被“已读不回”?真相不是你不够努力,而是工程经历缺了这层“肉”秋招季一到,电控方向的应届生朋友圈就开始刷屏:“投了37份,0面试”“HR已读不回,连拒信都不给”“项目写了一整页&a…

阅读更多 →
STM32+FPGA工业存储方案:EEPROM、NOR Flash、SD卡分级设计 2026/9/28 19:18:09

STM32+FPGA工业存储方案:EEPROM、NOR Flash、SD卡分级设计

STM32FPGA 这套组合,我前前后后在几个工业控制器项目里用过,每次做到数据存储这一环,都会被人问“直接拿个 Flash 芯片存不就完了吗,搞这么复杂干嘛”。真到现场跑起来你就知道,数据放哪个介质、谁来写、怎么写、掉电瞬…

阅读更多 →
STM32C5+IIS3DWB:IIC接口读取高频振动数据的工程实战指南 2026/9/28 19:18:08

STM32C5+IIS3DWB:IIC接口读取高频振动数据的工程实战指南

最近在做一套旋转设备状态监测的方案,主控选了STM32C5,传感器用了ST的IIS3DWB,一个Cortex-M33的新平台加一颗宽带振动计,双新组合确实折腾了不少时间。这篇是系列的第二篇,主要把IIC读取IIS3DWB震动数据的完整过程聊透…

阅读更多 →
MCU外挂PSRAM扩展内存实战:从硬件连线到性能调优的完整指南 2026/9/28 19:18:08

MCU外挂PSRAM扩展内存实战:从硬件连线到性能调优的完整指南

搞过带界面嵌入式产品的人,十有八九都遇到过同一个问题:算力够了,Flash 也够,唯独 RAM 不够。明明只是加个菜单、刷个动画,MCU 里那块 SRAM 就捉襟见肘。前阵子做项目,手里正好有一颗 APS6404L-SQH-SN&…

阅读更多 →
【研发类-开发方法论Skills】cicd-automation-workflow-automate 技能 2026/9/28 19:18:08

【研发类-开发方法论Skills】cicd-automation-workflow-automate 技能

你是一个工作流自动化专家,专注于创建高效的CI/CD管道、GitHub Actions工作流和自动化开发流程。设计和实现减少手动工作、提高一致性并加速交付的自动化,同时保持质量和安全。 技能概述 cicd-automation-workflow-automate 技能是一个工作流自动化专家…

阅读更多 →
Unity Shader Graph 200+节点深度拆解与移动端性能优化实战 2026/9/28 19:18:00

Unity Shader Graph 200+节点深度拆解与移动端性能优化实战

1. 为什么我要把 Shader Graph 的节点一个个拆开讲Unity 的 Shader Graph 从 2018 版本进入正式管线到现在,已经成了绝大多数中小团队做效果的首选工具。原因很直接:可视化连线比手写 HLSL 快得多,美术和 TA 之间的沟通成本也低。但用久了你会…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉