当前位置: 首页 > news >正文

【JUC】共享模型之无锁

1. CAS 与 volatile

1.1 问题引入

现在有一个转账接口及其实现类,内部定义了getBalance获取余额的方法和withDraw取款方法,并且定义了demo静态方法用于模拟1000个线程同时进行取款操作,全部线程运行完毕后获取余额,观察是否存在并发安全问题,相关代码实现如下:

/*** 问题的提出:模拟1000个线程取款操作,观察是否线程安全*/
@Slf4j
public class TestProblem {public static void main(String[] args) throws InterruptedException {Account.demo(new AccountImpl(10000));}
}class AccountImpl implements Account {private Integer balance;public AccountImpl(Integer balance) {this.balance = balance;}@Overridepublic Integer getBalance() {return balance;}@Overridepublic void withDraw(int amount) {this.balance -= 10;}
}interface Account {Integer getBalance();void withDraw(int amount);static void demo(Account account) throws InterruptedException {List<Thread> threads = new LinkedList<>();// 创建1000个线程启动模拟取款操作for (int i = 0; i < 1000; i++) {threads.add(new Thread(() -> {account.withDraw(10);}));}for (int i = 0; i < 1000; i++) {Thread t = threads.get(i);t.start();}for (int i = 0; i < 1000; i++) {Thread t = threads.get(i);t.join();}// 获取最终余额,观察现象System.out.println("余额为:" + account.getBalance());}
}

最终代码运行效果如下图所示:

其实也不难分析,多个线程同时对balance共享变量进行读写操作,存在线程安全问题!如果读者有相关经验的话就会想到可以使用关键字synchronized加锁解决,但是本章引入 CAS (Compare And Swap) 机制来解决该问题,基于 CAS 的实现类代码如下:

/*** 使用CAS机制实现线程安全*/
class CasAccountImpl implements Account {private AtomicInteger balance;public CasAccountImpl(Integer balance) {this.balance = new AtomicInteger(balance);}@Overridepublic Integer getBalance() {return balance.get();}@Overridepublic void withDraw(int amount) {while (true) {int expectAccount = balance.get();int changeAccount = expectAccount - amount;if (balance.compareAndSet(expectAccount, changeAccount)) {break;}}}
}

代码运行效果如下图所示:

1.2 CAS 原理分析

在上述解决代码中我们通过 AtomicInteger 原子类实现了线程安全,其内部使用了 CAS 机制,那么接下来我们就要思考 CAS 是如何在无锁的情况下实现线程安全的。

原理分析:在上述代码中仔细观察withDraw方法的实现,内部使用了while循环,compareAndSet(expectAccount, changeAccount)含义是比较内存中 balance 的结果与 expectAccount 是否一致,如果一致就修改成 changeAccount 并且返回 true (然后退出循环);反之如果 balance 的结果与 expectAccount 不一致就不进行修改,并返回 false(继续执行循环),从而保证最后一定执行修改成功并返回,流程图如下图所示:

💡 温馨提示:CAS 底层是一个硬件指令lock cmpxchg,无论是在单核 CPU 还是多核 CPU 都能保证原子性

  • 在多核CPU中,某个核心执行到带 lock 的指令时,就会锁住总线,不让其他核心执行中断调度机制,只有该核执行完毕后才会开启总线,保证多核心下的原子性

1.3 CAS 与 volatile

可以想到,由于 CAS 的过程涉及到多线程间的共享变量读写操作,因此需要搭配volatile关键字保证共享变量的可见性,以防止线程读取工作内存的缓存值而影响结果

CAS 特点总结:CAS 适用于并发线程数较少而 CPU 核心数较多的场景

  • CAS 是一个乐观锁机制,主打的就是一个不断尝试,如果发生了线程冲突就再执行一次
  • synchronized 是一个悲观锁机制,当前线程为了不让其他线程抢占共享变量于是加上锁来保证
  • CAS 是一种无阻塞、无锁并发的编程思想

2. Atomic 原子类

2.1 原子整数

Java 标准库当中提供了一系列 Atomic 相关的原子整数类可供使用,比如 AtomicInteger、AtomicLong、AtomicBoolean 等等,此处以 AtomicInteger 为例介绍相关操作方法

  • getAndIncrement:先获取值再执行自增(类比i++)
  • incrementAndGet:先执行自增再获取值(类比++i)
  • getAndDecrement:先获取值再执行自减(类比i–)
  • decrementAndGet:在执行自减再获取值(类比–i)
  • getAndAdd:在获取值再执行累加
  • addAndGet:在执行累加再获取值
  • getAndUpdate:在获取值再执行操作
  • updateAndGet:先执行操作再获取值
  • getAndAccumulate:先获取值再执行积聚操作
  • accumulateAndGet:先执行积聚操作再获取值

相关演示代码如下:

/*** 测试原子整数* AtomicInteger、AtomicBoolean、AtomicLong* @author ricejson*/
@Slf4j
public class TestAtomicInteger {public static void main(String[] args) {// 1. 创建原子整数AtomicInteger atomicInteger = new AtomicInteger();// 2. 获取并自增(先获取值,然后自增) 获取值:0, 实际值:1log.debug("{}", atomicInteger.getAndIncrement());// 3. 自增并获取(在执行自增,再获取值)获取值:2, 实际值:2log.debug("{}", atomicInteger.incrementAndGet());// 4. 获取并自减(先获取值,然后自减)获取值:2, 实际值:1log.debug("{}", atomicInteger.getAndDecrement());// 5. 自减并获取(先执行自减,再获取值)获取值:0, 实际值:0log.debug("{}", atomicInteger.decrementAndGet());// 6. 获取并加值(先获取值,再加上指定值)获取值:0, 实际值:5log.debug("{}", atomicInteger.getAndAdd(5));// 7. 加值并获取(先加上指定值,再获取值)获取值:10, 实际值:10log.debug("{}", atomicInteger.addAndGet(5));// 8. 获取并操作(先获取值,再进行操作)其中p为当前结果 获取值:10,实际值:8log.debug("{}", atomicInteger.getAndUpdate((p) -> p-2));// 9. 操作并获取(先进行操作,再获取值)其中p为当前结果 获取值:10,实际值:10log.debug("{}", atomicInteger.updateAndGet((p) -> p+2));// 10. 积聚并获取(先进行积聚操作,再获取值)其中p为当前结果,x为参数值 获取值:20,实际值:20log.debug("{}", atomicInteger.accumulateAndGet(10, (p, x) -> p+x));// 11. 获取并积聚(先获取值,再进行积聚操作)其中p为当前结果,x为参数值 获取值:20,实际值:30log.debug("{}", atomicInteger.getAndAccumulate(10, (p, x) -> p+x));}
}

代码执行结果如下:

2.2 原子引用

2.2.1 AtomicReference

首先思考为什么要引入原子引用类型,这是因为像 AtomicInteger 只能保证 Integer 类型的原子,但是如果需要保护的对象是一个自定义的引用类型或者非包装引用类型,就需要借助原子引用(AtomicReference)了,比如我们将原先的转账代码中的 balance 修改为 BigDecimal 类型,就需要使用 AtomicReference 来保证线程安全

/*** 测试原子引用操作* @author ricejson*/
@Slf4j
public class TestAtomicReference {public static void main(String[] args) throws InterruptedException {BigDecimalAccount.demo(new BigDecimalAccountImpl(new BigDecimal(10000)));BigDecimalAccount.demo(new CasBigDecimalAccountImpl(new BigDecimal(10000)));}
}class CasBigDecimalAccountImpl implements BigDecimalAccount {private AtomicReference<BigDecimal> balance;public CasBigDecimalAccountImpl(BigDecimal balance) {this.balance = new AtomicReference<>(balance);}@Overridepublic BigDecimal getBalance() {return balance.get();}@Overridepublic void withDraw(BigDecimal amount) {while (true) {// 期望值BigDecimal expect = balance.get();// 修改值BigDecimal target = expect.subtract(amount);if (balance.compareAndSet(expect, target)) {break;}}}
}class BigDecimalAccountImpl implements BigDecimalAccount {private BigDecimal balance;public BigDecimalAccountImpl(BigDecimal balance) {this.balance = balance;}@Overridepublic BigDecimal getBalance() {return balance;}@Overridepublic void withDraw(BigDecimal amount) {this.balance = this.balance.subtract(amount);}
}interface BigDecimalAccount {BigDecimal getBalance();void withDraw(BigDecimal amount);static void demo(BigDecimalAccount account) throws InterruptedException {List<Thread> threads = new LinkedList<>();for (int i = 0; i < 1000; i++) {threads.add(new Thread(() -> {account.withDraw(new BigDecimal(10));}));}threads.forEach(Thread::start);for (int i = 0; i < 1000; i++) {threads.get(i).join();}// 汇总结果System.out.println("余额:" + account.getBalance());}
}
2.2.2 ABA 问题

下面我们来演示一下经典的 ABA 问题,测试代码如下:

/*** 测试AtomicReference的ABA问题* @author ricejson*/
@Slf4j
public class TestABA {public static void main(String[] args) throws InterruptedException {AtomicReference<String> ref = new AtomicReference<>("A");String prev = ref.get();Thread t1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);// 修改成Blog.debug("change A -> B:{}", ref.compareAndSet("A", "B"));} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t1.join();Thread t2 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);// 修改成Alog.debug("change B -> A:{}", ref.compareAndSet("B", "A"));} catch (InterruptedException e) {e.printStackTrace();}});t2.start();t2.join();TimeUnit.SECONDS.sleep(3);log.debug("change A -> C:{}", ref.compareAndSet(prev, "C"));}
}

运行效果如下:

问题分析:我们在主线程之中希望将原先的 A 修改为 C,但是在这之前,又有两个线程分别执行了 A -> B、B -> A 的过程,因此这里的 A 已经不再是原先的 A 了,但是站在主线程的角度无法区分,这实际上是存在问题的!例如原先的转账逻辑是账户是期望的500元才会转账,然后用户多按了一下转账按钮导致触发另一个线程完成转账500,然后有其他用户给该账户转账了500元,此时账户为期望的500,导致又触发了一次转账逻辑,造成资损

2.2.3 AtomicStampedReference

为了解决上述 ABA 问题,于是Java标准库又引入了 AtomicStampedReference 类,该类解决 ABA 问题的核心就在于多引入了“版本号”的机制,即每个线程操作完都需要同步让版本号(一个整数)执行自增操作,此时线程不仅需要对比内部变量的结果还需要对比版本号有没有被修改,下面我们就来使用 AtomicStampedReference 解决上述 ABA 问题,相关代码如下:

/*** 测试AtomicStampedReference* @author ricejson*/
@Slf4j
public class TestAtomicStampedReference {public static void main(String[] args) throws InterruptedException {AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);int stamp = ref.getStamp();Thread t1 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);log.debug("change A -> B:{}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp()+1));} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t1.join();Thread t2 = new Thread(() -> {try {TimeUnit.SECONDS.sleep(1);log.debug("change B -> A:{}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp()+1));} catch (InterruptedException e) {e.printStackTrace();}});t2.start();t2.join();TimeUnit.SECONDS.sleep(3);log.debug("change A -> C:{}", ref.compareAndSet(ref.getReference(), "C", stamp, ref.getStamp()+1));}
}

