1.简介

并发中常用的ReentrantLock,是一种典型的排他锁,这类锁在同一时刻只允许一个线程进行访问,实际上将并行操作变成了串行操作。在并发量大的业务中,其整体效率、吞吐量不能满足实现的需要。而且实际的业务中一般情况是读多于写,多个线程读操作不会改变已经有的数据,不会有数据的一致性问题,而一个写操作就会改变数据,其他的的读操作就可能读到过期的数据。读写锁正是为了这种业务需求而产生的,读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

读写锁同时支持公平锁和非公平锁,默认实现是非公平锁,非公平锁的吞吐量高于公平锁。

读写锁也支持重入,读锁和写锁的最大重入次数是65535次,这是由int类型本身所能表达的范围区间所决定的。

读写锁能够实现锁降级,它按照先获取写锁、获取读锁再释放写锁的次序执行锁操作,而且写锁能够降级为读锁。

2.ReentrantReadWriteLock的类结构

从UML图可以看出ReentrantReadWriteLock中有多个内部类,这些内部类与ReentrantLock有些类似,都有Sync、NofairSync、FairSync,前者Sync继承于AQS,前者Sync是NofairSync、FairSync的父类。

与 ReentrantLock相比,虽然这3个静态内部类名字相同,但内部却有差异。ReentrantLock是排他锁,又可以说是一种写锁,这三个静态内部类只重写了AQS中的tryAcquire(int) 、tryRelease(int)这两个排他锁相关方法。而ReentrantReadWriteLock是读写锁,要实现排他锁和共享锁这两种锁,Sync、NofairSync、Fair不仅重写SynctryAcquire(int) 、tryRelease(int)方法,另外还重写了AQS中的tryAcquireShared(int)、 tryReleaseShared(int)这两个共享锁相关方法。

ReadLock是表示读锁的静态内部类,它主要委托NofairSync/FairSync中的共享锁相关方法实现的,如"void acquire(int)" "boolean release(int)"等(这两个方法是父类AQS的模板方法,模板方法再去调用自身重写的tryAcquire(int) 、tryRelease(int)方法)。

WriteLock是表示写锁的静态内部类,它主要委托NofairSync/FairSync中的排他锁相关方法实现的,如"void acquireShared(int)" "boolean releaseShared(int)"等。

HoldCounter和ThreadLocalHoldCounter又都是Sync的静态内部类,HoldCounter类的主要作用是记录获一个获取取到共享锁的读线程的重入次数,ThreadLocalHoldCounter继承于线程局部变量ThreadLocal,主要是为每个获取到共享锁的读线程单独维护一个HoldCounter。

Sync中几个值得注意的成员变量

    private transient ThreadLocalHoldCounter readHolds;

    private transient HoldCounter cachedHoldCounter;

    private transient Thread firstReader = null;

    private transient int firstReaderHoldCount;

readHolds表示当前线程持有的可重入读锁的数量。 仅在Sync的构造方法和readObject方法中初始化,每当线程的读锁重入计数减少至0时将其移除。

cachedHoldCounter表示最后一个成功获取读锁的线程的重入次数计数器。 在下一个要释放的线程是最后一个要获取的线程的常见情况下,这可以节省在ThreadLocalHoldCounter中查找的时间。

firstReader表示第一个获得读取锁定的线程。firstReader是唯一一个最后一次将读锁重入计数从0更改为1且此后没有释放读取锁的线程; 如果没有这样的线程,则firstReader为null。firstReader使得读取追踪适用于非竞争读锁的效率更高。

firstReaderHoldCount表示第一个成功获取读锁的线程的重入次数。

3.ReadWriteLock的一些方法与使用示例

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类还有一些使我们更容易并发编程的一些辅助方法,这些方法都直接委托Sync(或其子类NofairSync/FairSync)去实现。

    public int getReadLockCount() {
return sync.getReadLockCount();
} public boolean isWriteLocked() {
return sync.isWriteLocked();
} public boolean isWriteLockedByCurrentThread() {
return sync.isHeldExclusively();
} public int getWriteHoldCount() {
return sync.getWriteHoldCount();
} public int getReadHoldCount() {
return sync.getReadHoldCount();
}

getReadLockCount()返回成功获取读锁的线程数,即此锁持有的读取锁的数量。。

isWriteLocked()返回写锁是否被某个线程成功获取了。

isWriteLockedByCurrentThread()返回写锁是否被当前线程成功获取了。

getWriteHoldCount()返回当前线程重复获取写锁(重入)的次数。

getReadHoldCount()返加当前线程重复获取读锁(重入)的次数。

使用示例

