`
student_lp
  • 浏览: 428879 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java高并发编程:11--ReentrantReadWriteLock深入分析

阅读更多

一、ReentrantReadWriteLock与ReentrantLock

    说到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。

    ReentrantLock 实现了标准的互斥操作,也就是一次只能有一个线程持有锁,也即所谓独占锁的概念。前面的章节中一直在强调这个特点。显然这个特点在一定程度上面减低了吞吐量,实际上独占锁是一种保守的锁策略,在这种情况下任何“读/读”,“写/读”,“写/写”操作都不能同时发生。但是同样需要强调的一个概念是,锁是有一定的开销的,当并发比较大的时候,锁的开销就比较客观了。所以如果可能的话就尽量少用锁,非要用锁的话就尝试看能否改造为读写锁。

    ReadWriteLock 描述的是:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。也就是说读写锁使用的场合是一个共享资源被大量读取操作,而只有少量的写操作(修改数据)。清单0描述了ReadWriteLock的API。

    ReadWriteLock 接口如下:

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

   上面描述的ReadWriteLock结构,这里需要说明的是ReadWriteLock并不是Lock的子接口,只不过ReadWriteLock借助Lock来实现读写两个视角。在ReadWriteLock中每次读取共享数据就需要读取锁,当需要修改共享数据时就需要写入锁。看起来好像是两个锁,但其实不尽然,下文会指出。

二、ReentrantReadWriteLock的特性

    ReentrantReadWriteLock有以下几个特性:

  • 公平性
    • 非公平锁(默认) 这个和独占锁的非公平性一样,由于读线程之间没有锁竞争,所以读操作没有公平性和非公平性,写操作时,由于写操作可能立即获取到锁,所以会推迟一个或多个读操作或者写操作。因此非公平锁的吞吐量要高于公平锁。
    • 公平锁 利用AQS的CLH队列,释放当前保持的锁(读锁或者写锁)时,优先为等待时间最长的那个写线程分配写入锁,当前前提是写线程的等待时间要比所有读线程的等待时间要长。同样一个线程持有写入锁或者有一个写线程已经在等待了,那么试图获取公平锁的(非重入)所有线程(包括读写线程)都将被阻塞,直到最先的写线程释放锁。如果读线程的等待时间比写线程的等待时间还有长,那么一旦上一个写线程释放锁,这一组读线程将获取锁。
  • 重入性
    • 读写锁允许读线程和写线程按照请求锁的顺序重新获取读取锁或者写入锁。当然了只有写线程释放了锁,读线程才能获取重入锁。
    • 写线程获取写入锁后可以再次获取读取锁,但是读线程获取读取锁后却不能获取写入锁。
    • 另外读写锁最多支持65535个递归写入锁和65535个递归读取锁。
  • 锁降级
    • 写线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
  • 锁升级
    • 读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。
  • 锁获取中断
    • 读取锁和写入锁都支持获取锁期间被中断。这个和独占锁一致。
  • 条件变量
    • 写入锁提供了条件变量(Condition)的支持,这个和独占锁一致,但是读取锁却不允许获取条件变量,将得到一个UnsupportedOperationException异常。
  • 重入数
    • 读取锁和写入锁的数量最大分别只能是65535(包括重入数)。

三、ReentrantReadWriteLock的内部实现

3.1 读写锁是独占锁的两个不同视图

    ReentrantReadWriteLock里面的锁主体就是一个Sync,也就是上面提到的FairSync或者NonfairSync,所以说实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样,所以前面才有读写锁是独占锁的两个不同视图一说。

    ReentrantReadWriteLock里面有两个类:ReadLock/WriteLock,这两个类都是Lock的实现。

    清单1 ReadLock 片段

public static class ReadLock implements Lock, java.io.Serializable  {
    private final Sync sync;

    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

    public void lock() {
        sync.acquireShared(1);
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    public  boolean tryLock() {
        return sync.tryReadLock();
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public  void unlock() {
        sync.releaseShared(1);
    }

    public Condition newCondition() {
        throw new UnsupportedOperationException();
    }

}

    清单2 WriteLock 片段

public static class WriteLock implements Lock, java.io.Serializable  {
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    public void lock() {
        sync.acquire(1);
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock( ) {
        return sync.tryWriteLock();
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isHeldByCurrentThread() {
        return sync.isHeldExclusively();
    }

    public int getHoldCount() {
        return sync.getWriteHoldCount();
    }
}

    清单1描述的是读锁的实现,清单2描述的是写锁的实现。显然WriteLock就是一个独占锁,这和ReentrantLock里面的实现几乎相同,都是使用了AQS的acquire/release操作。当然了在内部处理方式上与ReentrantLock还是有一点不同的。对比清单1和清单2可以看到,ReadLock获取的是共享锁,WriteLock获取的是独占锁。

3.2 ReentrantReadWriteLock中的state

    在AQS章节中介绍到AQS中有一个state字段(int类型,32位)用来描述有多少线程获持有锁。在独占锁的时代这个值通常是0或者1(如果是重入的就是重入的次数),在共享锁的时代就是持有锁的数量。在上一节中谈到,ReadWriteLock的读、写锁是相关但是又不一致的,所以需要两个数来描述读锁(共享锁)和写锁(独占锁)的数量。显然现在一个state就不够用了。于是在ReentrantReadWrilteLock里面将这个字段一分为二,高位16位表示共享锁的数量,低位16位表示独占锁的数量(或者重入数量)。2^16-1=65536,这就是上节中提到的为什么共享锁和独占锁的数量最大只能是65535的原因了。

3.3 读写锁的获取和释放

    有了上面的知识后再来分析读写锁的获取和释放就容易多了。

    清单3 写入锁获取片段

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if ((w == 0 && writerShouldBlock(current)) ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

   清单3 是写入锁获取的逻辑片段,整个工作流程是这样的:

    1. 持有锁线程数非0(c=getState()不为0),如果写线程数(w)为0(那么读线程数就不为0)或者独占锁线程(持有锁的线程)不是当前线程就返回失败,或者写入锁的数量(其实是重入数)大于65535就抛出一个Error异常。否则进行2。
    2. 如果当且写线程数位0(那么读线程也应该为0,因为步骤1已经处理c!=0的情况),并且当前线程需要阻塞那么就返回失败;如果增加写线程数失败也返回失败。否则进行3。
    3. 设置独占线程(写线程)为当前线程,返回true。

    清单3 中 exclusiveCount(c)就是获取写线程数(包括重入数),也就是state的低16位值。另外这里有一段逻辑是当前写线程是否需要阻塞writerShouldBlock(current)。清单4 和清单5 就是公平锁和非公平锁中是否需要阻塞的片段。很显然对于非公平锁而言总是不阻塞当前线程,而对于公平锁而言如果AQS队列不为空或者当前线程不是在AQS的队列头那么就阻塞线程,直到队列前面的线程处理完锁逻辑。

    清单4 公平读写锁写线程是否阻塞 

final boolean writerShouldBlock(Thread current) {
    return !isFirst(current);
}

    清单5 非公平读写锁写线程是否阻塞 

final boolean writerShouldBlock(Thread current) {
    return false;
}

   写入锁的获取逻辑清楚后,释放锁就比较简单了。清单6 描述的写入锁释放逻辑片段,其实就是检测下剩下的写入锁数量,如果是0就将独占锁线程清空(意味着没有线程获取锁),否则就是说当前是重入锁的一次释放,所以不能将独占锁线程清空。然后将剩余线程状态数写回AQS。

   清单6 写入锁释放逻辑片段 

protected final boolean tryRelease(int releases) {
    int nextc = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    if (exclusiveCount(nextc) == 0) {
        setExclusiveOwnerThread(null);
        setState(nextc);
        return true;
    } else {
        setState(nextc);
        return false;
    }
}

    清单3~6 描述的写入锁的获取释放过程。读取锁的获取和释放过程要稍微复杂些。 清单7描述的是读取锁的获取过程。

    清单7 读取锁获取过程片段 

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    if (sharedCount(c) == MAX_COUNT)
        throw new Error("Maximum lock count exceeded");
    if (!readerShouldBlock(current) &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != current.getId())
            cachedHoldCounter = rh = readHolds.get();
        rh.count++;
        return 1;
    }
    return fullTryAcquireShared(current);
}

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != current.getId())
        rh = readHolds.get();
    for (;;) {
        int c = getState();
        int w = exclusiveCount(c);
        if ((w != 0 && getExclusiveOwnerThread() != current) ||
            ((rh.count | w) == 0 && readerShouldBlock(current)))
            return -1;
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            cachedHoldCounter = rh; // cache for release
            rh.count++;
            return 1;
        }
    }
}

    读取锁获取的过程是这样的:

    1. 如果写线程持有锁(也就是独占锁数量不为0),并且独占线程不是当前线程,那么就返回失败。因为允许写入线程获取锁的同时获取读取锁。否则进行2。
    2. 如果读线程请求锁数量达到了65535(包括重入锁),那么就跑出一个错误Error,否则进行3。
    3. 如果读线程不用等待(实际上是是否需要公平锁),并且增加读取锁状态数成功,那么就返回成功,否则进行4。
    4. 步骤3失败的原因是CAS操作修改状态数失败,那么就需要循环不断尝试去修改状态直到成功或者锁被写入线程占有。实际上是过程3的不断尝试直到CAS计数成功或者被写入线程占有锁。

3.4 HoldCounter

    在清单7 中有一个对象HoldCounter,这里暂且不提这是什么结构和为什么存在这样一个结构。

    接下来根据清单8 我们来看如何释放一个读取锁。同样先不理HoldCounter,关键的在于for循环里面,其实就是一个不断尝试的CAS操作,直到修改状态成功。前面说过state的高16位描述的共享锁(读取锁)的数量,所以每次都需要减去2^16,这样就相当于读取锁数量减1。实际上SHARED_UNIT=1<<16。

    清单8 读取锁释放过程

protected final boolean tryReleaseShared(int unused) {
    HoldCounter rh = cachedHoldCounter;
    Thread current = Thread.currentThread();
    if (rh == null || rh.tid != current.getId())
        rh = readHolds.get();
    if (rh.tryDecrement() <= 0)
        throw new IllegalMonitorStateException();
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

    好了,现在回头看HoldCounter到底是一个什么东西。首先我们可以看到只有在获取共享锁(读取锁)的时候加1,也只有在释放共享锁的时候减1有作用,并且在释放锁的时候抛出了一个IllegalMonitorStateException异常。而我们知道IllegalMonitorStateException通常描述的是一个线程操作一个不属于自己的监视器对象的引发的异常。也就是说这里的意思是一个线程释放了一个不属于自己或者不存在的共享锁。

    前面的章节中一再强调,对于共享锁,其实并不是锁的概念,更像是计数器的概念。一个共享锁就相对于一次计数器操作,一次获取共享锁相当于计数器加1,释放一个共享锁就相当于计数器减1。显然只有线程持有了共享锁(也就是当前线程携带一个计数器,描述自己持有多少个共享锁或者多重共享锁),才能释放一个共享锁。否则一个没有获取共享锁的线程调用一次释放操作就会导致读写锁的state(持有锁的线程数,包括重入数)错误。

     明白了HoldCounter的作用后我们就可以猜到它的作用其实就是当前线程持有共享锁(读取锁)的数量,包括重入的数量。那么这个数量就必须和线程绑定在一起。

     在Java里面将一个对象和线程绑定在一起,就只有ThreadLocal才能实现了。所以毫无疑问HoldCounter就应该是绑定到线程上的一个计数器。

     清单9 线程持有读取锁数量的计数器

static final class HoldCounter {
    int count;
    final long tid = Thread.currentThread().getId();
    int tryDecrement() {
        int c = count;
        if (c > 0)
            count = c - 1;
        return c;
    }
}

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

    清单9 描述的是线程持有读取锁数量的计数器。可以看到这里使用ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。

    除了readLock()和writeLock()外,Lock对象还允许tryLock(),那么ReadLock和WriteLock的tryLock()不一样。清单10 和清单11 分别描述了读取锁的tryLock()和写入锁的tryLock()。

读取锁tryLock()也就是tryReadLock()成功的条件是:没有写入锁或者写入锁是当前线程,并且读线程共享锁数量没有超过65535个。

    写入锁tryLock()也就是tryWriteLock()成功的条件是: 没有写入锁或者写入锁是当前线程,并且尝试一次修改state成功。

    清单10 读取锁的tryLock()

final boolean tryReadLock() {
    Thread current = Thread.currentThread();
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return false;
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                cachedHoldCounter = rh = readHolds.get();
            rh.count++;
            return true;
        }
    }
}

    清单11 写入锁的tryLock()

final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c);
        if (w == 0 ||current != getExclusiveOwnerThread())
            return false;
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if (!compareAndSetState(c, c + 1))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

四、小结

   使用ReentrantReadWriteLock可以推广到大部分读,少量写的场景,因为读线程之间没有竞争,所以比起sychronzied,性能好很多。

    如果需要较为精确的控制缓存,使用ReentrantReadWriteLock倒也不失为一个方案。

 

转子:http://blog.csdn.net/vernonzheng/article/details/8297230

分享到:
评论

相关推荐

    仿Haier 海尔家电家居触屏版html5响应式手机wap企业网站模板.zip

    触屏版自适应手机wap软件网站模板 触屏版自适应手机wap软件网站模板

    手机wap网站 仿腾讯新闻.zip

    触屏版自适应手机wap软件网站模板 触屏版自适应手机wap软件网站模板

    机械设计多层储物架sw18可编辑非常好的设计图纸100%好用.zip

    机械设计多层储物架sw18可编辑非常好的设计图纸100%好用.zip

    基于普中51开发板的超声波测距+蜂鸣器报警 (附开发板原理图)

    基于普中51开发板的超声波测距+蜂鸣器报警 (附开发板原理图) 基于普中51开发板的超声波测距+蜂鸣器报警 (附开发板原理图)

    【重磅、详细、2022更新!】1990-2022上市公司环境保护税(排污费)数据大合集!

    【原创整理,严禁任何团队和个人转载获利,转载必究!】 环境保护 税的实施效应是近年来研究的热点方向之一,将上市公司环境保护税与企业创新、企业治理 等领域问题的实证研究更是如日中天。附件内为1990-2022上市公司环境保护税( 排污费)数据大合集,包括上市公司应缴环境保护税、实缴环境保护税及分项数据,样本期 长达33年!累计涵盖近15w+观测值数量,3500+样本企业,数据涵盖年度与月度 变量,可根据需要自行筛选选用!本数据集包括参考来自权威文献做法构建的衡量上市公司 环境保护税的详细数据,包括测算出的最终指标以及所有原始数据! 附 件内所有文件均包括xls、dta格式面板数据,无偿赠送您权威参考文献原文、参考代 码、原始数据(指标构建所需的各个变量都可直接查到,充分保证数据真实性、准确性!! )以及2022最新版本上市公司年度信息,充分节约您宝贵的时间,提升科研效率!本数 据集可直接用Stata、Eviews等软件进行计量检验!本数据集适用于所有涉及上 市公司环境保护税效应方向的实证研究与理论分析,更是非常适合在此基础上进行拓展研究 ,能够帮助大家高

    自动编码器autoencoder-python源码.zip

    自动编码器autoencoder-python源码.zip

    node-v10.16.2-linux-x64.tar.xz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    ONNXRuntime部署yolov5-lite目标检测包含C++和Python 源码+模型+说明.zip

    ONNXRuntime部署yolov5-lite目标检测包含C++和Python 源码+模型+说明.zip

    省级2008-2020信息传输、软件和信息技术服务业城镇单位就业人员+软件业务收入等

    省级2008-2020信息传输、软件和信息技术服务业城镇单位就业人员+软件业务收 入等 指标包括:信息传输、软件和信息技术服务业城镇单位就业人员(万人)、软件业务 收入(万元)、高技术产品出口额占商品出口额比重(%)

    基于matlab图像处理的碎纸片的拼接复原内含数据集和源码.docx

    本文档是课题研究的研究报告内含调研以及源码设计以及结果分析

    Unity3D版本游戏源码2-97草莓大作战游戏开发模板BerryMatch-Three4.2

    Unity3D版本游戏源码2-97草莓大作战游戏开发模板Berry Match-Three 4.2提取方式是百度网盘分享地址

    南京政府微门户触屏版手机wap政府网站模板下载.zip

    触屏版自适应手机wap软件网站模板 触屏版自适应手机wap软件网站模板

    node-v12.1.0-linux-ppc64le.tar.xz

    Node.js,简称Node,是一个开源且跨平台的JavaScript运行时环境,它允许在浏览器外运行JavaScript代码。Node.js于2009年由Ryan Dahl创立,旨在创建高性能的Web服务器和网络应用程序。它基于Google Chrome的V8 JavaScript引擎,可以在Windows、Linux、Unix、Mac OS X等操作系统上运行。 Node.js的特点之一是事件驱动和非阻塞I/O模型,这使得它非常适合处理大量并发连接,从而在构建实时应用程序如在线游戏、聊天应用以及实时通讯服务时表现卓越。此外,Node.js使用了模块化的架构,通过npm(Node package manager,Node包管理器),社区成员可以共享和复用代码,极大地促进了Node.js生态系统的发展和扩张。 Node.js不仅用于服务器端开发。随着技术的发展,它也被用于构建工具链、开发桌面应用程序、物联网设备等。Node.js能够处理文件系统、操作数据库、处理网络请求等,因此,开发者可以用JavaScript编写全栈应用程序,这一点大大提高了开发效率和便捷性。 在实践中,许多大型企业和组织已经采用Node.js作为其Web应用程序的开发平台,如Netflix、PayPal和Walmart等。它们利用Node.js提高了应用性能,简化了开发流程,并且能更快地响应市场需求。

    某城市表层土壤重金属污染分析方案设计以及结果分析.doc

    本文档是课题研究的研究报告内含调研以及源码设计以及结果分析

    unity开发的相关知识什么是unity开发以及学习的意义

    unity开发

    HTML5+CSS+JS精品网页模板100 大学生期末大作业 Web前端网页制作

    HTML5+CSS+JS精品网页模板,设置导航条、轮翻效果,鼠标滑动效果,自动弹窗,点击事件、链接等功能;适用于大学生期末大作业或公司网页制作。响应式网页,可以根据不同的设备屏幕大小自动调整页面布局; 支持如Dreamweaver、HBuilder、Text 、Vscode 等任意html编辑软件进行编辑修改; 支持包括IE、Firefox、Chrome、Safari主流浏览器浏览; 下载文件解压缩,用Dreamweaver、HBuilder、Text 、Vscode 等任意html编辑软件打开,只需更改源代码中的文字和图片可直接使用。图片的命名和格式需要与原图片的名字和格式一致,其他的无需更改。如碰到HTML5+CSS+JS等专业技术问题,以及需要对应行业的模板等相关源码、模板、资料、教程等,随时联系博主咨询。 网页设计和制作、大学生网页课程设计、期末大作业、毕业设计、网页模板,网页成品源代码等,5000+套Web案例源码,主题涵盖各行各业,关注作者联系获取更多源码; 更多优质网页博文、网页模板移步查阅我的CSDN主页:angella.blog.csdn.net。

    pytorch_mnist-python源码.zip

    pytorch_mnist-python源码.zip

    仿中企动力手机wap企业网站模板.zip

    触屏版自适应手机wap软件网站模板 触屏版自适应手机wap软件网站模板

    手机js特效jquery触屏自适应实现随机图片瀑布流无限加载.zip

    触屏版自适应手机wap软件网站模板 触屏版自适应手机wap软件网站模板

    集团公司主数据(mdm)治理总体解决方案MDM.pptx

    集团公司主数据(mdm)治理总体解决方案MDM.pptx

Global site tag (gtag.js) - Google Analytics