代码运行结果如下:

2.2.4 AtomicMarkableReference

问题分析:但是有时候我们只需要判断保护变量有没有被其他线程修改过即可,而不需要明确知道该保护变量具体被修改了几次,这个时候就需要借助 AtomicMarkableReference 类了,该类对应的 stamp 实际上为 boolean 变量,我们可以通过一个案例来演示:现在有一个 GarbageBag 类型的垃圾袋变量,主线程(主人)运行逻辑为:如果垃圾袋垃圾满了就去倒垃圾(替换为新的BarbageBag),此时保姆线程运行逻辑为 CAS 操作如果是原先满的垃圾袋就替换为空的,后续主线程发现垃圾袋已经不是满的了就不进行任何操作,测试代码如下:

/*** 测试AtomicMarkableReference* @author ricejson*/
@Slf4j
public class TestAtomicMarkableReference {public static void main(String[] args) throws InterruptedException {log.debug("主线程启动...");GarbageBag garbageBag = new GarbageBag("满垃圾袋");AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<GarbageBag>(garbageBag, true);GarbageBag prev = ref.getReference();new Thread(() -> {log.debug("保姆线程启动...");while (true) {GarbageBag bag = ref.getReference();bag.setDesc("空垃圾袋");if (ref.compareAndSet(bag, bag, true, false)) {break;}}log.debug(ref.getReference().toString());}).start();TimeUnit.SECONDS.sleep(1);log.debug("主线程想换个垃圾袋");log.debug("垃圾袋有没有换成功: {}", ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false));}
}class GarbageBag {private String desc;public GarbageBag(String desc) {this.desc = desc;}public void setDesc(String desc) {this.desc = desc;}@Overridepublic String toString() {return "GarbageBag{" +"desc='" + desc + '\'' +'}';}
}

