ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。
读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。ReadWriteLock适用于读多写少的并发情况。

使用

    private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private static int i;public static void main(String[] args) {for (int j = 0; j < 2; j++) {new Thread(() -> {try {lock.readLock().lock();i++;System.out.println(Thread.currentThread().getName() + " i = " + i + " readLock begin " + new Date());Thread.sleep(1000);} catch (Exception e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + " i = " + i + " readLock end " + new Date());lock.readLock().unlock();}}).start();}for (int j = 0; j < 2 ; j++) {new Thread(() -> {try {lock.writeLock().lock();i++;System.out.println(Thread.currentThread().getName() + " i = " + i + " writeLock begin " + new Date());Thread.sleep(1000);} catch (Exception e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + " i = " + i + " writeLock end " + new Date());lock.writeLock().unlock();}}).start();}try {lock.readLock().lock();i++;System.out.println(Thread.currentThread().getName() + " i = " + i + " readLock begin " + new Date());Thread.sleep(1000);} catch (Exception e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + " i = " + i + " readLock end " + new Date());lock.readLock().unlock();}}

main i = 1 readLock begin Mon Apr 29 13:55:22 CST 2019
Thread-0 i = 2 readLock begin Mon Apr 29 13:55:22 CST 2019
Thread-1 i = 3 readLock begin Mon Apr 29 13:55:22 CST 2019
main i = 3 readLock end Mon Apr 29 13:55:23 CST 2019
Thread-0 i = 3 readLock end Mon Apr 29 13:55:23 CST 2019
Thread-1 i = 3 readLock end Mon Apr 29 13:55:23 CST 2019
Thread-3 i = 4 writeLock begin Mon Apr 29 13:55:23 CST 2019
Thread-3 i = 4 writeLock end Mon Apr 29 13:55:24 CST 2019
Thread-2 i = 5 writeLock begin Mon Apr 29 13:55:24 CST 2019
Thread-2 i = 5 writeLock end Mon Apr 29 13:55:25 CST 2019

特性

获取顺序
非公平模式(默认)
当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。

公平模式

当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。
当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁。

可重入
允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。

锁降级
允许写锁降低为读锁

中断锁的获取
在读锁和写锁的获取过程中支持中断

支持Condition
写锁提供Condition实现

监控
提供确定锁是否被持有等辅助方法

源码分析

ReadWriteLock是接口,只有两个获取锁的方法

public interface ReadWriteLock {/*** Returns the lock used for reading.** @return the lock used for reading*/Lock readLock();/*** Returns the lock used for writing.** @return the lock used for writing*/Lock writeLock();
}

ReentrantReadWriteLock变量和构造器,默认是非公平锁,和ReentrantLock一样

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {private static final long serialVersionUID = -6992448646407690164L;/** Inner class providing readlock */private final ReentrantReadWriteLock.ReadLock readerLock;/** Inner class providing writelock */private final ReentrantReadWriteLock.WriteLock writerLock;/** Performs all synchronization mechanics */final Sync sync;/*** Creates a new {@code ReentrantReadWriteLock} with* default (nonfair) ordering properties.*/public ReentrantReadWriteLock() {this(false);}/*** Creates a new {@code ReentrantReadWriteLock} with* the given fairness policy.** @param fair {@code true} if this lock should use a fair ordering policy*/public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);}public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