下面的代码使用HashMap去缓存简称/全称对应关系,addName()方法是写操作,因此使用写锁,getFullName()方法是读操作,因而使用读锁。

package juc;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class WRDemo {
private final ReadWriteLock wrLock = new ReentrantReadWriteLock(false);
private final Lock readLock = wrLock.readLock();
private final Lock writeLock = wrLock.writeLock();
private final Map<String, String> cacheForShortName = new HashMap<>(); public boolean addName(String shortName, String fullName) {
final Lock wl = writeLock;
wl.lock();
try {
if (!cacheForShortName.containsKey(shortName)) {
cacheForShortName.put(shortName, fullName);
return true;
}
return false;
} finally {
wl.unlock();
}
} public String getFullName(String shortName) {
final Lock rl = readLock;
rl.lock();
try {
return cacheForShortName.get(shortName);
} finally {
rl.unlock();
}
} public static void main(String[] args) {
final WRDemo wrDemo = new WRDemo();
new Thread(() -> {
boolean flag = wrDemo.addName("bj", "Beijing");
System.out.println("添加 bj-Beijing:" + (flag ? "成功" : "失败")); }).start();
new Thread(() -> {
boolean flag = wrDemo.addName("sh", "Shanghai");
System.out.println("添加 sh-Shanghai:" + (flag ? "成功" : "失败")); }).start();
new Thread(() -> {
System.out.println( "上海"+wrDemo.getFullName("sh")); }).start();
new Thread(() -> {
System.out.println("上海"+ wrDemo.getFullName("sh")); }).start();
new Thread(() -> {
boolean flag = wrDemo.addName("bj", "Beijing");
System.out.println("第二次添加 bj-Beijing:" + (flag ? "成功" : "失败")); }).start();
}
}

简称缓存

4.取/存读写状态的设计理念

ReentrantLock使用AQS中的int类型的state成员变量来保存排他锁状态,重入一次使state自增1,每释放一次重入状态state自减1(以前的帖子)。ReentrantReadWriteLock的重入设计理念应该也是基于此。ReentrantLock只需要保存写锁状态,直接使用state成员变量就可以实现,关键在于如何同时保存ReentrantReadWriteLock的读锁状态与写锁状态。其实可以借鉴CPU的标志寄存器的设计理念,将一个变量"按位分割",不同的位范围表示不同的含义。ReentrantReadWriteLock就是将state的高16位表示读状态、低16位表示写状态(int类型4个字节,共32比特位)。

读取读写状态

利用位运算操作,可以分别将高16位和低16位的值取出来。

取低16位的写状态,只需要将state的高16位的所有位设为0即可,根据“0和(1或0)进行按位与操作结果都是0、(0或1)和1进行与操作结果均为其本身”的特点,可以使用按位与操作表达式"state&0x0000FFFF"实现,所以在Sync中exculusiveCount()方法体中有"c&EXCLUSIVE_MASK",而EXCLUSIVE_MASK等于0x0000FFFF。

取高16位的读状态,只需要将state的低16位的抹除即可,可将state无符号右移16位,所以Sync中的sharedCount方法体中有代码"c >>> SHARED_SHIFT"。

        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; }

写入读写状态

假设当前同步状态值为S, 由于低16位表示写锁状态,当写锁状态增加1时,只需要将state设为S+1即可,而高16位表示读锁状态,需要在state的第17位加1,即将state设为S+0x00010000.

因些在尝试获取写锁tryAcquire()方法中有代码“compareAndSetState(c, c + acquires)”,在尝试获取读锁tryAcquireShared方法中有代码“compareAndSetState(c, c + SHARED_UNIT))”

   protected final int tryAcquireShared(int unused) {
//....
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {//SHARED_UNIT=0x00010000 }
//....
}
protected final boolean tryAcquire(int acquires) {
//....省略
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))//acquires=1
return false;
//....省略
}

5.写锁的获取与释放

写锁的lock()方法实际调用Sync的父类AQS的acquire(int)方法,acquire(int)是模板方法,acquire(int)的主要逻辑在以前的帖子中分析过,我们重点关注被Sync重写的tryAcuqrie(int)方法。

    public void lock() {
sync.acquire(1);
} public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

尝试获取写锁

tryAcquire的基本逻辑:如果读锁被获取了或写锁被其他线程获取了,那么尝试获取写锁失败。如果在之前当前线程已经获取了写锁,增加重入次数,尝试获取写锁成功。若未有任何读锁、写锁被获取,则进行CAS更新state,
若更新成功,尝试获取写锁成功,返回true,若更新失败,尝试获取写锁失败,返回false.
    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.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);//写锁重入的次数