代码运行效果如下图所示:

2.3 原子数组

原子数组主要用于对多个原子对象的保护,常见的原子数组类有:

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

现在模拟有10个线程分别对数组当中的每个元素执行自增10000次,这时就需要借助 AtomicIntegerArray ,相关演示代码如下:

/*** 测试原子数组* AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray*/
@Slf4j
public class TestAtomicIntegerArray {public static void main(String[] args) throws InterruptedException {// 线程不安全demo(() -> new int[10],(array) -> array.length,(array, idx) -> array[idx]++,(array) -> log.debug(Arrays.toString(array)));// 线程安全demo(() -> new AtomicIntegerArray(10),(array) -> array.length(),(array, idx) -> array.getAndIncrement(idx),(array) -> log.debug(array.toString()));}public static <T> void demo(Supplier<T> supplier,Function<T, Integer> lenFunc,BiConsumer<T, Integer> putConsumer,Consumer<T> printConsumer) throws InterruptedException {T array = supplier.get();int len = lenFunc.apply(array);List<Thread> threads = new LinkedList<>();for (int i = 0; i < len; i++) {Thread t = new Thread(() -> {// 每个线程操作10000次for (int j = 0; j < 10000; j++) {putConsumer.accept(array, j % len);}});t.start();threads.add(t);}for (Thread thread : threads) {thread.join();}// 打印结果printConsumer.accept(array);}
}

代码运行效果如下:

2.4 字段更新器

Java标准库还提供了一种工具,可以在不使用 AtomicInteger 等原子类的情况下对共享变量进行原子化保护,比如使用 AtomicIntegerFieldUpdater,可以保护某个类的 Integer 类型的字段,测试代码如下:

/*** 测试原子更新器* AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater* @author ricejson*/
@Slf4j
public class TestAtomicIntegerFieldUpdater {private volatile int field = 0;public static void main(String[] args) {AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(TestAtomicIntegerFieldUpdater.class, "field");TestAtomicIntegerFieldUpdater test = new TestAtomicIntegerFieldUpdater();// 更新成功log.debug("是否更新成功: {}", updater.compareAndSet(test, 0, 10));log.debug("{}", test.field);// 更新成功log.debug("是否更新成功: {}", updater.compareAndSet(test, 10, 20));log.debug("{}", test.field);// 更新失败log.debug("是否更新成功: {}", updater.compareAndSet(test, 10, 30));log.debug("{}", test.field);}
}

代码运行结果如下:

2.5 原子累加器

2.5.1 LongAdder 效果演示

Java 标准库还提供了 LongAdder 原子累加器的组件用于替换频繁的 AtomicLong 的 add 操作,性能对比代码如下:

/*** 测试原子累加器* @author ricejson*/
@Slf4j
public class TestLongAdder {public static void main(String[] args) throws InterruptedException {// 测试AtomicLongdemo(() -> new AtomicLong(), (adder) -> adder.getAndIncrement());// 测试LongAdderdemo(() -> new LongAdder(), (adder) -> adder.increment());}public static <T> void demo(Supplier<T> supplier,Consumer<T> consumer) throws InterruptedException {T adder = supplier.get();long startTime = System.nanoTime();// 四个线程每个累加500000List<Thread> threads = new LinkedList<>();for (int i = 0; i < 4; i++) {Thread thread = new Thread(() -> {for (int j = 0; j < 500000; j++) {consumer.accept(adder);}});thread.start();threads.add(thread);}for (Thread thread : threads) {thread.join();}long endTime = System.nanoTime();// 统计运行时间log.debug("运行时间: {}", endTime - startTime);// 统计运行结果log.debug("最终结果: {}", adder.toString());}
}

代码运行效果如下:

可以发现 LongAdder 组件运行速率提升了两倍以上!

2.5.2 LongAdder 源码分析

接下来我们通过查看源码的方式分析 LongAdder 高性能的原因:

如上图所示,LongAdder内部存在三个核心组件:

