如何正确理解Java领域中的并发锁,我们应该具体掌握到什么程度?

苍穹之边,浩瀚之挚,眰恦之美; 悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》





写在开头





对于Java领域中的锁,其实从接触Java至今,我相信每一位Java Developer都会有这样的一个感觉?不论是Java对锁的实现还是应用,真的是一种“群英荟萃”,而且每一种锁都有点各有各的驴,各有各的本,各不相同。


在很多情况下,以及在各种锁的应用场景里,各式各样的定义,难免会让我们觉得无所适从,很难清楚该如何对这些锁做到得心应手?


在并发编程色世界中,一般情况下,我们只需了解其是如何使用锁之后就已经满足我们大部分的需求,但是作为一名对技术研究有执念和热情的人来说,深入探究和分析才是对技术的探秘之乐趣。


作为一名Java Developer来说,深入探究和分析和正确了解和掌握这些锁的机制和原理,需要我们带着一些实际问题,通过对其探究分析和加上实际应用分析,才能真正意义上理解和掌握。


一般来说,针对于不同场景提供的锁,都用于解决什么问题?不论是从实现方式上,还是从使用场景上,都可以应对这些锁的特点,我们又该如何认识和理解?


接下来,今天我们就一起来盘一盘,Java领域中那些并发锁,盘点一下相关的锁,从设计基本思想和设计实现,以及应用分析等方面来总体分析探讨一下。


关健术语





本文用到的一些关键词语以及常用术语,主要如下:





基本概述





在Java领域中,单纯从Java对其实现的方式上来看,我们大体上可以将其分为基于Java语法层面(关键词)实现的锁和基于JDK层面实现的锁。


基于这两个基本点,可以作为我们对于Java领域中的锁的一个基础认识,这对于我们认识和了解Java领域中的锁指导一个参考方向。


一般来说,锁是并发编程中最基础和最常用的一项技术,而且在Java的内部JDK中其使用也是非常地广泛。


接下来,我们便一起探究和认识一下Java领域中的各种各样的锁。


一.锁的基本理论


锁的基本理论主要是指从锁的基本定义和基本特点以及基本意义去分析的一般模型理论,是一套帮助我们认识和了解锁的简单的思维方法论。





一般在了解一个事物之前,我们都会按照基本定义,基本特点以及基本意义去看待这个事物。在计算机的世界里,锁本身也和我们实际生活一样,也是一个比较普遍且应用场景繁多的一种事物。


比如,在操作系统中,也定义了各种各样的锁;在数据库系统中也出现了锁。甚至,在CPU处理器架构中都会看见锁的身影。


但是,这里就会有一个问题:既然都在使用锁,可是对于锁该去如何定义,似乎都很难给出一个准确的定义? 换而言之,这也许就是我们对于锁只是知道有这个东西,但是一直有云里雾里的基本原因。


从本质上讲,计算机软件开发领域中的锁是一种协调多个进程 或者多个线程对某一个资源的访问的控制机制,其核心是作用于资源,也作用于着这个定义中提到的进程和线程等。其中:








一般来说,线程主要分为位于系统内核空间的线程称为内核线程(Kernel Thread)和位于应用程序的用户空间的线程被称为用户线程(User Thread)两种,其中:





也就是我们一般说的Java线程等均属于用户线程,而内核线程主要是操作系统封装的函数库以及API等。


而且最关健的就是,我们平日里所提到Java线程和JVM都是位于用户空间之中,从Java层到操作系统系统的线程调度顺序来看,一般流程是:java.lang.Thread(Target Thread)->Java Thread->OSThread->pthread->Kernel Thread。





简单来说,在Java领域中,锁是用于控制多个线程访问共享资源的工具。一般,锁提供对共享资源的独立访问:一次只有一个线程可以获取锁,所有对共享资源的访问都需要先获取锁。但是,某些锁可以并发访问共享资源。


