iis服务器助手广告广告
返回顶部
首页 > 资讯 > 前端开发 > html >Java的并发锁怎么理解
  • 661
分享到

Java的并发锁怎么理解

2024-04-02 19:04:59 661人浏览 安东尼
摘要

本篇内容主要讲解“Java的并发锁怎么理解”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java的并发锁怎么理解”吧!   Java 中的并发锁大致分为隐式锁

本篇内容主要讲解“Java的并发怎么理解”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java的并发锁怎么理解”吧!

  Java 中的并发锁大致分为隐式锁和显式锁两种。隐式锁就是我们最常使用的 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer) 实现的。还有的地方将 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色。

  我们只需要弄清楚 synchronized 和 AQS 的原理,再去理解并发锁的性质和局限就很简单了。因此这篇文章重点放在原理上,对于使用和特点不会过多涉及。

  概念辨析

  下面是关于锁的一些概念解释,这些都是一些关于锁的性质的描述,并非具体实现。

  悲观锁和乐观锁

  悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized。然而在真实环境中,大部分时候都不会产生冲突。悲观锁会造成很大的浪费。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试)。这种锁不会阻塞其他的线程,也不涉及上下文切换,性能开销小。代表实现是 CAS。

  公平锁和非公平锁

  公平锁是指各个线程在加锁前先检查有无排队的线程,按排队顺序去获得锁。 非公平锁是指线程加锁前不考虑排队问题,直接尝试获取锁,获取不到再去队尾排队。值得注意的是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。

  可重入锁和不可重入锁

  如果一个线程已经获取到了一个锁,那么它可以访问被这个锁锁住的所有代码块。不可重入锁与之相反。

  Synchronized 关键字

  Synchronized 是一种独占锁。在修饰静态方法时,锁的是类对象,如 Object.class。修饰非静态方法时,锁的是对象,即 this。修饰方法块时,锁的是括号里的对象。 每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性。

  类锁与对象锁

  synchronized 修饰静态方法时,锁的是类对象,如 Object.class。修饰非静态方法时,锁的是对象,即 this。 多个线程是可以同时执行同一个synchronized实例方法的,只要它们访问的对象是不同的。

  synchronized 锁住的是对象而非代码,只要访问的是同一个对象的 synchronized 方法,即使是不同的代码,也会被同步顺序访问。

  此外,需要说明的,synchronized方法不能防止非synchronized方法被同时执行,所以,一般在保护变量时,需要在所有访问该变量的方法上加上synchronized。

  实现原理

  synchronized 是基于 Java 对象头和 Monitor 机制来实现的。

  Java 对象头

  一个对象在内存中包含三部分:对象头,实例数据和对齐填充。其中 Java 对象头包含两部分:

  Class Metadata Address (类型指针)。存储类的元数据的指针。虚拟机通过这个指针找到它是哪个类的实例。

  Mark Word(标记字段)。存出一些对象自身运行时的数据。包括哈希码,GC 分代年龄,锁状态标志等。

  Monitor

  Mark Word 有一个字段指向 monitor 对象。monitor 中记录了锁的持有线程,等待的线程队列等信息。前面说的每个对象都有一个锁和一个等待队列,就是在这里实现的。 monitor 对象由 c++ 实现。其中有三个关键字段:

  _owner 记录当前持有锁的线程

  _EntryList 是一个队列,记录所有阻塞等待锁的线程

  _WaitSet 也是一个队列,记录调用 wait() 方法并还未被通知的线程。

  Monitor的操作机制如下:

  多个线程竞争锁时,会先进入 EntryList 队列。竞争成功的线程被标记为 Owner。其他线程继续在此队列中阻塞等待。

  如果 Owner 线程调用 wait() 方法,则其释放对象锁并进入 WaitSet 中等待被唤醒。Owner 被置空,EntryList 中的线程再次竞争锁。

  如果 Owner 线程执行完了,便会释放锁,Owner 被置空,EntryList 中的线程再次竞争锁。

  JVM 对 synchronized 的处理

  上面了解了 monitor 的机制,那虚拟机是如何将 synchronized 和 monitor 关联起来的呢?分两种情况:

  如果同步的是代码块,编译时会直接在同步代码块前加上 monitorenter 指令,代码块后加上 monitorexit 指令。这称为显示同步。

  如果同步的是方法,虚拟机会为方法设置 ACC_SYNCHRONIZED 标志。调用的时候 JVM 根据这个标志判断是否是同步方法。

  JVM 对 synchronized 的优化

  synchronized 是重量级锁,由于消耗太大,虚拟机对其做了一些优化。

  自旋锁与自适应自旋

  在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 jdk1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  锁消除

  虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

  锁粗化

  当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。

  轻量级锁

  对绝大部分的锁来说,在整个同步周期内都不存在竞争。如果没有竞争,轻量级锁可以使用 CAS 操作避免使用互斥量的开销。

  偏向锁

  偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。

  CAS

  操作模型

  CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下:

  unsafe.compareAndSwapint(this, valueOffset, expect, update);

  复制代码

  它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。操作时,先从内存位置读取到值,然后和预期值A比较。如果相等,则将此内存位置的值改为新值 B,返回 true。如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。

  这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。

  重试机制(循环 CAS)

  有很多文章说,CAS 操作失败后会一直重试直到成功,这种说法很不严谨。

  第一,CAS 本身并未实现失败后的处理机制,它只负责返回成功或失败的布尔值,后续由调用者自行处理。只不过我们最常用的处理方式是重试而已。

  第二,这句话很容易理解错,被理解成重新比较并交换。实际上失败的时候,原值已经被修改,如果不更改期望值,再怎么比较都会失败。而新值同样需要修改。

  所以正确的方法是,使用一个死循环进行 CAS 操作,成功了就结束循环返回,失败了就重新从内存读取值和计算新值,再调用 CAS。看下 AtomicInteger 的源码就什么都懂了:

  public final int incrementAndGet () {

  for (;;) {

  int current = get();

  int next = current + 1;

  if (compareAndSet(current, next))

  return next;    }}

  底层实现

  CAS 主要分三步,读取-比较-修改。其中比较是在检测是否有冲突,如果检测到没有冲突后,其他线程还能修改这个值,那么 CAS 还是无法保证正确性。所以最关键的是要保证比较-修改这两步操作的原子性。

  CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢?

  在处理器中,一般有两种方式来实现上述效果:总线锁和缓存锁。在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。而缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。

  CAS 就是通过这种方式实现比较和交换操作的原子性的。 值得注意的是, CAS 只是保证了操作的原子性,并不保证变量的可见性,因此变量需要加上 volatile 关键字。

  ABA 问题

  上面提到,CAS 保证了比较和交换的原子性。但是从读取到开始比较这段期间,其他核心仍然是可以修改这个值的。如果核心将 A 修改为 B,CAS 可以判断出来。但是如果核心将 A 修改为 B 再修改回 A。那么 CAS 会认为这个值并没有被改变,从而继续操作。这是和实际情况不符的。解决方案是加一个版本号。

  可重入锁 ReentrantLock

  ReentrantLock 使用代码实现了和 synchronized 一样的语义,包括可重入,保证内存可见性和解决竞态条件问题等。相比 synchronized,它还有如下好处:

  支持以非阻塞方式获取锁

  可以响应中断

  可以限时

  支持了公平锁和非公平锁

  基本用法如下:

  public class Counter {

  private final Lock lock = new ReentrantLock();

  private volatile int count;    public void incr() {

  lock.lock();

  try {

  count++;

  } finally {

  lock.unlock();

  }

  }

  public int getCount()

  {

  return count;

  }

  }

  ReentrantLock 内部有两个内部类,分别是 FairSync 和 NoFairSync,对应公平锁和非公平锁。他们都继承自 Sync。Sync 又继承自AQS。

  AQS

  AQS 全称 AbstractQueuedSynchronizer。AQS 中有两个重要的成员:

  成员变量 state。用于表示锁现在的状态,用 volatile 修饰,保证内存一致性。同时所用对 state 的操作都是使用 CAS 进行的。state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。

  还有一个双向链表,链表除了头结点外,每一个节点都记录了线程的信息,代表一个等待线程。这是一个 FIFO 的链表。

  下面以 ReentrantLock 非公平锁的代码看看 AQS 的原理。

  请求锁

  请求锁时有三种可能:

  如果没有线程持有锁,则请求成功,当前线程直接获取到锁。

  如果当前线程已经持有锁,则使用 CAS 将 state 值加1,表示自己再次申请了锁,释放锁时减1。这就是可重入性的实现。

  如果由其他线程持有锁,那么将自己添加进等待队列。

  final void lock() {

  if (compareAndSetState(0, 1))

  setExclusiveOwnerThread(Thread.currentThread()); //没有线程持有锁时,直接获取锁,对应情况1

  else

  acquire(1);

  }

  public final void acquire(int arg) {

  if (!tryAcquire(arg) && //在此方法中会判断当前持有线程是否等于自己,对应情况2

  acquireQueued(addWaiter(node.EXCLUSIVE), arg)) //将自己加入队列中,对应情况3

  selfInterrupt();

  }

  创建 Node 节点并加入链表

  如果没竞争到锁,这时候就要进入等待队列。队列是默认有一个 head 节点的,并且不包含线程信息。上面情况3中,addWaiter 会创建一个 Node,并添加到链表的末尾,Node 中持有当前线程的引用。同时还有一个成员变量 waitStatus,表示线程的等待状态,初始值为0。我们还需要关注两个值:

  CANCELLED,值为1,表示取消状态,就是说我不要这个锁了,请你把我移出去。

  SINGAL,值为-1,表示下一个节点正在挂起等待,注意是下一个节点,不是当前节点。

  同时,加到链表末尾的操作使用了 CAS+死循环的模式,很有代表性,拿出来看一看:

  Node node = new Node(mode);

  for (;;) {

  Node oldTail = tail;

  if (oldTail != null) {

  U.putObject(node, Node.PREV, oldTail);

  if (compareAndSetTail(oldTail, node)) {

  oldTail.next = node;

  return node;

  }

  } else {

  initializeSyncQueue();

  }

  }

  可以看到,在死循环里调用了 CAS 的方法。如果多个线程同时调用该方法,那么每次循环都只有一个线程执行成功,其他线程进入下一次循环,重新调用。N个线程就会循环N次。这样就在无锁的模式下实现了并发模型。

  挂起等待

  如果此节点的上一个节点是头部节点,则再次尝试获取锁,获取到了就移除并返回。获取不到就进入下一步;

  判断前一个节点的 waitStatus,如果是 SINGAL,则返回 true,并调用 LockSupport.park() 将线程挂起;

  如果是 CANCELLED,则将前一个节点移除;

  如果是其他值,则将前一个节点的 waitStatus 标记为 SINGAL,进入下一次循环。

  可以看到,一个线程最多有两次机会,还竞争不到就去挂起等待。

  final boolean acquireQueued(final Node node, int arg) {

  try {

  boolean interrupted = false;

  for (;;) {

  final Node p = node.predecessor();

  if (p == head && tryAcquire(arg)) {

  setHead(node);

  p.next = null; // help GC

  return interrupted;

  }

  if (shouldParkAfterFailedAcquire(p, node) &&

  parkAndCheckInterrupt())

  interrupted = true;

  }

  } catch (Throwable t) {

  cancelAcquire(node);

  throw t;

  }

  }

  释放锁

  调用 tryRelease,此方法由子类实现。实现非常简单,如果当前线程是持有锁的线程,就将 state 减1。减完后如果 state 大于0,表示当前线程仍然持有锁,返回 false。如果等于0,表示已经没有线程持有锁,返回 true,进入下一步;

  如果头部节点的 waitStatus 不等于0,则调用LockSupport.unpark()唤醒其下一个节点。头部节点的下一个节点就是等待队列中的第一个线程,这反映了 AQS 先进先出的特点。另外,即使是非公平锁,进入队列之后,还是得按顺序来。

  public final boolean release(int arg) {

  if (tryRelease(arg)) { //将 state 减1

  Node h = head;

  if (h != null && h.waitStatus != 0)

  unparkSuccessor(h);

  return true;

  }

  return false;

  }

  private void unparkSuccessor(Node node) {

  int ws = node.waitStatus;

  if (ws < 0)

  node.compareAndSetWaitStatus(ws, 0);

  Node s = node.next;

  if (s == null || s.waitStatus > 0) {

  s = null;

  for (Node p = tail; p != node && p != null; p = p.prev)

  if (p.waitStatus <= 0)

  s = p;

  }

  if (s != null) //唤醒第一个等待的线程

  LockSupport.unpark(s.thread);

  }

  公平锁如何实现

  上面分析的是非公平锁,那公平锁呢?很简单,在竞争锁之前判断一下等待队列中有没有线程在等待就行了。

  protected final boolean tryAcquire(int acquires) {

  final Thread current = Thread.currentThread();

  int c = getState();

  if (c == 0) {

  if (!hasQueuedPredecessors() && //判断等待队列是否有节点

  compareAndSetState(0, acquires)) {

  setExclusiveOwnerThread(current);

  return true;

  }

  }

  &hellip;&hellip;

  return false;

  }

  可重入读写锁 ReentrantReadWriteLock

  读写锁机制

  理解 ReentrantLock 和 AQS 之后,再来理解读写锁就很简单了。读写锁有一个读锁和一个写锁,分别对应读操作和锁操作。锁的特性如下:

  只有一个线程可以获取到写锁。在获取写锁时,只有没有任何线程持有任何锁才能获取成功;

  如果有线程正持有写锁,其他任何线程都获取不到任何锁;

  没有线程持有写锁时,可以有多个线程获取到读锁。

  上面锁的特点保证了可以并发读取,这大大提高了效率,在实际开发中非常有用。