  • cells:累加单元数组(多线程分配到不同的累加单元进行累加,最后汇总结果,减少竞争冲突)
  • base:基准累加单元,如果 cells 没有创建就在 base 单元进行累加
  • cellsBusy:类似于CAS锁,保证cells的创建与扩容过程原子化

现在我们就来分析increment方法内部是如何操作的,increment() 方法内部调用 add(1L),因此核心逻辑在add方法内部:

上述代码总结如下:

  1. 如果 cells 数组还没有创建,就尝试将累加值累加到 base 单元上,累加失败就进入longAccumulate方法;累加成功直接退出
  2. 如果 cells 数组已经创建出来,但是线程对应的单元没有创建,就进入longAccumulate 方法
  3. 如果 cells 数组已经创建出来,并且线程对应单元也存在,此时将累加值累加到对应 cell 单元上,累加失败就进入longAccumulate方法,累加成功直接退出

接下来核心就是 longAccumulate 方法的具体实现:可以发现核心逻辑都在 for(;;)循环内部

  1. 先来看第二个 if 块,进入条件为 cells 还没有创建出来,并且没有创建、扩容的过程,此时通过casCellsBusy方法尝试使用 cas 的方式对 cellsBusy 变量进行加锁,内部完成 cells 单元的创建,并且在 0, 1 位置处完成某个单元的创建与赋值,最后释放 cellsBusy 变量的锁并退出