对于并发访问共享资源来说,主要是依据现在大多数操作系统的线程的调度方式是抢占式调度,因此加锁是为了维护数据的一致性和完整性,其实就是数据的安全性。


综上所述,我们便可以得到一个关于锁的基本概念模型,接下来我们便来一一盘点以下主要有哪些锁。


二.锁的基本分类


在Java领域中,我们可以将锁大致分为基于Java语法层面(关键词)实现的锁和基于JDK层面实现的锁。





单纯从Java对其实现的方式上来看,我们大体上可以将其分为基于Java语法层面(关键词)实现的锁和基于JDK层面实现的锁。其中:





需要特别注意的是,在Java领域中,基于JDK层面的锁通过CAS操作解决了并发编程中的原子性问题,而基于Java语法层面实现的锁解决了并发编程中的原子性问题和可见性问题。


除此之外之外,在Java并发容器中曾用到过一种Segment数组结构来实现的分段锁。


而从具体到对应的Java线程资源来说,我们按照是否含有某一特性来定义锁,主要可以从如下几个方面来看:














针对于上述描述的各种情况,接下来,我们便来一起详细看看,在Java领域中,这个锁的具体情况。


三.Java内置锁


在Java领域中,Java内置锁主要是指基于Java语法层面(关键词)实现的锁。





在Java领域中,我们把基于Java语法层面(关键词)实现的锁称为内置锁,比如synchronized 关键字。


对于synchronized 关键字的解释,最直接的就是Java语言中为开发人员提供的同步工具,可以看作是Java中的一种“语法糖”。主要宗旨在于解决多线程并发执行过程中数据同步的问题。


不像其他的编程语言(C++),在处理同步问题时都需要自己进行锁处理,主要特点就是简单,直接声明即可。


在 Java 程序中,利用 synchronized 关键字来对程序进行加锁,其实现同步的语义是互斥锁。既可以用来声明一个 synchronized 代码块,也可以直接标记静态方法或者实例方法。


其中,对于互斥的概念来说,在数学范畴来讲,是一个数学名词,表示和描述的是事件A与事件B在任何一次试验中都不会同时发生,则称事件A与事件B互斥。


因此,对于互斥锁可以理解为: 对于某一个锁来说,任意时刻只能有一个线程获得该锁,对于其他线程想获取锁的时候就得等待或者被阻塞。


1.使用方式


在Java领域中,synchronized关键字互斥锁主要有作用于对象方法上面,作用于类静态方法上面,作用于对象方法里面,作用于类静态方法里面等4种方式。





在Java领域中,synchronized关键字从使用方式来看,主要可以分为:



















一般当我们在编写代码的过程中,如果按照上述方式声明时,被synchronized关键字声明的代码会比普通代码在编译之后,使用javap -c xxx.class 查看字节码,就会发现多两个monitorenter和monitorexit指令。


2.基本思想


在Java领域中,synchronized关键字互斥锁主要基于一个阻塞队列和等待对列,类似于一种“等待-通知”的工作机制来实现。





一般情况下,“等待 - 通知”的工作机制的要求是线程首先获取互斥锁,其中:





在Java领域中, Java 语言内置的 synchronized 配合java.lang.Object类定义的 wait()、notify()、notifyAll() 这三个方法就能轻松实现等待 - 通知机制,其中:






一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。


为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。


对于等待队列的工作机制来说,同一时刻,只允许一个线程进入 synchronized 保护的临界区。当有一个线程进入临界区后,其他线程就只能进入图中左边的等待队列里等待。这个等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。在并发程序中,其中:









对于通知队列的工作机制来说,那线程要求的条件满足时,该怎么通知这个等待的线程呢?很简单,就是 Java 对象的 notify() 和 notifyAll() 方法。当条件满足时调用 notify(),会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。为什么说是曾经满足过呢?其中:









上面我们一直强调 wait()、notify()、notifyAll() 方法操作的等待队列是互斥锁的等待队列,其中:





而且 wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。


如果在 synchronized{}外部调用,或者锁定的 this,而用 target.wait() 调用的话,JVM 会抛出一个运行时异常:java.lang.IllegalMonitorStateException。


对于和notifyAll() 和notify()来实现通知机制,特别需要注意的是,两者之间的区别:





从感觉上来讲,应该是 notify() 更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但是实际上使用 notify() 也很有风险,主要在于可能导致某些线程永远不会被通知到。


在具体使用过程中,所以除非经过深思熟虑,一般推荐尽量使用 notifyAll()。


3.基本实现


在Java领域中,synchronized关键字互斥锁主要基于Java HotSpot(TM) VM 虚拟机通过Monitor(监视器)来实现monitorenter和monitorexit指令的。





在Java HotSpot(TM) VM 虚拟机中,主要是通过Monitor(监视器)来实现monitorenter和monitorexit指令的,Monitor(监视器)一般包括一个阻塞队列和一个等待队列,其中:





其中,需要注意的是,当调用 wait()方法后会释放锁并通知阻塞队列。


一般来说,当Java字节码(class)被托管到Java HotSpot(TM) VM 虚拟机后,Monitor(监视器)就被采用ObjectMonitor接管,其中:





对于monitorenter指令来说,其中:





主要工作流程如下:






对于monitorexit指令来说,其中:





主要工作流程如下:





综上所述,monitorenter和monitorexit两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现。被阻塞的线程会被挂起、等待重新调度,会导致"用户态和内核态"两个态之间来回切换,对性能有较大影响。


4.具体实现


在Java领域中,JVM中每个对象都会有一个监视器,监视器和对象一起创建、销毁。监视器相当于一个用来监视这些线程进入的特殊房间,其义务是保证(同一时间)只有一个线程可以访问被保护的临界区代码块。





本质上,监视器是一种同步工具,也可以说是一种同步机制,主要特点是:





在Hotspot虚拟机中,监视器是由C++类ObjectMonitor实现的,ObjectMonitor类定义在ObjectMonitor.hpp文件中,ObjectMonitor的Owner(_owner)、WaitSet(_WaitSet)、Cxq(_cxq)、EntryList(_EntryList)这几个属性比较关键。





ObjectMonitor的WaitSet、Cxq、EntryList这三个队列存放抢夺重量级锁的线程,而ObjectMonitor的Owner所指向的线程即为获得锁的线程。其中:






Cxq并不是一个真正的队列,只是一个虚拟队列,原因在于Cxq是由Node及其next指针逻辑构成的,并不存在一个队列的数据结构。每次新加入Node会在Cxq的队头进行,通过CAS改变第一个节点的指针为新增节点,同时设置新增节点的next指向后续节点;从Cxq取得元素时,会从队尾获取。显然,Cxq结构是一个无锁结构。在线程进入Cxq前,抢锁线程会先尝试通过CAS自旋获取锁,如果获取不到,就进入Cxq队列,这明显对于已经进入Cxq队列的线程是不公平的。所以,synchronized同步块所使用的重量级锁是不公平锁。


EntryList与Cxq在逻辑上都属于等待队列。Cxq会被线程并发访问,为了降低对Cxq队尾的争用,而建立EntryList。在Owner线程释放锁时,JVM会从Cxq中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为OnDeck Thread(Ready Thread)。EntryList中的线程作为候选竞争线程而存在。


JVM不直接把锁传递给Owner Thread,而是把锁竞争的权利交给OnDeck Thread,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大地提升系统的吞吐量,在JVM中,也把这种选择行为称为“竞争切换”。OnDeck Thread获取到锁资源后会变为Owner Thread。无法获得锁的OnDeck Thread则会依然留在EntryList中,考虑到公平性,OnDeck Thread在EntryList中的位置不发生变化(依然在队头)。在OnDeck Thread成为Owner的过程中,还有一个不公平的事情,就是后来的新抢锁线程可能直接通过CAS自旋成为Owner而抢到锁。