if (c != 0) {//当前锁至少持有1个读锁或1个写锁(任何情况下最多只能有一个写锁)
// (Note: if c != 0 and w == 0 then shared count != 0)
/**
* 因为 共享锁重入次数+排他锁重入次数 < state ,即 shareCout+ w<c,而又c>0 && w=0,
* 那么shareCount>0,当前读锁被某线程获取了,所以这里w=0表示当前读锁被某些线程获取了,
* 在读锁被获取了的情况下,不能获取写锁(只有在所有读锁、写锁均补充释放才能获取写锁),返回false。
*
* "current != getExclusiveOwnerThread()"为true表明,
* 前置条件"w==0"不成立,那么w>0,即写锁已经被某线程获取到了,
* 而"current != getExclusiveOwnerThread()"条件本身又表明,当前线程不是获取到写锁的线程
* 所以尝试获取写锁失败,返回false
*
* 综合起来说,当有读锁被某些线程成功获取或写锁被其他线程成功获取时,
* 尝试获取写锁失败,返回false。
*/
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) //超出了最大可重入次数,MAX_COUNT=0x0000FFFF
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//之前写锁已经被当前线程成功获取,重入次数自增
//尝试获取写锁成功,返回true
setState(c + acquires);
return true;
} //getState=0,当前锁不持有任何读锁、写锁 if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false; //cas更新失败,尝试获取写锁失败,返回false
//cas更新成功,设置当前线程为独占线程,尝试获取写锁成功,,返回false
setExclusiveOwnerThread(current);
return true;
}

这里与ReentrantLock不同的地方在于多了对读锁是否存在的判断。读锁的读取线程进行读取操作时,它不能主动感知写入操作,如果同时进行读写操作,读入的数据可能是被删除的数据或是过期的数据,读取时不能写入,所以在已有读锁的时候不能再去获取写锁。反过来也是一样,为保证数据的一致性,在有写锁的时候,它要阻塞其他所有的读写操作,不能再去获取任何读锁和写锁。

尝试释放写锁

tryRelease(int)尝试释放锁的基本逻辑和ReentrantLock几乎一样:将重入次数自减,当重入次数为0,将独占线程设为null,返回true,反之返回false.

        protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

6.读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

尝试获取读锁

tryAcquireShared(int)的主要逻辑:1.如果另一个线程持有写锁定,则失败。 2.否则,此线程符合请求获取读锁的条件,因而进一步询问于根据队列策略是否应该阻塞请求读锁的线程。 如果不是,请尝试按CAS更新state和更新相关重入次数的计数器。此步骤不检查重入获取,这将推迟到完整版本fullTryAcquireShared(Thread)方法,以避免在更典型的非重入情况下检查重入次数的计数。 3.如果第2步失败,表明第2步中应该阻塞读线程或重入次数饱和或CAS失败,此时进入进入fullTryAcquireShared()方法进行完整版的读锁获取重试。

    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); //所有线程获取读锁的总次数
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {//不应该阻塞读线程且CAS更新state成功
if (r == 0) { //之前读锁没有被任何线程获取
firstReader = current; //将第一个获取到读锁的线程设为当前线程
firstReaderHoldCount = 1; //第一个获取到读锁的线程重入次数初始为1.
} else if (firstReader == current) { //当前线程之前已经获取到读锁,且是第一个获取到读锁的线程
firstReaderHoldCount++;//重入次数自增1
} else {
//读锁被某些线程获取了,且当前线程不是第一个获取到读锁的线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))//当前线程不是最后获取到读锁的线程
//当线程本地变量readHolds中的属于当前线程的HoldCounter赋给cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)//当前线程是最后获取到读锁的线程且重入的次数是0
readHolds.set(rh);//将cachedHoldCounter设置为readHolds的当前线程变量
rh.count++;//重入次数自增
}
return 1;
}
//当应该阻塞读线程或重入次数饱和或CAS失败,进入完整版的读锁获取重试
return fullTryAcquireShared(current);
}

fullTryAcquireShared()完整版的获取读,可处理tryAcquireShared中CAS失败和被忽略的可重入读

    final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1; ////写锁被其他线程成功获取了,尝试获取读锁失败。
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) { //写锁未被任何线程获取,且不应该阻塞读线程
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {//确保读锁不会申请重入
// assert firstReaderHoldCount > 0;
} else {//当前线程不是第一个获取到读锁的线程
//更新cachedHoldCounter readHolds
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");
//下面的代码和tryAcquireShared(int)的代码逻辑类似
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;
}
}
}