  1. 再来看第三个 if 块,说明上述两个条件均不成立,此时尝试给 base 累加单元累加值

  1. 重点关注第一个 if 代码块
    1. case1:cells 数组存在,但是线程对应累加单元还没有创建,此时尝试对 cellsBusy 加锁,然后执行创建对应累加单元,完成累加操作后退出并释放锁

2. case2:cells单元数组已经创建,并且线程对应累加单元已存在,此时尝试往累加单元进行累加操作

3. case3:如果 n >= CPU个数,强制让 collide 为false,进入下一个if分支并设置 collide 为true

4. case4:如果 n <= CPU 核心数,先加锁,然后执行 cells 的扩容操作,再释放锁,重新进入循环尝试分配新的累加单元

5. case5:如果超过 CPU 核数,就执行`h = advanceProbe(h)`重新使用新的算法分配累加单元

3. unsafe

3.1 概述

其实 AtomicInteger 等原子类底层是基于 unsafe 包实现的,unsafe 包提供了更为底层、可操作内存的实现,而unsafe 类型的对象需要借助反射才能够获取,相关代码如下:

/*** 测试通过反射获取Unsafe对象* @author ricejson*/
@Slf4j
public class TestUnsafe {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {// 1. 通过反射获取unsafe对象Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");// 2. 设置私有可允许访问theUnsafe.setAccessible(true);// 3. 获取对象Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 4. 打印结果System.out.println(unsafe);}
}

3.2 基于 unsafe 的 CAS 操作

我们也可以通过 unsafe 包来实现更为底层的 CAS 操作,使用 unsafe 对象的 compareAndSet 操作我们需要实现获取到指定对象的字段偏移量

/*** 测试基于unsafe的CAS操作* @author ricejson*/
@Slf4j
public class TestUnsafeCAS {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {// 1. 获取unsafe字段Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");// 2. 设置字段可访问theUnsafe.setAccessible(true);// 3. 获取对象Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 4. 获取字段偏移量Student student = new Student(18);Field ageField = Student.class.getDeclaredField("age");ageField.setAccessible(true);long ageOffset = unsafe.objectFieldOffset(ageField);// 4. 调用cas操作log.debug("{}", unsafe.compareAndSwapInt(student, ageOffset, 18, 20));// 5. 打印studentlog.debug("{}", student);}
}class Student {private int age;public Student(int age) {this.age = age;}@Overridepublic String toString() {return "Student{" +"age=" + age +'}';}
}

代码运行结果如下图所示:

3.3 自定义 AtomicInteger

学会了基于 unsafe 的 cas 操作,接下来我们就可以自定义模拟实现 AtomicInteger 类了,并使用之前的 Account 测试效果,详细代码如下:

/*** 自定义模拟实现AtomicInteger类* @author ricejson*/
@Slf4j
public class TestMyAtomicInteger implements Account {@Overridepublic Integer getBalance() {return this.value;}@Overridepublic void withDraw(int amount) {while (true) {int expectAccount = this.value;int changeAccount = expectAccount - amount;if (this.compareAndSet(expectAccount, changeAccount)) {break;}}}private Unsafe unsafe;private volatile int value;private long valueOffset;public TestMyAtomicInteger(int value) throws NoSuchFieldException, IllegalAccessException {this.value = value;Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");theUnsafe.setAccessible(true);Unsafe unsafeObj = (Unsafe) theUnsafe.get(null);this.unsafe = unsafeObj;Field valueField = TestMyAtomicInteger.class.getDeclaredField("value");valueField.setAccessible(true);this.valueOffset = unsafe.objectFieldOffset(valueField);}public boolean compareAndSet(int oldValue, int newValue) {return unsafe.compareAndSwapInt(this, this.valueOffset, oldValue, newValue);}public static void main(String[] args) throws NoSuchFieldException, InterruptedException, IllegalAccessException {Account.demo(new TestMyAtomicInteger(10000));}
}

代码运行结果如下图所示:

相关文章:

  • 免费私有化部署! PawSQL社区版,超越EverSQL的企业级SQL优化工具面向个人开发者开放使用了
  • Linux系统添加路由
  • 免费开放试乘体验!苏州金龙自动驾驶巴士即将上线阳澄数谷
  • 产品构建设计的人性密码:从“假需求陷阱”到“人性博弈”,拆解售前翻车现场的底层逻辑
  • JDBC指南
  • git仓库中.git 文件很大,怎么清理掉一部分
  • Git 使用全攻略:从入门到精通
  • buuctf RSA之旅
  • Linux中的DNS的安装与配置
  • 羽毛球订场小程序源码介绍
  • Spring Boot 的高级特性与经典的设计模式应用
  • 排序复习/上(C语言版)
  • C++--内存管理
  • (已解决:基于WSL2技术)Windows11家庭中文版(win11家庭版)如何配置和使用Docker Desktop
  • 新能源充电桩智慧管理系统:未来新能源汽车移动充电服务发展前景怎样?
  • 网络Tips20-007
  • 深入探讨Java中的上下文传递与ThreadLocal的局限性及Scoped Values的兴起
  • Comsol如何确定合适的研究输出时步?
  • 高校快递物流管理系统设计与实现(SpringBoot+MySQL)
  • 网络协议之一根网线就能连接两台电脑?
  • 钱进已任外交部新闻司副司长
  • 著名古人类学家高星获推选为国际史前与原史研究院院士
  • 陈龙带你观察上海生物多样性,纪录片《我的城市邻居》明播出
  • 盲人不能刷脸认证、营业厅拒人工核验,央媒:别让刷脸困住尊严
  • 体坛联播|雷霆抢七淘汰掘金,国米错失意甲登顶良机
  • 浙江一教师被指殴打并威胁小学生,教育局通报涉事人被行拘