如果Owner线程被Object.wait()方法阻塞,就转移到WaitSet队列中,直到某个时刻通过Object.notify()或者Object.notifyAll()唤醒,该线程就会重新进入EntryList中。


处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,线程的阻塞或者唤醒都需要操作系统来帮忙,Linux内核下采用pthread_mutex_lock系统调用实现,进程需要从用户态切换到内核态。


5.基本分类


在Java领域中,synchronized关键字互斥锁主要中内置锁一共有4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这些状态随着竞争情况逐渐升级。





在Java领域中,一般Java对象(Object实例)结构包括三部分:对象头、对象体和对齐字节,其中:















Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。


在JDK 1.6版本之前,所有的Java内置锁都是重量级锁。重量级锁会造成CPU在用户态和核心态之间频繁切换,所以代价高、效率低。


JDK 1.6版本为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁的实现。


在JDK 1.6版本中内置锁一共有4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这些状态随着竞争情况逐渐升级。其中:







因此,根据上述的锁状态来看,我们可以把Java内置锁分为无锁,偏向锁,轻量级锁和重量级锁等4种锁,其中:










从锁升级的状态顺序来看,只能是: 无锁->偏向锁->轻量级锁->重量级锁 ,而且顺序不可逆,也就是不能降级。





综上所述,在Java内置锁中,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。


6.应用分析


在Java领域中,synchronized关键字互斥锁主要中内置锁使用简单,但是锁的粒度比较大,无法支持超时等。





从synchronized的执行过程,大致如下:









总体来说,偏向锁是在没有发生锁争用的情况下使用的;一旦有了第二个线程争用锁,偏向锁就会升级为轻量级锁;如果锁争用很激烈,轻量级锁的CAS自旋到达阈值后,轻量级锁就会升级为重量级锁。


四.Java显式锁


在Java领域中,Java显式锁主要是指基于JDK层面实现的锁。





在Java领域中,基于JDK层面实现的锁都存在于java.util.concurrent.locks包下面,大致可以分为:







一直以来,并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作等。


Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。


1.JDK源码


在Java领域中,Java显式锁从JDK源码表现出来的锁大致可以分为基于Lock接口实现的锁,基于ReadWriteLock接口实现的锁,基于AQS基础同步器实现的锁,以及基于自定义API操作实现的锁等。





在Java领域中,基于JDK源码层面体现出来的锁,主要分为如下几种:







从一定程度上说,Java显式锁都是基于AQS基础同步器实现的锁,其中JDK1.8版本中提供的StampedLock是是对ReentrantReadWriteLock读写锁的一种改进。


综上所述,认识和掌握Java内置锁,都需要AQS基础同步器设计与实现,它是ava内置锁的基础和核心实现。


2.基本思想


在Java领域中,Java显式锁的基本思想来源于JDK并发包JUC的作者Doug Lea,发表的论文为java.util.concurrent Synchronizer Framework 。





在Java领域中,同步器是指专门为多线程并发而设计的同步机制,在这种机制下,多线程并发执行时线程之间通过某种共享状态实现同步,只有满足某种条件时线程才能执行。


在不同的应用场景中,对同步器的需求也不同,JDK将各种同步器的相同部分抽象封装成一个统一的基础同步器,然后基于这个同步器为模板,通过继承的方式来实现不同的同步器,即就是我们说的统一的基础AQS同步器。


在JDK的并发包java.util.concurrent.下面,提供了各种同步工具,其中大部分同步工具都基于AbstractQueuedSynchronizer类实现,即就是AQS同步器,为不同场景提供了实现锁以及同步机制的基础框架,为同步状态的原子性管理,线程阻塞与解除以及排队管理提供一种通用的机制。


其中,AQS的理论基础是JDK并发包JUC的作者Doug Lea,发表的论文为java.util.concurrent Synchronizer Framework [AQS Framework论文],其中包括框架的基础原理,需求,设计,实现思路,设计以及用户和性能分析等。