fullTryAcquireShared与tryAcquireShared中的代码部分很相似,但由于不使tryAcquireShared与重试和延迟读取“hold counts”之间的交互复杂化,因此整体上更简单。

 

尝试释放读锁

tryReleaseShared(int),读锁的每次释放均减少读状态,减少的值是(1<<16)。与排他锁的最大不同在于,此处在更新state时,使用CAS更新,而不是无条件更新state,这是从线程安全的的角度考虑的因为可能有多个读线程同时释放读锁。

    protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
//如果当前线程是第一个读锁线程,将其重入次数自减,当重入次数为0时,firstReader赋空
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
//更新cachedHoldCounter readHolds,重入次数自减,当重入次数为0时,
// 将当前线程的HoldCounter从线程本地变量readHolds中移除
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 (;;) {
/**
* cas更新state,读锁重入次数减1,不能使用无条件更新state.
* 这里是从线程安全的的角度考虑的,因为可能有多个读线程同时释放读锁。
*/
int c = getState();
int nextc = c - SHARED_UNIT;
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;
}
}

读写锁ReentrantReadWriteLock源代码浅析的更多相关文章

  1. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  2. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  3. [图解Java]读写锁ReentrantReadWriteLock

    图解ReentrantReadWriteLock 如果之前使用过读写锁, 那么可以直接看本篇文章. 如果之前未使用过, 那么请配合我的另一篇文章一起看:[源码分析]读写锁ReentrantReadWr ...

  4. 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥

    介绍 DK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁:一个读锁,一个写锁.通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下, ...

  5. Java并发(十):读写锁ReentrantReadWriteLock

    先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...

  6. 轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理

    转载:https://blog.csdn.net/yanyan19880509/article/details/52435135 前言 前面介绍了java中排它锁,共享锁的底层实现机制,本篇再进一步, ...

  7. [源码分析]读写锁ReentrantReadWriteLock

    一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前 ...

  8. 【原创】读写锁ReentrantReadWriteLock原理分析(一)

    Java里面真正意义的锁并不多,其实真正的实现Lock接口的类就三个,ReentrantLock和ReentrantReadWriteLock的两个内部类(ReentrantReadWriteLock ...

  9. 线程高级篇-读写锁ReentrantReadWriteLock

    转载原文:http://blog.csdn.net/john8169/article/details/53228016 读写锁: 分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,这是有JVM自己控制的 ...

随机推荐

  1. linux(centos6.9)下使用yum安装mysql,及启动MySQL等

    查看系统自带的mysql版本:rpm -qa | grep mysql 卸载mysql:rpm -e mysql-libs-5.1.73-8.el6_8.x86_64 --nodeps 1. 安装my ...

  2. 编程练习 将一个字符串中的空格替换为 "%20"

    重点:字符串和元组一样, 是不可变对象. 所以将创建一个新的字符串对象,将改变后的字符加入到该新的对象里. 两种方法: 1.python的 replace函数 2.判断修改 def replace(a ...

  3. 安装数据库Typical path for xclock: /usr/X11R6/bin/xclock 错误问题

    [oracle@localhost database]$ ./runInstaller Starting Oracle Universal Installer... Checking Temp spa ...

  4. [笔记]ul>li>a做分布时, 让其居中显示效果

    结构: <div id="page"> <ul> <li><a href="#">首页</a>< ...

  5. Java提升四:Stream流

    1.Stream流的定义 Stream是Java中的一个接口.它的作用类似于迭代器,但其功能比迭代器强大,主要用于对数组和集合的操作. Stream中的流式思想:每一步只操作,不存储. 2.Strea ...

  6. MS SQLSERVER 自增ID列竟然会重复

    MS SQLSERVER 2008 R2 datacenter edition 自增的ID列,设为了主键. 从没遇到过的情况.

  7. ubuntu18.04下载yarn

    下载curl sudo apt-get update && sudo apt-get install curl 配置库 curl -sS https://dl.yarnpkg.com/ ...

  8. (八)微信小程序---获取定位信息chooseLocation

    微信小程序---获取定位信息  chooseLocation wxml <view bindtap="getlocalPath">{{localPath}}</v ...

  9. CentOS7 静默安装Oracle XE 11g

    Oracle XE简介 Oracle Database 11g 快捷版 (Oracle Database XE) 是一款基于 Oracle Database 11g 第 2 版代码库的小型入门级数据库 ...

  10. 浅谈arduino的bootloader

    在arduino的板子上,作为核心的avr单片机往往都会烧录一个bootloader,这个叫做bootloader的东东其实是arduino研发团队针对arduino板子开发的一小段代码,借助于这段代 ...