核心内部类Sync继承aqs

    abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 6317671515068378041L;/** Read vs write count extraction constants and functions.* Lock state is logically divided into two unsigned shorts:* The lower one representing the exclusive (writer) lock hold count,* and the upper the shared (reader) hold count.*/static final int SHARED_SHIFT   = 16;static final int SHARED_UNIT    = (1 << SHARED_SHIFT);static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** Returns the number of shared holds represented in count  */static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }/** Returns the number of exclusive holds represented in count  */static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }/*** A counter for per-thread read hold counts.* Maintained as a ThreadLocal; cached in cachedHoldCounter*/static final class HoldCounter {int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());}/*** ThreadLocal subclass. Easiest to explicitly define for sake* of deserialization mechanics.*/static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}/*** The number of reentrant read locks held by current thread.* Initialized only in constructor and readObject.* Removed whenever a thread's read hold count drops to 0.*/private transient ThreadLocalHoldCounter readHolds;/*** The hold count of the last thread to successfully acquire* readLock. This saves ThreadLocal lookup in the common case* where the next thread to release is the last one to* acquire. This is non-volatile since it is just used* as a heuristic, and would be great for threads to cache.*/private transient HoldCounter cachedHoldCounter;/*** firstReader is the first thread to have acquired the read lock.* firstReaderHoldCount is firstReader's hold count.*/private transient Thread firstReader = null;private transient int firstReaderHoldCount;Sync() {readHolds = new ThreadLocalHoldCounter();setState(getState()); // ensures visibility of readHolds}

ReentrantReadWriteLock中的state代表了读锁的数量和写锁的持有与否,整个结构如下:

两个Sync

    /*** Nonfair version of Sync*/static final class NonfairSync extends Sync {private static final long serialVersionUID = -8159625535654395037L;final boolean writerShouldBlock() {return false; // writers can always barge}final boolean readerShouldBlock() {/* As a heuristic to avoid indefinite writer starvation,* block if the thread that momentarily appears to be head* of queue, if one exists, is a waiting writer.  This is* only a probabilistic effect since a new reader will not* block if there is a waiting writer behind other enabled* readers that have not yet drained from the queue.*/return apparentlyFirstQueuedIsExclusive();}}/*** Fair version of Sync*/static final class FairSync extends Sync {private static final long serialVersionUID = -2274990926593161451L;final boolean writerShouldBlock() {return hasQueuedPredecessors();}final boolean readerShouldBlock() {return hasQueuedPredecessors();}}

NonfairSync 没有构造器,父类Sync有构造器,执行了,初始化锁计数器的ThreadLocal

读锁获取

ReentrantReadWriteLock-ReadLock

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

AbstractQueuedSynchronizer

    public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);}

ReentrantReadWriteLock-Sync

static final int SHARED_SHIFT   = 16;static final int SHARED_UNIT    = (1 << SHARED_SHIFT);static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** Returns the number of shared holds represented in count  */static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }/** Returns the number of exclusive holds represented in count  */static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for*    lock wrt state, so ask if it should block*    because of queue policy. If not, try*    to grant by CASing state and updating count.*    Note that step does not check for reentrant*    acquires, which is postponed to full version*    to avoid having to check hold count in*    the more typical non-reentrant case.* 3. If step 2 fails either because thread*    apparently not eligible or CAS fails or count*    saturated, chain to version with full retry loop.*/Thread current = Thread.currentThread();int c = getState();//如果当前有写线程并且本线程不是写线程,不符合重入,失败if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;//得到读锁的个数int r = sharedCount(c);//如果读不应该阻塞并且读锁的个数小于最大值65535,并且可以成功更新状态值,成功if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {//如果当前读锁为0if (r == 0) {//第一个读线程就是当前线程firstReader = current;firstReaderHoldCount = 1;}//如果当前线程重入了,记录firstReaderHoldCountelse if (firstReader == current) {firstReaderHoldCount++;}//当前读线程和第一个读线程不同,记录每一个线程读的次数else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}//否则,循环尝试return fullTryAcquireShared(current);}

失败的原因有三,第一是应该读线程应该阻塞;第二是因为读锁达到了上线;第三是因为CAS失败,有其他线程在并发更新state,那么会调动fullTryAcquireShared方法。

NonfairSync,非公平锁,当有写锁等待时,即第一个入队节点是独占模式时,读锁获取要阻塞

        final boolean readerShouldBlock() {/* As a heuristic to avoid indefinite writer starvation,* block if the thread that momentarily appears to be head* of queue, if one exists, is a waiting writer.  This is* only a probabilistic effect since a new reader will not* block if there is a waiting writer behind other enabled* readers that have not yet drained from the queue.*/return apparentlyFirstQueuedIsExclusive();}

AbstractQueuedSynchronizer判断第一个入队节点是否是独占锁,头结点一定是空节点,第二个节点是第一个入对接点,

/*** Returns {@code true} if the apparent first queued thread, if one* exists, is waiting in exclusive mode.  If this method returns* {@code true}, and the current thread is attempting to acquire in* shared mode (that is, this method is invoked from {@link* #tryAcquireShared}) then it is guaranteed that the current thread* is not the first queued thread.  Used only as a heuristic in* ReentrantReadWriteLock.*/final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null &&(s = h.next)  != null &&!s.isShared()         &&s.thread != null;}