3.基本实现


在Java领域中,Java显式锁从一定程度上说,Java显式锁都是基于AQS基础同步器实现的锁。





从JDK1.8版本的源码来看,AbstractQueuedSynchronizer的主要继承了抽象类AbstractOwnableSynchronizer,其主要封装了setExclusiveOwnerThread()和getExclusiveOwnerThread()两个方法。其中:





对于一个AbstractQueuedSynchronizer(AQS同步器)从内部结构上来说,主要有5个核心要素: 同步状态,等待队列,独占模式,共享模式,条件队列。其中:











从采用的数据结构来看,AQS同步器主要是将线程封装到一个Node里面,并维护一个CLH Node FIFO队列(非阻塞FIFO队列),以为着在并发条件下,对此队列中进行插入和移除操作时不会阻塞,主要是采用CAS+自旋锁来保证节点的插入和移除的原子性操作,从而实现快速插入的。


从JDK1.8版本的源码来看,AbstractQueuedSynchronizer的源码结构主要如下:








但是,特别需要注意的是,在JDK1.8版本之后,AbstractQueuedSynchronizer的源码结构有所不同:








由此可见,最大的不同就是使用VarHandle来替代Unsafe类,Varhandle是对变量或参数定义的变量系列的动态强类型引用,包括静态字段,非静态字段,数组元素或堆外数据结构的组件。 在各种访问模式下都支持访问这些变量,包括简单的读/写访问,volatile 的读/写访问以及 CAS (compare-and-set)访问。简单来说 Variable 就是对这些变量进行绑定,通过 Varhandle 直接对这些变量进行操。


4.具体实现


在Java领域中,Java显式锁中基于AQS基础同步器实现的锁主要都是采用自旋锁(CLH锁)+CAS操作来实现。





在介绍内置锁的时候,提到轻量级锁的主要分类为普通自旋锁和自适应自旋锁,但其实对于自旋锁的实现方式来看,主要可以分为普通自旋锁和自适应自旋锁,CLH锁和MCS锁等4种,其中:







自旋锁是一种实现同步的方案,属于一种非阻塞锁,与常规锁主要的区别就在于获取锁失败之后的处理方式不同,主要体现在:





其实,自旋是一钟忙等待状态,会一直消耗CPU的执行时间。一般情况下,常规互斥锁适用于持有锁长时间的情况,自旋锁适合持有时间短的情况。


其中,对于CLH锁来说,其核心是为解决同步带来的花销问题,Craig,Landim,Hagersten三人发明了CLH锁,其中主要是:





CLH锁将众多线程长时间对资源的竞争,通过有序化这些线程将其转化为只需要对本地变量检测。唯一存在竞争的地方就是入队之前对尾部节点tail 的竞争,相对来说,当前线程对资源的竞争次数减少,这节省了CPU缓存同步的消耗,从而提升了系统性能。


但是同时也有一个问题,CLH锁虽然解决了大量线程同时操作同一个变量时带来的开销问题,如果前驱节点和当前节点在本地主存中不存在,则访问时间过长,也会引起性能问题。MCS锁就时为解决这个问题提出的,作者主要是John Mellor Curmmey和Michhael Scott两人发明的。


而对于CAS操作来说,CAS(Compare And Swap,比较并交换)操作时一种乐观锁策略,主要涉及三个操作数据:内存值,预期值,新值,主要是指当且仅当预期值和内存值相等时才去修改内存值为新值。


CAS操作的具体逻辑,主要可以分为三个步骤:






除此之外,需要注意的是CAS操作具有原子性,主要是由CPU硬件指令来保证,并且通过Java本地接口(Java Native Interface,JNI)调用本地硬件指令实现。


当然,CAS操作避免了悲观策略独占对象的 问题,同时提高了并发性能,但是也有以下三个问题:






其中,在Java领域中,对于CAS操作在