到此,相信大家对“Java的并发锁怎么理解”有了更深的了解,不妨来实际操作一番吧!这里是编程网网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

--结束END--

本文标题: Java的并发锁怎么理解

本文链接: https://www.lsjlt.com/news/90293.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Java的并发锁怎么理解
    本篇内容主要讲解“Java的并发锁怎么理解”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java的并发锁怎么理解”吧!   Java 中的并发锁大致分为隐式锁...
    99+
    2024-04-02
  • 如何理解Java并发下的乐观锁
    本篇内容主要讲解“如何理解Java并发下的乐观锁”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何理解Java并发下的乐观锁”吧!在聊乐观锁之前,先给大家复习一个概念:原子操作:什么是原子操作呢...
    99+
    2023-06-15
  • Java并发怎么理解
    本篇内容介绍了“Java并发怎么理解”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是并发?用户通常认为计算机在同一时刻可以做多个事情是理...
    99+
    2023-06-02
  • Java并发中ReentrantLock锁怎么用
    这篇文章主要讲解了“Java并发中ReentrantLock锁怎么用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java并发中ReentrantLock锁怎么用”吧!重入锁可以替代关键字 ...
    99+
    2023-06-21
  • java ReentrantLock并发锁使用详解
    目录一、ReentrantLock是什么1-1、ReentrantLock和synchronized区别1-2、ReentrantLock的使用1-2-1、ReentrantLock...
    99+
    2022-11-13
    java ReentrantLock并发锁 java ReentrantLock
  • 掌握 Java 线程池,解锁并发处理的潜力
    在当今复杂的分布式系统中,并发处理对于高效执行任务至关重要。Java 线程池是一种强大的工具,它通过管理和调度线程,帮助开发人员充分利用并发性,从而提高性能和可伸缩性。 线程池概述 线程池是一个线程的集合,这些线程可以根据需要按需创建和销...
    99+
    2024-03-13
    线程池
  • Java锁怎么理解
    本篇内容主要讲解“Java锁怎么理解”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java锁怎么理解”吧!自旋?自旋锁如果此时拿不到锁,它不马上进入阻塞状态,而愿意等待一段时间。如果循环一定的次...
    99+
    2023-06-16
  • 怎么理解Java并发可见性
    本篇内容介绍了“怎么理解Java并发可见性”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!01 可见性的阐述可见性 的定义是:一个线程对共享变...
    99+
    2023-06-16
  • java高并发的ReentrantLock重入锁
    目录synchronized的局限性ReentrantLockReentrantLock基本使用ReentrantLock是可重入锁ReentrantLock实现公平锁Reentra...
    99+
    2024-04-02
  • Java并发编程之StampedLock锁怎么应用
    本篇内容介绍了“Java并发编程之StampedLock锁怎么应用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!StampedLock:St...
    99+
    2023-06-30
  • 怎么利用Redis锁解决高并发
    小编给大家分享一下怎么利用Redis锁解决高并发,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!redis技术的使用:redis真的是一个很好的技术,它可以很好的在一定程度上解决网站一瞬间的...
    99+
    2024-04-02
  • 怎么理解iOS开发中的锁
    本篇内容介绍了“怎么理解iOS开发中的锁”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!OSSpinLock上述文章中已经介绍了 OSSpin...
    99+
    2023-06-04
  • Java 内存模型与死锁:深入理解并发编程中的死锁问题
    Java 内存模型(JMM)是一套规范,它定义了 Java 程序中变量是如何在多个线程之间共享的。JMM 规定了线程如何从主内存中读取和写入变量,以及如何将变量的值存储到主内存中。 死锁是并发编程中常见的一种问题,它发生在两个或多个线程...
    99+
    2024-02-04
    Java 内存模型 死锁 并发编程 同步 等待 通知 中断
  • java怎么处理高并发
    在处理高并发时,可以使用以下几种方法来提高Java应用程序的性能和并发处理能力:1. 使用线程池:通过使用线程池可以减少线程的创建和...
    99+
    2023-08-23
    java
  • 浅谈Java并发中ReentrantLock锁应该怎么用
    目录1、重入锁说明2、中断响应说明3、锁申请等待限时tryLock(long, TimeUnit)tryLock()4、公平锁说明源码(JDK8)重入锁可以替代关键字 synchro...
    99+
    2024-04-02
  • redis怎么实现并发锁
    Redis可以使用SETNX(SET if Not eXists)命令实现并发锁。以下是一个使用Redis实现并发锁的示例代码:``...
    99+
    2023-08-30
    redis
  • Java并发之嵌套管程锁死详解
    ·嵌套管程死锁是如何发生的·具体的嵌套管程死锁的例子·嵌套管程死锁 vs 死锁嵌套管程锁死类似于死锁, 下面是一个嵌套管程锁死的场景:Thread 1 synchronizes on AThread 1 synchronizes on B ...
    99+
    2023-05-30
    java 嵌套 并发
  • 深入了解Java并发AQS的独占锁模式
    目录概述自定义独占锁例子核心原理机制源码解析成员变量独占锁获取acquire(int)独占锁释放release(int)总结概述 稍微对并发源码了解的朋友都知道,很多并发工具如Ree...
    99+
    2022-11-13
    Java并发AQS独占锁模式 Java AQS独占锁 Java AQS
  • 怎么理解Java诡异并发中的有序性
    这篇文章主要介绍“怎么理解Java诡异并发中的有序性”,在日常操作中,相信很多人在怎么理解Java诡异并发中的有序性问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么理解Ja...
    99+
    2024-04-02
  • java并发模型中的两种锁是什么
    这篇文章主要介绍java并发模型中的两种锁是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!Java可以用来干什么Java主要应用于:1. web开发;2. Android开发;3. 客户端开发;4. 网页开发;5...
    99+
    2023-06-14
软考高级职称资格查询
推荐阅读
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作