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. jmeter用Stepping Thread Group 递增并发数

    jmeter安装插件Stepping Thread Group 如图所示设置的时候,本以为是每2秒 按 1 2 3 4 递增的,总共请求应该是10个,可是运行后却请求了几十个. 这个是有关线程数是否就 ...

  2. WorkerServices构建Windows服务

    .NET Core 3.1和WorkerServices构建Windows服务 介绍 ASP.NET Core 3增加了一个非常有意思的功能Worker Service.他是一个ASP.NET Cor ...

  3. <BitMap>大名鼎鼎的bitmap算法

    BitMap 抛砖引玉 首先,我们思考一个问题:如何在3亿个整数(0~2亿)中判断某一个数是否存在?现在只有一台机器,内存只有500M 这个问题像不像我们之前提到过的一个在0-10个数中,判断某一个数 ...

  4. P1073 多选题常见计分法

    P1073 多选题常见计分法 转跳点:

  5. upper_bound()和low_bound函数的基本使用和理解(转载,已获博主授权)

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/sdz20172133/article/details/80101838 前提:一个非降序列!!!!! ...

  6. 留学生Essay写作清晰简洁8大原则

    英语Essay写作中国留学生需要掌握的一个技能,一篇文笔简洁优雅的Essay对于提高分数会有很大帮助.但目前的情况是,很多人并没有受过专门的学术Essay写作训练,在写Essay时经常会出现各种各样的 ...

  7. Oracle 中启用 scott 用户 的方法

    解锁scott: SQL> alter user scott account unlock 修改密码: SQL> alter user scott identified by tiger ...

  8. Consul 简介及集群安装

    简介 Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册.服务发现和配置管理的功能. Consul的功能都很实用,其中包括:服务注册/发现.健康检查.Key/Value ...

  9. Java 布尔运算

    章节 Java 基础 Java 简介 Java 环境搭建 Java 基本语法 Java 注释 Java 变量 Java 数据类型 Java 字符串 Java 类型转换 Java 运算符 Java 字符 ...

  10. UVA - 536 Tree Recovery (二叉树重建)

    题意:已知先序中序,输出后序. #pragma comment(linker, "/STACK:102400000, 102400000") #include<cstdio& ...