AbstractQueuedSynchronizer-Node判断入队节点是否是共享模式,通过nextWaiter字段标识

    static final class Node {/** Marker to indicate a node is waiting in shared mode */static final Node SHARED = new Node();/** Marker to indicate a node is waiting in exclusive mode */static final Node EXCLUSIVE = null;/*** Link to next node waiting on condition, or the special* value SHARED.  Because condition queues are accessed only* when holding in exclusive mode, we just need a simple* linked queue to hold nodes while they are waiting on* conditions. They are then transferred to the queue to* re-acquire. And because conditions can only be exclusive,* we save a field by using special value to indicate shared* mode.*/Node nextWaiter;/*** Returns true if node is waiting in shared mode.*/final boolean isShared() {return nextWaiter == SHARED;}

ReentrantReadWriteLock-Sync

        /*** Full version of acquire for reads, that handles CAS misses* and reentrant reads not dealt with in tryAcquireShared.*/final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) {int c = getState();//一旦有别的线程获得了写锁,返回-1,失败if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;} //如果读线程需要阻塞else if (readerShouldBlock()) {// Make sure we're not acquiring read lock reentrantlyif (firstReader == current) {// assert firstReaderHoldCount > 0;}//说明有别的读线程占有了锁else {if (rh == null) {rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) {rh = readHolds.get();if (rh.count == 0)readHolds.remove();}}if (rh.count == 0)return -1;}}//如果读锁达到了最大值,抛出异常if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");//如果成功更改状态,成功返回if (compareAndSetState(c, c + SHARED_UNIT)) {if (sharedCount(c) == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {if (rh == null)rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;cachedHoldCounter = rh; // cache for release}return 1;}}}

AbstractQueuedSynchronizer获取锁

/*** Acquires in shared uninterruptible mode.* @param arg the acquire argument*/private void doAcquireShared(int arg) {final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

写锁获取

ReentrantReadWriteLock-WriteLock

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

AbstractQueuedSynchronizer

/*** Acquires in exclusive mode, ignoring interrupts.  Implemented* by invoking at least once {@link #tryAcquire},* returning on success.  Otherwise the thread is queued, possibly* repeatedly blocking and unblocking, invoking {@link* #tryAcquire} until success.  This method can be used* to implement method {@link Lock#lock}.** @param arg the acquire argument.  This value is conveyed to*        {@link #tryAcquire} but is otherwise uninterpreted and*        can represent anything you like.*/public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

写锁使用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程加入到等待队列中。

protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero*    and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only*    happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if*    it is either a reentrant acquire or*    queue policy allows it. If so, update state*    and set owner.*///得到调用lock方法的当前线程Thread current = Thread.currentThread();int c = getState();//得到写锁的个数int w = exclusiveCount(c);//如果当前有写锁或者读锁if (c != 0) {// 如果写锁为0或者当前线程不是独占线程(不符合重入),返回falseif (w == 0 || current != getExclusiveOwnerThread())return false;//如果写锁的个数超过了最大值,抛出异常if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 写锁重入,返回truesetState(c + acquires);return true;}//如果当前没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回falseif (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//否则将当前线程置为获得写锁的线程,返回truesetExclusiveOwnerThread(current);return true;}

ReentrantReadWriteLock-NonfairSync非公平锁的写锁始终阻塞

        final boolean writerShouldBlock() {return false; // writers can always barge}

读锁的释放

释放锁要做的就是更改AQS的状态值以及唤醒队列中的等待线程来继续获取锁。
ReentrantReadWriteLock-ReadLock

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

AbstractQueuedSynchronizer

/*** Releases in shared mode.  Implemented by unblocking one or more* threads if {@link #tryReleaseShared} returns true.*/public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}

ReentrantReadWriteLock-Sync

 protected final boolean tryReleaseShared(int unused) {//得到调用unlock的线程Thread current = Thread.currentThread();//如果是第一个获得读锁的线程if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;}//否则,是HoldCounter中计数-1else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;if (count <= 1) {readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}//死循环for (;;) {int c = getState();//释放一把读锁int nextc = c - SHARED_UNIT;//如果CAS更新状态成功,返回读锁是否等于0;失败的话,则重试if (compareAndSetState(c, nextc))// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0;}}

第一步是更新firstReader或HoldCounter的计数,接下来进入死循环,尝试更新AQS的状态,一旦更新成功,则返回;否则,则重试。

AbstractQueuedSynchronizer

/*** Release action for shared mode -- signals successor and ensures* propagation. (Note: For exclusive mode, release just amounts* to calling unparkSuccessor of head if it needs signal.)*/private void doReleaseShared() {/** Ensure that a release propagates, even if there are other* in-progress acquires/releases.  This proceeds in the usual* way of trying to unparkSuccessor of head if it needs* signal. But if it does not, status is set to PROPAGATE to* ensure that upon release, propagation continues.* Additionally, we must loop in case a new node is added* while we are doing this. Also, unlike other uses of* unparkSuccessor, we need to know if CAS to reset status* fails, if so rechecking.*/for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;            // loop to recheck casesunparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;                // loop on failed CAS}if (h == head)                   // loop if head changedbreak;}}

写锁的释放

ReentrantReadWriteLock-WriteLock

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

AbstractQueuedSynchronizer

    /*** Releases in exclusive mode.  Implemented by unblocking one or* more threads if {@link #tryRelease} returns true.* This method can be used to implement method {@link Lock#unlock}.*/public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

tryRelease尝试释放锁,一旦释放成功了,那么如果等待队列中有线程再等待,那么调用unparkSuccessor将下一个线程解除挂起

ReentrantReadWriteLock-Sync

/** Note that tryRelease and tryAcquire can be called by* Conditions. So it is possible that their arguments contain* both read and write holds that are all released during a* condition wait and re-established in tryAcquire.*/protected final boolean tryRelease(int releases) {//如果没有线程持有写锁,但是仍要释放,抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;//如果没有写锁了,那么将AQS的线程置为nullif (free)setExclusiveOwnerThread(null);//更新状态setState(nextc);return free;}protected final boolean isHeldExclusively() {// While we must in general read state before owner,// we don't need to do so to check if current thread is ownerreturn getExclusiveOwnerThread() == Thread.currentThread();}

ReadWriteLock1.8源码相关推荐

  1. 查看Hotspot源码,查看java各个版本源码的网站,如何查看jdk源码

    java开发必知必会之看源码,而看源码的第一步则是找到源码

  2. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  3. liunx上mysql源码安装mysql,搞定linux上MySQL编程(一):linux上源码安装MySQL

    [版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet,文章仅供学习交流,请勿用于商业用途] 1. 首先下载源码包: ftp://ftp.jaist.ac.jp/pub/m ...

  4. java调用clang编译的so_写Java这么久,JDK源码编译过没?编译JDK源码踩坑纪实

    好奇害死羊 很多小伙伴们做Java开发,天天写Java代码,肯定离不开Java基础环境:JDK,毕竟我们写好的Java代码也是跑在JVM虚拟机上. 一般来说,我们学Java之前,第一步就是安装JDK环 ...

  5. Go 源码里的这些 //go: 指令,go:linkname 你知道吗?

    原文地址: Go 源码里的这些 //go: 指令,你知道吗? 一文解惑 //go:linkname 指令

  6. 超详细中文预训练模型ERNIE使用指南-源码

    作者 | 高开远,上海交通大学,自然语言处理研究方向 最近在工作上处理的都是中文语料,也尝试了一些最近放出来的预训练模型(ERNIE,BERT-CHINESE,WWM-BERT-CHINESE),比对 ...

  7. 谷歌BERT预训练源码解析(二):模型构建

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/weixin_39470744/arti ...

  8. pytorch源码解析:Python层 pytorchmodule源码

    尝试使用了pytorch,相比其他深度学习框架,pytorch显得简洁易懂.花时间读了部分源码,主要结合简单例子带着问题阅读,不涉及源码中C拓展库的实现. 一个简单例子 实现单层softmax二分类, ...

  9. Bert系列(二)——源码解读之模型主体

    本篇文章主要是解读模型主体代码modeling.py.在阅读这篇文章之前希望读者们对bert的相关理论有一定的了解,尤其是transformer的结构原理,网上的资料很多,本文内容对原理部分就不做过多 ...

最新文章

  1. mysql通用分页_MySQL海量数据的通用存储过程分页代码
  2. 电脑如何下载python3-python3可以在哪里下载
  3. 文本框 清空_VBA代码中利用文本框,完成人机对话过程
  4. 在Ubuntu中安装PHP,MySQL,Nginx和phpMyAdmin
  5. 拒绝offer的理由_接受拒绝的3大理由
  6. 精神独立,才是一个人最大的底气
  7. 观察者-学历差距造成的差距有多大
  8. 火山引擎对外开放推荐算法等字节跳动核心技术
  9. MCU——矩阵键盘扫描问题记录
  10. Java金融计算机计算irr_手把手教你使用金融计算器
  11. Linux软件源镜像修改
  12. —— GPS测量原理及应用复习-6 ——
  13. 解决分类中样本分布不平衡问题
  14. APP打包后上传遇到ERROR ITMS-90096解决办法
  15. javaScript 琐碎
  16. mysql级联更新_Mysql实现级联操作(级联更新、级联删除)(转)
  17. BigDecimal的8种round舍入模式
  18. 台式机win10系统能连接上wifi,但是不能上网,终于解决了
  19. 静态生存期和动态生存期
  20. 2021年过氧化工艺模拟试题及过氧化工艺证考试

热门文章

  1. 【零基础学Java】—Collections集合工具类(四十二)
  2. gels imagej 图片处理_如何用ImageJ分析运动细胞?
  3. 美股涨跌幅限制是多少?
  4. 老公年收入百万,却不愿拿出二十万帮我弟弟买房子,我该离婚吗?
  5. 二线城市,存款多少可以退休?
  6. 为什么要多读书?多看书?
  7. 远洋渔船一次出海好几年,生活枯燥,那你在船上最开心的事是啥?
  8. 一个女人在公司做领导是如何在4年内做到年薪200万的?
  9. 如何区分PLC输入是源型输入还是漏型输入?
  10. C++低级程序设计支持规则