解析ReentrantLock实现原理
在Java中通常实现锁有两种方式,一种是synchronized关键字,另一种是Lock(Lock的实现主要有ReentrantLock、ReadLock和WriteLock)。synchronized是基于JVM层面实现的,而Lock是基于JDK层面实现的。synchronized是基于Monitor实现的,ReentrantLock是基于AQS实现的,AQS的基础又是CAS。
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。
在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入,在JDK1.5之后synchronized引入了偏向锁,轻量级锁和重量级锁,从而大大的提高了synchronized的性能。
对于使用者的直观体验上Lock是比较复杂的,需要lock和realse,如果忘记释放锁就会产生死锁的问题,所以,通常需要在finally中进行锁的释放。但是synchronized的使用十分简单,只需要对自己的方法或者关注的同步对象或类使用synchronized关键字即可。但是对于锁的粒度控制比较粗,同时对于实现一些锁的状态的转移比较困难。
Locks包结构
Lock的实现主要有ReentrantLock、ReadLock和WriteLock,ReentrantLock类在java.util.concurrent.locks包中,它的上一级的包java.util.concurrent主要是常用的并发控制类。

下面是ReentrantLock的UML图,从图中可以看出,ReentrantLock实现Lock接口,在ReentrantLock中Sync类引用了AbstractQueuedSynchronizer的子类,所有的同步操作都是依靠AbstractQueuedSynchronizer(队列同步器)实现。
AbstractQueuedSynchronizer结构
ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatch、FutureTask、Semaphore、ReentrantLock等都有一个内部类是这个抽象类的子类。 由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点,Node里面有:
| 属 性 | 定 义 |
| Node SHARED = new Node() | 表示Node处于共享模式 |
| Node EXCLUSIVE = null | 表示Node处于独占模式 |
| int CANCELLED = 1 | 因为超时或者中断,Node被设置为取消状态,被取消的Node不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态,处于这种状态的Node会被踢出队列,被GC回收 |
| int SIGNAL = -1 | 表示这个Node的继任Node被阻塞了,到时需要通知它 |
| int CONDITION = -2 | 表示这个Node在条件队列中,因为等待某个条件而被阻塞 |
| int PROPAGATE = -3 | 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播 |
| int waitStatus | 0,新Node会处于这种状态 |
| Node prev | 队列中某个Node的前驱Node |
| Node next | 队列中某个Node的后继Node |
| Thread thread | 这个Node持有的线程,表示等待锁的线程 |
| Node nextWaiter | 表示下一个等待condition的Node |
AbstractQueuedSynchronizer中有其余的变量和方法:
| 属性/方法 | 含 义 |
| Thread exclusiveOwnerThread | 这个是AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者 |
| Node | 上面已经介绍过了,FIFO队列的基本单位 |
| Node head | FIFO队列中的头Node |
| Node tail | FIFO队列中的尾Node |
| int state | 同步状态,0表示未锁 |
| int getState() | 获取同步状态 |
| setState(int newState) | 设置同步状态 |
| boolean compareAndSetState(int expect, int update) | 利用CAS进行State的设置 |
| long spinForTimeoutThreshold = 1000L | 线程自旋等待的时间 |
| Node enq(final Node node) | 插入一个Node到FIFO队列中 |
| Node addWaiter(Node mode) | 为当前线程和指定模式创建并扩充一个等待队列 |
| void setHead(Node node) | 设置队列的头Node |
| void unparkSuccessor(Node node) | 如果存在的话,唤起Node持有的线程 |
| void doReleaseShared() | 共享模式下做释放锁的动作 |
| void cancelAcquire(Node node) | 取消正在进行的Node获取锁的尝试 |
| boolean shouldParkAfterFailedAcquire(Node pred, Node node) | 在尝试获取锁失败后是否应该禁用当前线程并等待 |
| void selfInterrupt() | 中断当前线程本身 |
| boolean parkAndCheckInterrupt() | 禁用当前线程进入等待状态并中断线程本身 |
| boolean acquireQueued(final Node node, int arg) | 队列中的线程获取锁 |
| tryAcquire(int arg) | 尝试获得锁(由AQS的子类实现它) |
| tryRelease(int arg) | 尝试释放锁(由AQS的子类实现它) |
| isHeldExclusively() | 是否独自持有锁 |
| acquire(int arg) | 获取锁 |
| release(int arg) | 释放锁 |
| compareAndSetHead(Node update) | 利用CAS设置头Node |
| compareAndSetTail(Node expect, Node update) | 利用CAS设置尾Node |
| compareAndSetWaitStatus(Node node, int expect, int update) | 利用CAS设置某个Node中的等待状态 |
上面列出了AQS中最主要的一些方法和属性。整个AQS是典型的模板模式的应用,设计得十分精巧,对于FIFO队列的各种操作在AQS中已经实现了,AQS的子类一般只需要重写tryAcquire(int arg)和tryRelease(int arg)两个方法即可。
ReentrantLock的lock实现
ReentrantLock中有一个抽象类Sync:
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
}
ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync。由于ReentrantLock我们用的比较多的是非公平锁,所以看下非公平锁是如何实现的。假设线程1调用了ReentrantLock的lock()方法,那么线程1将会独占锁,整个调用链十分简单:

第一个获取锁的线程就做了两件事情:
1、设置AbstractQueuedSynchronizer的state为1。
2、设置AbstractOwnableSynchronizer的thread为当前线程。
这两步做完之后就表示线程1独占了锁。然后线程2也要尝试获取同一个锁,在线程1没有释放锁的情况下必然是行不通的,所以线程2就要阻塞。那么,线程2如何被阻塞?看下线程2的方法调用链,这就比较复杂了:

代码流程如下:
ReentrantLock调用lock方法:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先线程2尝试利用CAS去判断state是不是0,是0就设置为1,当然这一步操作肯定是失败的,因为线程1已经将state设置成了1,所以第2行必定是false,因此线程2走第5行的acquire方法:
/**
* 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.
* tips:此处主要是处理没有获取到锁的线程
* tryAcquire:重新进行一次锁获取和进行锁重入的处理。
* addWaiter:将线程添加到等待队列中。
* acquireQueued:自旋获取锁。
* selfInterrupt:中断线程。
* 三个条件的关系为and,如果 acquireQueued返回true,那么线程被中断selfInterrupt会中断线程
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
先走第一个判断条件尝试获取一次锁,如果获取的结果为false即失败,走第二个判断条件添加FIFO等待队列。所以先看一下tryAcquire方法做了什么,这个方法最终调用到的是Sync的nonfairTryAcquire方法:(ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。)
AbstractQueuedSynchronizer为抽象方法,调用tryAcquire时,调用的为NonfairSync的tryAcquire。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1. 如果该锁未被任何线程占有,该锁能被当前线程获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2.若被占有,检查占有线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 3. 再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
具体初始化公平锁和非公平锁在ReentrantReadWriteLock的初始化代码中:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
由于state是volatile的,所以state对线程2具有可见性,线程2拿到最新的state,再次判断一下能否持有锁(可能线程1同步代码执行得比较快,这会儿已经释放了锁),不可以就返回false。
注意一下else if这段代码的作用是让某个线程可以多次调用同一个ReentrantLock,每调用一次给state+1,由于某个线程已经持有了锁,所以这里不会有竞争,因此不需要利用CAS设置state(相当于一个偏向锁)。从这段代码可以看到,nextc每次加1,当nextc<0的时候抛出error,那么同一个锁最多能重入Integer.MAX_VALUE次,也就是2147483647。
然后就走到if的第二个判断里面了,先走AQS的addWaiter方法:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
先创建一个当前线程的Node,模式为独占模式(因为传入的mode是一个NULL),再判断一下队列上有没有节点,没有就创建一个队列,因此走enq方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
Node h = new Node(); // Dummy header
h.next = node;
node.prev = h;
if (compareAndSetHead(h)) {
tail = node;
return h;
}
}
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这个方法其实画一张图应该比较好理解,形成一个队列之后应该是这样的:

每一步都用图表示出来了,由于线程2所在的Node是第一个要等待的Node,因此FIFO队列上肯定没有内容,tail为null,走的就是第4行~第10行的代码逻辑。这里用了CAS设置头Node,当然有可能线程2设置头Node的时候CPU切换了,线程3已经把头Node设置好了形成了上图所示的一个队列,这时线程2再循环一次获取tail,由于tail是volatile的,所以对线程2可见,线程2看见tail不为null,就走到了13行的else里面去往尾Node后面添加自身。整个过程下来,形成了一个双向队列。最后走AQS的acquireQueued(node, 1):
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
此时再做判断,由于线程2是双向队列的真正的第一个Node(前面还有一个h),所以第5行~第10行再次判断一下线程2能不能获取锁(可能这段时间内线程1已经执行完了把锁释放了,state从1变为了0),如果还是不行,先调用AQS的shouldParkAfterFailedAcquire(p, node)方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int s = pred.waitStatus;
if (s < 0)
/*
* This node has already set status asking a release
* to signal it, so it can safely park
*/
return true;
if (s > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
else
/*
* Indicate that we need a signal, but don't park yet. Caller
* will need to retry to make sure it cannot acquire before
* parking.
*/
compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
return false;
}
这个waitStatus是h的waitStatus,很明显是0,所以此时把h的waitStatus设置为Noed.SIGNAL即-1并返回false。既然返回了false,上面的acquireQueued的11行if自然不成立,再走一次for循环,还是先尝试获取锁,不成功,继续走shouldParkAfterFailedAcquire,此时waitStatus为-1,小于0,走第三行的判断,返回true。然后走acquireQueued的11行if的第二个判断条件parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
最后一步,调用LockSupport的park方法阻塞住了当前的线程。至此,使用ReentrantLock让线程1独占锁、线程2进入FIFO队列并阻塞的完整流程已经整理出来了。
ReentrantLock的unlock实现
调用ReentrantLock的unlock方法:
public void unlock() {
sync.release(1);
}
走AQS的release:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
先调用Sync的tryRelease尝试释放锁:
protected final boolean tryRelease(int releases) {
//1. 同步状态减1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//2. 只有当同步状态为0时,锁成功被释放,返回true
free = true;
setExclusiveOwnerThread(null);
}
// 3. 锁未被完全释放,返回false
setState(c);
return free;
}
首先,只有当c==0的时候才会让free=true,这和上面一个线程多次调用lock方法累加state是对应的,调用了多少次的lock()方法自然必须调用同样次数的unlock()方法才行,这样才把一个锁给全部解开。
当一条线程对同一个ReentrantLock全部解锁之后,AQS的state自然就是0了,AbstractOwnableSynchronizer的exclusiveOwnerThread将被设置为null,这样就表示没有线程占有锁,方法返回true。代码继续往下走,上面的release方法的第四行,h不为null成立,h的waitStatus为-1,不等于0也成立,所以走第5行的unparkSuccessor方法:
private void unparkSuccessor(Node node) {
/*
* Try to clear status in anticipation of signalling. It is
* OK if this fails or if status is changed by waiting thread.
*/
compareAndSetWaitStatus(node, Node.SIGNAL, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
s即h的下一个Node,这个Node里面的线程就是线程2,由于这个Node不等于null,所以走21行,线程2被unPark了,得以运行。有一个很重要的问题是:锁被解了怎样保证整个FIFO队列减少一个Node呢?这是一个很巧妙的设计,又回到了AQS的acquireQueued方法了:
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
被阻塞的线程2是被阻塞在第12行,注意这里并没有return语句,也就是说,阻塞完成线程2依然会进行for循环。然后,阻塞完成了,线程2所在的Node的前驱Node是p,线程2尝试tryAcquire,成功,然后线程2就成为了head节点了,把p的next设置为null,这样原头Node里面的所有对象都不指向任何块内存空间,h属于栈内存的内容,方法结束被自动回收,这样随着方法的调用完毕,原头Node也没有任何的引用指向它了,这样它就被GC自动回收了。此时,遇到一个return语句,acquireQueued方法结束,后面的Node也是一样的原理。
这里有一个细节,看一下setHead方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead方法里面的前驱Node是Null,也没有线程,那么为什么不用一个在等待的线程作为Head Node呢?
因为一个线程随时有可能因为中断而取消,而取消的话,Node自然就要被GC了,那GC前必然要把头Node的后继Node变为一个新的头而且要应对多种情况,这样就很麻烦。用一个没有thread的Node作为头,相当于起了一个引导作用,因为head没有线程,自然也不会被取消。
再看一下上面unparkSuccessor的14行~20行,就是为了防止head的下一个node被取消的情况,这样,就从尾到头遍历,找出离head最近的一个node,对这个node进行unPark操作。
compareAndSetState锁获取
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a <tt>volatile</tt> read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that the actual
* value was not equal to the expected value.
*
* tips: 1.compareAndSetState的实现主要是通过Unsafe类实现的。
* 2.之所以命名为Unsafe,是因为这个类对于JVM来说是不安全的,我们平时也是使用不了这个类的。
* 3.Unsafe类内封装了一些可以直接操作指定内存位置的接口,是不是感觉和C有点像了?
* 4.Unsafe类封装了CAS操作,来达到乐观的锁的争抢的效果
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
接下来简单的看一下 compareAndSwapInt的实现:
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
一个native方法,以CAS的方式将制定字段设置为指定的值。这个方法可能是用java实现不了,只能依赖JVm底层的C代码实现。下面看看操作的stateOffset:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset; static {
try {
//这个方法很有意思,主要的意思是获取AbstractQueuedSynchronizer的state成员的偏移量
//通过这个偏移量来更新state成员,另外state是volatile的来保证可见性。
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next")); } catch (Exception ex) { throw new Error(ex); }
}
stateOffset 是AbstractQueuedSynchronizer内部定义的一个状态量,AbstractQueuedSynchronizer是线程的竞态条件,所以只要某一个线程CAS改变状态成功,同时在没有释放的情况下,其他线程必然失败。
对于竞争成功的线程会调用 setExclusiveOwnerThread方法:
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread; /**
* Sets the thread that currently owns exclusive access. A
* <tt>null</tt> argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* <tt>volatile</tt> field accesses.
*/
protected final void setExclusiveOwnerThread(Thread t) {
exclusiveOwnerThread = t;
}
这个实现是比较简单的,只是获取当前线程的引用,令AbstractOwnableSynchronizer中的exclusiveOwnerThread引用到当前线程。竞争失败的线程,会调用acquire方法,这个方法也是ReentrantLock设计的精华之处(参考ReentrantLock的Lock实现)。
ReentrantLock其他方法的实现
如果能理解ReentrantLock的实现方式,那么你会发现ReentrantLock中其余一些方法的实现还是很简单的,从JDK API关于ReentrantLock方法的介绍这部分,举几个例子:
1、int getHoldCount()
//获取ReentrantLock的lock()方法被调用了几次,就是state的当前值
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
2、Thread getOwner()
//获取当前占有锁的线程,就是AbstractOwnableSynchronizer中exclusiveOwnerThread的值
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
3、Collection<Thread> getQueuedThreads()
//从尾到头遍历一下,添加进ArrayList中
public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}
4、int getQueuedLength()
//从尾到头遍历一下,累加n。当然这个方法和上面那个方法可能是不准确的,因为遍历的时候可能别的线程又往队列尾部添加了Node。
public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}
解析ReentrantLock实现原理的更多相关文章
- ReentrantLock实现原理深入探究
前言 这篇文章被归到Java基础分类中,其实真的一点都不基础.网上写ReentrantLock的使用.ReentrantLock和synchronized的区别的文章很多,研究ReentrantLoc ...
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- Nginx 解析PHP的原理 | CGI、FastCGI及php-fpm的关系
Nginx解析PHP的原理,CGI/FastCGI以及PHP-Fpm的关系. 一.PHP+Nginx应运而生的场景.随着互联网的发展,用户对此接受面广,数据流的增大使得Web端的运行承载压力日益增大, ...
- 【Java并发编程】15、ReentrantLock实现原理深入探究
原文已经写得非常详细了,直接把大神的文章转发过来了 https://www.cnblogs.com/xrq730/p/4979021.html 前言 这篇文章被归到Java基础分类中,其实真的一点都 ...
- Nginx解析PHP的原理 | CGI、FastCGI及php-fpm的关系
Nginx解析PHP的原理,CGI/FastCGI以及PHP-Fpm的关系. 一.PHP+Nginx应运而生的场景.随着互联网的发展,用户对此接受面广,数据流的增大使得Web端的运行承载压力日益增大, ...
- 互联网轻量级框架SSM-查缺补漏第七天(MyBatis的解析和运行原理)
第七章MyBatis的解析和运行原理 SqlSessionFactory是MyBatis的核心类之一,其最重要的功能就是提供创建MyBatis的核心借口SqlSession,所以要先创建SqlSess ...
- Atitit。Tree文件解析器的原理流程与设计实现 java c# php js
Atitit.Tree文件解析器的原理流程与设计实现 java c# php js 1. 解析原理与流程1 1.1. 判断目录 ,表示服 dirFlagChar = "└├─&quo ...
- Mybatis的解析和运行原理
Mybatis的解析和运行原理 Mybatis的运行过程大致分为两大步:第一步,读取配置文件缓存到Configuration对象,用以创建 SqlSessionFactory:第二步,SqlSessi ...
随机推荐
- 在MyEclipse使用Git新建分支,并上传分支---图文教程
1.选中项目,右键--->Team--->Switch To--->New Branch: 2.在弹出的窗口中,填写新建的分支名称,如下图 3.当前分支就会变成新建分支“test” ...
- [STM32F103]定时器PWM输入
typedef struct { uint16_t TIM_OCMode; //PWM模式1或者模式2 uint16_t TIM_OutputState; //输出使能 OR失能 uint16_t ...
- JS 实现右下角弹窗
<!DOCTYPE HTML> <head> <title>JS实现右下角弹窗</title> <meta http-equiv="co ...
- mybatis入门篇:Mybatis高级查询
1.ResultMap的association与collection association与collection功能类似,区别是一对一与一对多,这里以association为例. 首先说明一下需求: ...
- gogs windows
首先安装 git,然后下载 gogs. 在gogs 文件夹位置 启动. gogs.exe web 打开浏览器,输入 127.0.0.1:3000 ,安装 gogs,注意数据库选择,仓库根目录,管理员帐 ...
- djangorestframework-jwt自带的认证视图进行用户登录验证源代码学习
Django REST framework JWT djangorestframework-jwt自带的认证视图进行用户登录验证源代码学习 SECRET_KEY = '1)q(f8jrz^edwtr2 ...
- java和.net连接数据库的方法
.net连接数据库的方法 App.config <connectionStrings> <add name="" connectionString="D ...
- nginx出现404和403错误
配置文件的路径usr/local/nginx/conf/nginx.conf
- __name__ __main__ 作用
1 __name__ 在自己文件下面执行 就显示__main__ 2 如果__name__是在其他文件里面,然后通过当前文件调用到其他文件执行,就会显示的当前文件路劲的文件名结果: if __name ...
- pyqt5-数据库加载错误解决
1.无法连接postgresql 直接在pycharm上安装pyqt5没有QT这个文件夹, 在ancanda中装好使用. 切换加载环境,或者将第二个ptqt5拷贝替换第一个环境中的pyqt5