综上所述,主要说明Java显式锁为啥使用基于AQS基础同步器实现的锁主要都是采用自旋锁(CLH锁)+CAS操作来的具体实现。


5.基本分类


在Java领域中,Java显式锁的基本分类大致可以分为可重入锁和不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。





显式锁有很多种,从不同的角度来看,显式锁大概有以下几种分类:可重入锁和不可重入锁、悲观锁和乐观锁、公平锁和非公平锁、共享锁和独占锁、可中断锁和不可中断锁。


从同一个线程是否可以重复占有同一个锁对象的角度来分,显式锁可以分为可重入锁与不可重入锁。其中:





从线程进入临界区前是否锁住同步资源的角度来分,显式锁可以分为悲观锁和乐观锁。其中:





从抢占资源的公平性来说,显示锁可以分为公平锁和非公平锁,其中:





默认情况下,ReentrantLock实例是非公平锁,但是,如果在实例构造时传入了参数true,所得到的锁就是公平锁。另外,ReentrantLock的tryLock()方法是一个特例,一旦有线程释放了锁,正在tryLock的线程就能优先取到锁,即使已经有其他线程在等待队列中。


从在抢锁过程中能通过某些方法终止抢占过程角度来看,显式锁可以分为可中断锁和不可中断锁,其中:





简单来说,在抢锁过程中能通过某些方法终止抢占过程,这就是可中断锁,否则就是不可中断锁。


Java的synchronized内置锁就是一个不可中断锁,而JUC的显式锁(如ReentrantLock)是一个可中断锁。





综上所述,对于Java显式锁的基本分类,一般情况下我们都可按照这样的方式去分析。


6.应用分析


在Java领域中,Java显式锁的Java显式锁比Java内置锁的锁粒度更细腻,可以设置超时机制,更加可控,使用起来更加灵活。





对比基于Java内置锁实现一种简单的“等待-通知”方式的线程间通信:通过Object对象的wait、notify两类方法作为开关信号,用来完成通知方线程和等待方线程之间的通信。


“等待-通知”方式的线程间通信机制,具体来说是指一个线程A调用了同步对象的wait()方法进入等待状态,而另一线程B调用了同步对象的notify()或者notifyAll()方法去唤醒等待线程,当线程A收到线程B的唤醒通知后,就可以重新开始执行了。


需要特别注意的是,在通信过程中,线程需要拥有同步对象的监视器,在执行Object对象的wait、notify方法之前,线程必须先通过抢占到内置锁而成为其监视器的Owner。


与Object对象的wait、notify两类方法相类似,JUC也为大家提供了一个用于线程间进行“等待-通知”方式通信的接口——java.util.concurrent.locks.Condition。其中:













同时,JUC提供的一个线程阻塞与唤醒的工具类(java.util.concurrent.locks.LockSupport),该工具类可以让线程在任意位置阻塞和唤醒,其所有的方法都是静态方法。












相比之下,Java显式锁比Java内置锁的锁粒度更细腻,可以设置超时机制,更加可控,使用起来更加灵活。


五.Java锁综合对比分析


Java锁综合对比分析主要是对Java内置锁和Java显式锁等作一个对比分析,看看两者之间各自的特点。





在Java领域中,对于Java内置锁和Java显式锁,一般可以从以下几个方面去看:
















综上所述,通过对Java锁综合对比分析,我相信大家对于Java领域中的锁已经可以很好地认识以及深入了解。


写在最后





对于Java 领域中锁,我们一般可以从如下两个方面去认识,其中:





对于Java内置锁来说:









对于Java显式锁来说:









最后,技术研究之路任重而道远,愿我们熬的每一个通宵,都撑得起我们想在这条路上走下去的勇气,未来仍然可期,与君共勉!

展开阅读全文

页面更新:2024-06-17

标签:领域   队列   监视器   线程   程度   对象   状态   竞争   操作   方法   资源

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top