FEATURED · 精选文章

synchronized到底锁住了什么?深入解析锁对象与锁升级机制

发布时间 / 2026/9/9 7:56:29
来源 / 创域科博编辑部
栏目 / 资讯中心
synchronized到底锁住了什么?深入解析锁对象与锁升级机制 1. synchronized 到底锁住了什么从一次滑铁卢面试说起有位读者朋友去年面试某大厂被问到这么一个问题“synchronized 修饰在方法上锁的是当前对象修饰在静态方法上锁的是 Class 对象。那如果两个线程一个调用同步实例方法、一个调用同步静态方法它们会互斥吗”他当时脱口而出“会吧都是 synchronized。”结果当然是挂了。面完他回来问我我跟他解释完他第一反应是“原来 synchronized 锁的不是方法也不是代码块而是对象。我之前一直以为锁的是代码。”这个认知误区其实非常普遍。很多 Java 开发者从入门开始就天天写 synchronized知道它能保证线程安全但真被问到“锁的到底是什么”“为什么能保证线程安全”“锁升级的细节是什么”时往往就只能答个大概。这篇文章不是教科书式的复述而是把我这些年在使用、排查、优化 synchronized 过程中积累的理解包括踩过的坑、验证过的手段系统地整理一遍。你会搞清楚三件事synchronized 的特性到底怎么理解三种用法背后的锁对象分别是谁以及偏向锁、轻量级锁、重量级锁这套升级机制的来龙去脉。无论是准备面试还是写多线程代码时想弄明白“为什么这样写安全、那样写不安全”这篇文章应该都能帮到你。我会尽量用大白话拆解但涉及底层的地方不会回避细节因为那些细节才是区分“会用”和“懂”的分水岭。2. 三条核心特性原子性、可见性、有序性外加一个可重入2.1 原子性锁住的是“代码片段”的完整执行synchronized 的第一条特性是原子性。通俗点说被 synchronized 包裹的代码段在同一时刻只能有一个线程执行其他线程想进来就得在门外等着直到持有锁的线程执行完毕并释放锁。但这里有一个很容易忽略的点synchronized 保证的原子性是“加了锁的这段代码”的原子性而不是“方法里所有操作”的原子性。举个例子public class Counter { private int count 0; public synchronized void increment() { count; } } public class Counter2 { private int count 0; public void increment() { // 这里的 count 并不是原子操作 synchronized (this) { count; } } }上面的两种写法效果是一样的锁住的都是当前对象保证 count 这段临界区同一时刻只有一个线程执行。但如果你在 increment() 方法里既有 count又有其他不需要同步的操作那 synchronized 只保证临界区内的操作不被并发干扰临界区外的操作该并发还是并发。曾经有人问我“synchronized 是不是能保证方法内的所有语句都原子执行”答案是能保证它们作为一个整体不会被其他持有同一把锁的线程插入执行但如果你在同步代码块里调用了另一个没有加锁的方法那个方法在执行时仍然可能被其他线程并发访问。所以设计临界区的时候一定要把需要保护的操作都放进锁的范围内别只锁一半。2.2 可见性锁的释放与获取之间存在一条内存屏障可见性这个概念很多初学者理解得比较浅就觉得“多个线程同时访问一个变量用 synchronized 锁住就不会出问题”。这里面的机制值得认真说清楚。Java 内存模型JMM规定每个线程有自己的工作内存相当于 CPU 缓存线程对共享变量的读写先操作工作内存再同步到主内存。如果一个线程修改了变量但还没来得及写回主内存另一个线程读到的就是旧值这就出现了可见性问题。synchronized 之所以能解决可见性是因为它内部有一条隐含的规则当一个线程释放锁时会把工作内存中的共享变量刷新到主内存当另一个线程获取同一把锁时会重新从主内存加载共享变量到工作内存。换成大白话就是线程 A 在 synchronized 块里改了 count 的值退出块时这个值被强制刷回主内存线程 B 进入同一个锁保护的块时强制从主内存重新读一遍 count读到的就是 A 改过的最新值。这就像两个同事共用一个保险柜。A 打开保险柜存入新文件锁上柜门B 打开同一个保险柜看到的就是 A 存入的最新文件不会看到旧版本。但如果 B 开的是另一个保险柜也就是不同的锁对象那他看到的可能就是旧文件这就是为什么锁对象必须是同一个对象才能保证互斥和可见。2.3 有序性synchronized 如何对抗指令重排序指令重排序是编译器和 CPU 为了优化性能而做的调整单线程下没问题多线程下就可能出岔子。JMM 规定了一套 happens-before 规则其中与 synchronized 相关的核心就是一个锁的释放操作 happens-before 于后续对这个锁的获取操作。这个规则意味着线程 A 在释放锁之前做的所有写操作对于随后获取同一把锁的线程 B 来说都是可见的而且在 B 看来这些操作发生的顺序就是 A 代码里的顺序。即使编译器和 CPU 在 A 内部做了重排序在锁边界上也会插入内存屏障禁止相关指令跨越锁的边界重排序。很多人知道 volatile 能禁止重排序却不知道 synchronized 也有类似的效果。区别在于volatile 是对单个变量的读写保证有序性synchronized 是对整个临界区的操作在锁的边界上生效。2.4 可重入性同一个线程可以重复拿到同一把锁可重入性是我在面试中经常考查的一个点也是写代码时容易忽略的一个特性。简单说同一个线程在已经持有某把锁的情况下可以再次进入所有由该锁保护的代码块不会把自己锁死。public class ReentrantDemo { public synchronized void outer() { System.out.println(outer); inner(); // 这里能进入吗 } public synchronized void inner() { System.out.println(inner); } }上面的代码完全没问题outer() 和 inner() 都是 synchronized 方法锁对象都是 this。线程执行 outer() 时获取了 this 的锁然后调用 inner()发现需要 this 的锁但自己已经持有了于是直接放行。这就是可重入。可重入的底层实现是每个锁对象关联一个计数器和一个持有线程。当持有线程再次获取锁时计数器加一退出同步块时计数器减一减到零才真正释放锁。这个机制保证了递归调用、方法链式调用等场景下不会出现死锁。可重入性的作用在继承场景下更明显。如果父类有一个 synchronized 方法子类重写它并在方法里调用了父类的 synchronized 方法如果锁不可重入这里就会死锁。正是因为可重入这种写法才能正常工作。3. 三种用法剖析实例方法、静态方法、代码块的锁对象分别是谁3.1 同步实例方法锁的是 this不是方法synchronized 修饰实例方法时锁对象是调用这个方法的实例对象也就是 this。来看看这行代码public class Account { private int balance; public synchronized void withdraw(int amount) { if (balance amount) { balance - amount; } } public synchronized int getBalance() { return balance; } }这里两个方法锁的都是同一个 Account 实例。如果线程 A 在调用 withdraw()线程 B 想调用同一个对象上的 getBalance()就必须等 A 释放锁。这保证了 balance 的读写不会交叉执行。但要注意如果是两个不同的 Account 实例它们的锁互不干扰。线程 A 操作 account1.withdraw()线程 B 操作 account2.getBalance()它们可以同时执行因为锁对象不同。这在业务上通常是合理的不同账户的余额本来就该独立操作但如果你的需求是所有账户共享某个资源比如一个全局的总余额那锁 this 就不够了得用 Class 锁或者一个共享的静态锁对象。3.2 同步静态方法锁的是 Class 对象全局唯一synchronized 修饰静态方法时锁对象是这个类对应的 Class 对象。Class 对象在 JVM 中每个类只有一份所以所有通过该类的静态同步方法进行的并发控制是全局性的。public class ConfigManager { private static MapString, String config new HashMap(); public static synchronized void updateConfig(String key, String value) { config.put(key, value); } public static synchronized String getConfig(String key) { return config.get(key); } }无论创建多少个 ConfigManager 实例updateConfig() 和 getConfig() 用的锁都是 ConfigManager.class所以它们之间是互斥的。这里有个很微妙的点同一个类的同步实例方法锁 this和同步静态方法锁 Class 对象之间并不互斥。回到文章开头那个面试题一个线程执行实例同步方法另一个线程执行静态同步方法它们会不会互斥答案是不会。因为一把锁是实例对象另一把锁是 Class 对象锁根本不同大门都不是同一扇自然谈不上互斥。这是一个容易掉进去的坑。比如你在一个 Service 类里既有同步实例方法又有同步静态方法都访问同一个共享数据如果误以为它们会互斥那就埋下了并发隐患。3.3 同步代码块锁对象由你指定最灵活的姿势同步代码块能让你精确控制锁对象和锁的范围。锁对象可以是一个实例、一个 Class也可以是一个专门用于同步的常量对象。public class SafeListT { private final ListT list new ArrayList(); private final Object lock new Object(); public void add(T item) { synchronized (lock) { list.add(item); } } public T get(int index) { synchronized (lock) { return list.get(index); } } }用专门的 lock 对象而不是 this好处有两个。第一避免外部代码通过 synchronized(instance) 跟你竞争同一把锁减少不必要的阻塞第二如果你的类里有多组需要分别加锁的共享资源可以用不同的锁对象降低锁的粒度提高并发度。代码块还可以指定锁 Class 对象比如 synchronized (ConfigManager.class)效果和同步静态方法一样。这在你只需要锁某个局部操作、不想把整个方法都用 synchronized 修饰时非常有用。选择锁对象时有一个原则必须遵守所有需要互斥的线程必须使用同一个锁对象。比如两个线程一个用 synchronized(lockA)、一个用 synchronized(lockB)哪怕它们操作的是同一份数据也不会互斥反而会破坏线程安全。3.4 三种用法的实际选择什么场景该用哪一种日常开发中我给的建议是这样方法体很短整个方法都需要保护直接修饰方法最省事代码也简洁方法体较长但只有一小段涉及共享变量用同步代码块缩小锁范围提高并发度多个方法共享同一个资源但调用源不同实例/静态统一用同步代码块指定同一个锁对象避免锁对象不一致需要用多个锁分别保护多组资源使用显式锁对象每组资源配一把锁。实际经验是锁的粒度越小性能越好锁的粒度越精确代码越安全。不要嫌多写几行代码代码块 私有锁对象是综合最稳的写法。4. 锁机制底层偏向锁到重量级锁的四级升级之路4.1 从对象头说起Mark Word 里藏着的秘密synchronized 的锁信息存放在 Java 对象头的 Mark Word 里。在 64 位 JVM 中对象头由 Mark Word8 字节和 Klass Pointer8 字节默认开启压缩指针时是 4 字节组成。Mark Word 的默认存储结构里包含 hashCode、分代年龄、偏向锁标志位biased_lock和锁标志位lock bits。锁标志位只有 2 个比特却能表达四种状态01 表示无锁或偏向锁用 biased_lock 位区分00 表示轻量级锁10 表示重量级锁11 表示 GC 标记。这很关键Java 中的锁不是一上来就是重量级的而是从无锁状态开始根据竞争情况逐步升级。这个设计理念很朴素——大多数情况下锁往往不会被多个线程同时竞争没必要一开始就上最重的锁。4.2 偏向锁只有一把钥匙谁拿到就是谁的偏向锁的设计动机是很多 synchronized 代码在绝大多数情况下只被一个线程执行不存在竞争。那就不如让这个线程“偏向”于这把锁下次再来时不需要任何 CAS 操作直接进入。当一个线程第一次访问某同步块并获取锁时JVM 会在对象头的 Mark Word 里记录这个线程的 IDThread ID并设置偏向锁标志位。之后这个线程再次进入同一个同步块时只需检查 Thread ID 是否匹配匹配就直接执行无需任何原子操作性能极高。但这有个前提只有当一个线程反复进出同步块偏向锁才能发挥最大价值。一旦出现第二个线程尝试获取锁偏向锁就开始“不安定”了。偏向锁的撤销是延迟的。如果线程 B 来竞争JVM 先要让持有偏向锁的线程到达一个安全点safe point然后暂停它判断偏向线程是否还活着以及是否还在使用锁。如果偏向线程已经退出同步块就把锁状态重置为无锁状态biased_lock0lock01然后允许竞争如果偏向线程还在执行同步块就升级为轻量级锁让两个线程公平竞争。JVM 提供了几个偏向锁开关便于调试和优化-XX:UseBiasedLocking启用偏向锁JDK 8 默认开启-XX:BiasedLockingStartupDelay0JVM 启动后立即启用偏向锁默认有 4 秒延迟-XX:-UseBiasedLocking禁用偏向锁值得注意的是JDK 15 开始默认禁用了偏向锁JDK 18 之后直接移除了偏向锁。原因很实际偏向锁在竞争存在时撤销成本反而高于收益而且现代偏向锁的优化空间被其他机制如 JIT 优化覆盖了。这提醒我们锁优化策略是跟着 JVM 版本走的不要死记硬背一套。4.3 轻量级锁CAS 抢锁用自旋换阻塞当第二个线程开始竞争同一把锁时偏向锁撤销后锁状态升级为轻量级锁。为什么叫“轻量级”因为它的加锁和解锁不需要操作系统介入不涉及线程阻塞和唤醒而是基于 CASCompare And Swap。获取轻量级锁的过程是这样的线程在栈帧中创建锁记录Lock Record空间用于存放锁对象的 Mark Word 副本线程使用 CAS 尝试将对象头中的 Mark Word 替换为指向锁记录的指针如果替换成功说明获取锁成功记录锁状态为轻量级锁如果替换失败说明有其他线程持有了锁当前线程进入自旋spin不断重试 CAS自旋到一定次数自适应自旋会动态调整次数还拿不到锁就膨胀为重量级锁。这里有个理念要理解轻量级锁适合“线程交替执行同步块、竞争时间很短”的场景。CAS 操作比线程阻塞和唤醒的开销小得多。但如果多个线程同时竞争且持有锁的时间很长自旋就会白白浪费 CPU得不偿失此时就应升级为重量级锁。4.4 重量级锁依赖操作系统进入阻塞队列重量级锁是锁升级的最终形态。获取不到锁的线程会进入阻塞状态不占用 CPU等待操作系统唤醒。这个机制依赖底层操作系统的互斥量Mutex Lock来实现。这里涉及用户态和内核态的切换。阻塞和唤醒一个线程需要操作系统介入一次切换的成本可能在几十微秒到上百微秒比 CAS 自旋重试高出几个数量级。所以重量级锁的代价高但在高竞争场景下把 CPU 让给真正在干活的线程是合理的选择。锁升级到重量级锁后不会再回退到轻量级锁或偏向锁。这是一个单行道。JVM 里用 inflate 方法膨胀锁一旦升级整个对象头的 Mark Word 就记录为指向 Monitor 对象管程的指针通过 Monitor 来管理阻塞队列。Moniter 对象里维护了几个关键数据结构Owner当前持有锁的线程EntryList等待获取锁的线程队列WaitSet调用了 wait() 方法的线程队列。这一套结构对应着 synchronized wait/notify 的协作模型也是理解对象监视器Monitor的关键。4.5 锁升级全过程一次典型的线程竞争模拟为了把这四级状态串起来我模拟一个典型过程。假设有一个计数器对象 counter状态初始为无锁。线程 T1 第一次进入 synchronized(counter) 代码块。此时 counter 的锁状态为无锁JVM 将 T1 的线程 ID 写入 Mark Word锁状态变为偏向锁T1 每次进入都畅通无阻。线程 T2 出现了它也尝试进入同一个同步块。偏向锁发现 Thread ID 对不上撤销偏向锁升级为轻量级锁。T1 和 T2 用 CAS 开始抢锁抢不到的线程自旋等待重试。如果 T1 持有锁的时间很长T2 自旋多次仍拿不到锁自适应自旋机制认为竞争激烈于是锁膨胀为重量级锁。T2 进入阻塞队列等待操作系统唤醒。此后 T1 释放锁时会从 EntryList 中唤醒一个等待线程。整个过程可以概括为无锁 → 偏向锁 → 轻量级锁自旋→ 重量级锁阻塞。锁升级是为了在不同竞争强度下选择最合适的锁策略竞争少用偏向竞争短用自旋竞争激烈用阻塞尽量做到性能和资源占用的平衡。5. 反直觉场景与真实排错锁对象选错、锁粒度失控、锁死循环5.1 String 作为锁对象一个能让你排查到怀疑人生的坑来一个我自己实际踩过的坑。在一段业务代码里我想以订单号作为锁粒度让同一个订单的请求串行处理不同订单可以并行。代码写出来是这样private void processOrder(String orderId) { synchronized (orderId) { // 处理订单 } }看起来没毛病但一压测就发现不同订单的请求也被阻塞了。查了很久才发现问题出在字符串常量池上。Java 里的字符串字面量和 intern() 出来的字符串会被缓存到常量池。如果两个不同的订单号在代码里都能匹配到同一个常量池字符串或者你的 orderId 恰好是通过.intern()获取的那它们对应的就是同一个 String 对象锁自然就撞上了。更隐蔽的问题还有一个如果 orderId 为 nullsynchronized(null) 会在运行时抛 NullPointerException。别笑我见过生产环境因为这个直接挂掉的。正确做法是用String.intern()也没那么可靠因为常量池管理策略在不同版本 JVM 上有差异。更稳妥的方案是使用ConcurrentHashMap维护一个订单号到锁对象的映射private final ConcurrentHashMapString, Object lockMap new ConcurrentHashMap(); private void processOrder(String orderId) { Object lock lockMap.computeIfAbsent(orderId, k - new Object()); synchronized (lock) { // 处理订单 } }用完后记得清理 lockMap 中的条目否则会有内存泄漏风险。这个方案能精确控制锁粒度而且不会踩常量池的坑。5.2 包装类作为锁对象Integer 的缓存陷阱跟 String 类似的坑还有包装类。比如用 Integer 当锁对象private Integer count 0; public void increment() { synchronized (count) { count; } }这个写法有几层问题。第一Integer 的值在 [-128, 127] 范围内会走缓存也就是说所有值为 1 的 Integer 都是同一个对象锁粒度被放大了。第二count实际上创建了一个新的 Integer 对象原来的锁对象已经变了。你锁住的对象和业务数据对象不是同一个不同线程可能锁着不同的对象进入临界区线程安全完全失控。我之前用一段压测代码验证过两个线程分别对两个不同的 Integer 对象值相同比如都为 1加锁结果它们互斥了。因为这两个 Integer 对象实际是同一个缓存对象。这就是为什么用基本类型包装类当锁对象一定要格外谨慎。用锁对象的基本原则是锁对象必须是稳定的、不会被重新赋值的最好是 private final 的独立对象。5.3 锁粒度失控锁范围太大导致系统吞吐量骤降还有一次性能排查同事报接口响应变慢从 50ms 涨到 2 秒。查代码发现他们在一个方法上加了 synchronized但这个方法内部会调用远程 HTTP 服务耗时要几百毫秒。结果所有请求在 synchronized 门口排队吞吐量直线下降。这类问题的本质是锁范围太大把不需要同步的操作也锁住了。正确做法是把锁的粒度缩小只锁住操作共享资源的代码段。// 错误的写法 public synchronized ListString fetchAndProcess() { // 远程调用耗时 500ms ListString data remoteService.fetch(); // 本地处理只这段需要锁 ListString result process(data); return result; } // 正确的写法 public ListString fetchAndProcess() { ListString data remoteService.fetch(); synchronized (this) { // 只对本地处理加锁 ListString result process(data); return result; } }微服务架构下类似这种大锁问题很常见。远程调用数据库操作、HTTP 调用、Redis 操作都不应该放在 synchronized 块里。锁只保护临界资源不保护 IO 操作这条原则值得贴在工位上。5.4 synchronized 里的死循环锁不释放的现场还原最后一个排错案例。有个定时任务每隔 5 秒启动一个线程去处理一批数据逻辑里用了 synchronized。某天开始任务直接不执行了连日志都没有。导出线程快照发现某个线程一直处于RUNNABLE状态并且持有锁卡在一个 while 循环里出不来。原因是一个异常引发死循环导致同步块永远执行不完锁也就永远不释放。其他等待锁的线程全部阻塞任务队列越积越多。这个案例给我们一个启发synchronized 不像 ReentrantLock 那样支持 tryLock 超时中断它一旦进入只能等执行完或者抛异常退出。如果在同步块里用了可能导致死循环的代码比如 while(!stop)、没有超时的远程调用整个系统的并发能力都会被拖垮。所以使用 synchronized 时临界区内的代码应该尽量简短、确定、无阻塞。如果业务逻辑复杂可能需要考虑用 ReentrantLock 或者并发工具类如 CountDownLatch、Semaphore来实现更灵活的控制。6. 锁优化与替代方案锁消除、锁粗化以及为什么说 synchronized 不该背性能的锅6.1 锁消除JIT 编译器替你识破了一个“多余的锁”JIT 编译器会在运行期分析如果发现一个 synchronized 代码块在代码路径上不可能出现竞争就会直接移除这把锁。这叫锁消除。最常见的就是局部变量场景public void append(String a, String b) { StringBuffer sb new StringBuffer(); sb.append(a); sb.append(b); }StringBuffer 的 append 方法都是 synchronized 的但这里的 sb 是局部变量不可能被其他线程访问。JIT 通过逃逸分析确认 sb 不会逃逸出当前方法于是把所有 append 方法的同步逻辑优化掉等价于用 StringBuilder 执行。这个例子的启发是不要因为性能而拒绝使用 synchronized。在现代 JVM 的优化能力下很多看起来“重”的锁实际执行时可能已经被优化没了。JDK 的-XX:DoEscapeAnalysis和-XX:EliminateLocks默认都是开启的。6.2 锁粗化把连续多次加锁合并成一次与锁消除相对的是锁粗化。当 JIT 检测到同一个线程在短时间内反复对同一个对象加锁、释放锁它会把这些加锁操作合并成一个更大的同步块减少加锁解锁的次数。比如public void concat(String[] parts) { StringBuffer sb new StringBuffer(); for (String part : parts) { sb.append(part); // 每次 append 都加锁解锁 } }如果 parts 数组有 10 个元素这段代码理论上要加锁解锁 10 次。JIT 检测到这是同一个对象上的连续操作就会自动把锁粗化把这 10 次 append 合并到一个锁范围内只加锁解锁一次。这告诉我们写代码时不需要过度纠结“一个方法里是否多次使用同一个锁”JIT 有这个能力帮你优化。当然锁粗化是有条件的它要求这些加锁操作在时间上是连续且紧密的中间没有长耗时的操作。如果你的代码加锁、解锁之间隔了远程调用那 JIT 是不会帮你粗化的。6.3 为什么说 synchronized 不该背性能的锅很多人一谈到 synchronized 就觉得性能差宁可不用。但从 JDK 6 引入锁升级机制后synchronized 的默认表现已经非常好了无竞争时是偏向锁低竞争时是轻量级锁高竞争时才升级为重量级锁。加上 JIT 的锁消除、锁粗化多数场景下 synchronized 的性能已经不输 ReentrantLock。真正让性能变差的不是 synchronized 本身而是它被用错了地方锁了过大的范围、用了不合适的锁对象、在锁内做了 IO 操作。这些本质上是使用问题不是锁机制的问题。所以我一直建议默认情况下优先使用 synchronized而不是一上来就 ReentrantLock。synchronized 代码简洁、自动释放锁、可读性高。只有当需要可中断获取锁、超时获取锁、公平锁、多个条件队列这些 synchronized 无法支持的能力时才考虑 ReentrantLock。别再让 synchronized 背性能的锅了它现在的表现绝大多数场景下都够用。6.4 一个进阶建议理解 happens-before别死记锁规则最后想多说一句。很多人学 synchronized喜欢背“锁能保证原子性、可见性、有序性”这三条规则。但真正遇到并发 bug 时靠背规则往往无法定位问题。更实用的做法是理解 happens-before 关系尤其是锁相关的规则解锁 happens-before 后续的加锁。这条规则能推导出很多结论。比如线程 A 在 synchronized 块里写入的数据只要线程 B 随后进入同一个锁保护的代码块就一定能看到。这个推论不依赖你记住“可见性”这个概念而是从规则里直接推导出来。如果你习惯于用 happens-before 的视角去看并发问题遇到各种并发容器、锁、原子类时会更容易建立起整体认知而不是把每个并发工具都当成一个孤立的 API 去背。这个习惯我是在排查一类很隐蔽的并发 bug 时养成的一个线程改了配置另一个线程立刻去读代码里加了 synchronized但实际上用的是不同的锁对象。如果仅从“锁保证可见性”的角度思考很容易觉得“我加了锁啊”。但用 happens-before 看一下锁对象不同解锁和加锁之间就没有 happens-before 关系问题就清晰了。我自己的经验是遇到并发问题先画出锁对象和线程之间的对应关系再画 happens-before 边基本能快速定位出是锁对象不一致、锁粒度太大还是临界区代码本身有逻辑缺陷。synchronized 本身不算复杂复杂的是对锁模型的理解。把这套机制想透了以后无论是看 JUC 包里的源码还是排查生产环境的线程安全问题思路都会顺很多。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