CountDownLatch能够让一个线程在等待其他线程全部完成各自任务后再执行。而CountDownLatch是通过计数器来实现的,计数器的初始值即为任务的总数。
举个例子,如,同学聚会结束回家,每个人都要回各自的家,此时计数器的初始值为参加聚会的总人数,而每个人都是一个线程,每个同学到家后,都需要调用countDown方法,对计数器减一,表示完成回家的任务,当所有同学都到家后,主线程才可以执行通知班长全部到家的任务。再比如,所编写的应用程序,希望等待启动框架的线程启动完毕后再执行。
可以看到内部只用了一个同步器Sync,继承自AQS,没有公平和非公平实现
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch的构造方法需要一个count参数,代表初始任务的数量,往后每当调用一次(CountDownLatch.countDown()方法,count都减一,当count减到0的时候,调用CountDownLatch.await()方法的线程就可以执行其任务。
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
//尝试获取共享锁
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
//尝试释放共享锁
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
//获取当前状态
int c = getState();
//如果为0,则释放失败
if (c == 0)
return false;
//释放一次
int nextc = c-1;
//CAS更新状态
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
除此之外,CountDownLatch还有以下几个方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//AQS中的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)//如果尝试获取锁失败
doAcquireSharedInterruptibly(arg);//加入AQS队列排队
}
tryAcquireShared
就是Sync中的tryAcquireShared,当且仅当同步状态为0时返回1,即获取锁成功,因此只要state大于0,即计数没有结束,调用await()的线程都要阻塞排队等待唤醒。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放锁
doReleaseShared();//释放锁成功就唤醒下一个AQS中排队的线程
return true;
}
return false;
}
tryReleaseShared就是上Sync中的方法。每次减一,减到0时返回true,否则返回false
1.await(long timeout,TimeUnit unit)
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
此方法在await()的基础上添加了时间限制,如果调用await的线程在到达该时间后,count仍然没有0,其会继续执行,不再等到count到0。
2. public long getCount()
public long getCount() {
return sync.getCount();
}
即获取该CountDownLatch当前的count值
package com.wml.test1.countdownlatch;
import java.util.concurrent.CountDownLatch;
/**
* @author Wang
* @date 2020/1/2317:27
*/
public class CountDownLatchTest {
//1.
private static CountDownLatch finishEat=new CountDownLatch(1);
//2.
private static CountDownLatch friends=new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
//3.
for (int i = 0; i <10; i++) {
new Thread(()->{
try {
//4.
finishEat.await();
System.out.println(Thread.currentThread().getName()+"正在回家");
//5.
friends.countDown();
System.out.println(Thread.currentThread().getName()+"到家啦");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"同学"+(i+1)).start();
}
System.out.println("聚会结束,同学们准备回家");
//6.
finishEat.countDown();
//7.
friends.await();
//8.
System.out.println("同学全部到家");
}
}
说明:
1.代码1创建一个结束聚餐的CountDownLatch,finishEat。初始值为1,代表正在聚餐,当主线程调用countDown()时(代码6),count值从1变为0,代表结束聚餐。这时,其他调用finishEat.await();
的线程(各个同学回家的线程)由阻塞状态变为执行。
2.代码2创建一个同学回家的CountDownLatch,friends。初始值为10,代表参加聚会的10个同学,即10个同学需要执行回家任务。回家的任务需要在代码4finishEat.await();
后执行。
3.代码3创建10个回家的线程,每个线程中都执行一次friends.countDown();
,让friends的count减一
4.代码7friends.await();
主线程阻塞等待friends的count变为0时开始执行下面的代码。
结果:
聚会结束,同学们准备回家
同学1正在回家
同学1到家啦
同学2正在回家
同学2到家啦
同学4正在回家
同学4到家啦
同学3正在回家
同学3到家啦
同学5正在回家
同学5到家啦
同学6正在回家
同学6到家啦
同学全部到家
1. CountDownLatch可以看做一个计数器,数量为任务总数,一个或多个线程需要等待所有任务执行完毕后,才可以执行。
2. 内部实现基于AQS的共享模式的锁,像读写锁中的读锁一样,最后介绍的SemphoreTest 也是基于共享锁。因为其需要在获得锁后将所有等待线程都唤醒,而共享锁就可以在获得锁后向同步队列后传播唤醒等待中的线程,但互斥锁就不会传播唤醒。
3. await()方法要等待同步状态为0时才会被唤醒,即所有任务都执行完毕
4. countDown方法每次让同步状态减少1,减到0时表示释放完毕,即所有任务执行完,此时从await()方法返回,继续执行后面的任务。
Cyclic(循环的)Barrier(屏障),该工具做的事情是,当一个线程到达一个屏障(同步点/临界点)时会被阻塞,等待到最后一个线程到达该点后,被拦截阻塞的线程才可以继续执行。
比如:同学聚餐,不会是到一个就吃一个,而是到的人先等待,直到所有人都达到饭桌后,才开始吃饭。这里餐桌就类似barrier,每个同学都是一个线程,每个人到饭桌后都被阻塞,直到最后一个同学到达。
而因为CyclicBarrier是可循环的,当一组线程到达后,其仍然有效,可以继续下一组循环。
1.默认构造方法
public CyclicBarrier(int parties) {
this(parties, null);
}
传入的参数表示需要拦截的线程总数,每当一个线程调用CyclicBarrier.await()
方法后,会通知CyclicBarrier该线程已到达屏障。
另外一个构造函数多了一个action参数
2.带任务barrierAction的构造方法
该构造会在所有任务到达屏障后优先执行barrierAction
的线程任务
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
3.主要属性
ReentrantLock的解读可看java并发编程之ReentrantLock和读写锁ReentrantReadWriteLock
private static class Generation {
boolean broken = false;
}
//使用ReentrantLock重入锁
private final ReentrantLock lock = new ReentrantLock();
/**条件队列 */
private final Condition trip = lock.newCondition();
/**任务数 */
private final int parties;
/* 全部到达屏障后执行的任务 */
private final Runnable barrierCommand;
/** 当前代*/
private Generation generation = new Generation();
/**
*当前代在等待的线程数
*/
private int count;
与CountDownLatch一样,CyclicBarrier也为await提供了超时时间设置。
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
线程阻塞直到到达超时时间。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
该方法等待所有的线程都到达指定的临界点。
但如果当前线程不是最后到达的线程,则出于线程调度目的将其禁用,并使其处于休眠状态,直到发生以下情况之一:
在await
方法中调用的dowait
方法
以下是dowait
源码:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取当前代
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
//代码1
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//代码2
int index = --count;
if (index == 0) { // index为0了,表示已经是最后一个任务到达屏障
boolean ranAction = false;
try {
//如果使用了带任务的构造,则先执行该任务
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//调用下代
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 代码3 非最后一个线程,其他线程都会到这
for (;;) {
try {
if (!timed)
//调用Conditon的await方法,阻塞线程,加入到条件队列
trip.await();
else if (nanos > 0L)//如果有超时,则进行超时阻塞
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//唤醒线程
lock.unlock();
}
}
说明:
代码1:如果当前线程被中断,会调用breakBarrier
方法并抛出InterruptedException
异常。
breakBarrier
方法如下:
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
1.breakBarrier
首先将该代的broken
设置为true
,代表其被打破。
2.generation
是CyclicBarrier
的一个静态内部类Generation
的对象,该对象代表屏障的当前代,可以实现循环等待。broken为true,则该循环结束。
3.同时将count
重置为parties
;
count
为计数器,parties
是CyclicBarrier
的构造参数,代表拦截的线程数
4.调用trip
的signalAll()
方法,将所有阻塞的线程唤醒
trip
是成员变量Condition
的对象,可见是使用Condition实现阻塞队列的。
代码2:
计数器count
减一并赋值给int变量index
,如果此时index值为0,则判断当前barrierCommand
是否为空,如果不为空,则执行该操作的run()
方法。
最后置ranAction
为true
,个人理解代表继续执行,并执行nextGeneration()
进入下一代(下一个循环)。
nextGeneration()代码如下:
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
在nextGeneration()
中将所有线程转移到AQS队列,之后会从await()方法的while循环中退出,继续尝试获取锁 ,重置计数器,并重新new了一个Generation。
而在finally中,如果barrierCommand执行出错,或其他原因,会执行breakBa
在这里插入代码片rrier()
方法。
代码3:
无限循环直到发生tripped,断开,中断或超时。
timed
代表是否开启超时时间,nanos
为设定的超时时间
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
返回当前有多少个线程阻塞等待在屏障上
boolean isBroken()
返回查询阻塞等待的线程是否被中断打破
package com.wml.test1.cyclicbarrier;
import java.util.concurrent.*;
/**
* @author Wang
* @date 2020/1/2317:44
*/
public class CyclicBarrierTest {
//代码1
private static CyclicBarrier barrier=new CyclicBarrier(6,()->{
System.out.println("同学们都到齐了,咱们开饭");
});
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(6,100,0L,TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
for (int i = 1; i <= 6; i++) {
pool.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + "到了");
//代码2
barrier.await();
System.out.println(Thread.currentThread().getName() + "开吃");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
}
}
}
说明:
该例子为同学聚餐,每到一个同学便阻塞自己,当所有同学都到齐后,输出“开饭”,然后唤醒所有同学,执行“吃饭”
代码1:构造一个CyclicBarrier 屏障,拦截线程数为6,在进入下一代前执行System.out.println("同学们都到齐了,咱们开饭");
这一barrierCommand
,即当前代结束(所有线程都到达屏障时或其他原因)后优先执行该操作。
代码2:
每个线程到达屏障后执行await
方法,直到所有线程都到达,await中会执行唤醒所有阻塞线程的方法,最后才会执行后面的代码。
结果:
pool-1-thread-1到了
pool-1-thread-2到了
pool-1-thread-3到了
pool-1-thread-4到了
pool-1-thread-5到了
pool-1-thread-6到了
同学们都到齐了,咱们开饭
pool-1-thread-6开吃
pool-1-thread-2开吃
pool-1-thread-3开吃
pool-1-thread-5开吃
pool-1-thread-4开吃
pool-1-thread-1开吃
1.CountDownLatch技术器只能使用一次,CyclicBarrier的可以循环使用(基于代的模式,最后一个线程到达时会调用nextGeneration(),唤醒线程并创建新代,重新初始化count为parties)。
2.CountDownLatch是一个线程等待所有线程完成任务后才会执行自己的任务(显示调用countDown),是阻塞工作线程。CyclicBarrier是所有线程到达某个屏障(临界点)后互相等待,直到所有线程都到达后再共同进行下面的任务,到达后自动唤醒等待线程。
3.CountDownLatch调用countDown方法可以继续执行后面的,只是调用await方法的线程会阻塞等待直到所有线程都调用countDown;CyclicBarrier是所有的线程调用await进行自阻塞,等到所有线程都调用一次await后一起继续执行后面操作。
4.CyclicBarrier可以在所有线程都到达屏障后执行barrierAction操作,完成复杂的逻辑场景。
Semaphore即信号量,是用来控制同时访问共享资源的线程数量,或同时执行某个指定操作的数量。比如可以用来实现数据库的连接池、限流等。
如数据库的连接数为20个,这时只能有20个线程同时获取数据量连接,再多的线程只能阻塞等待,当一个线程归还连接后,阻塞的线程才能继续获取。
类结构如下:
可见其底层也是使用AQS实现的,同步器支持公平和非公平模式
Semaphore的构造方法:
public Semaphore(int permits, boolean fair) {
//fair为true为公平实现,为false是非公平实现
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//默认是非公平实现
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
传入一个整型permits,代表可用的许可证数量。操作时必须先获取许可证,才能继续操作,当操作完成后需释放许可证,其余没有获得许可证的便阻塞等待,直到有线程释放了许可证。
线程使用Semaphore的acquire()方法获取许可证,使用release()方法释放许可证,使用tryAcquire()方法尝试获取许可证.
几个方法都比较简单,一些方法使用了AQS中的,可以先看一下AQS的文章https://blog.csdn.net/weixin_43696529/article/details/104129483
public void acquire() throws InterruptedException {
//默认使用可中断方式获取许可,如果尝试获取失败,会进入AQS的队列中排队。
sync.acquireSharedInterruptibly(1);
}
//AbstractQueuedSynchronizer#acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//这里的实现在Semaphore的sync中
if (tryAcquireShared(arg) < 0)//尝试获取许可
doAcquireSharedInterruptibly(arg);//
}
//Semaphore.Sync.tryAcquire()
public boolean tryAcquire(int permits) {
if (permits < 0) throw new IllegalArgumentException();
return sync.nonfairTryAcquireShared(permits) >= 0;
}
调用同步器sync的nonfairTryAcquireShared
//非公平模式下获取许可
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取当前可获得的许可
int available = getState();
//计算减去本次需要的许可后的剩余许可
int remaining = available - acquires;
if (remaining < 0 ||//如果许可小于0,则直接返回
compareAndSetState(available, remaining))//如果大于0,则CAS更新许可后返回
return remaining;
}
}
因此返回负数时,表示获取失败;返回正数表示获取成功;
如果获取失败,则调用doAcquireSharedInterruptibly
加入AQS自旋获取,具体过程参考AQS一文https://blog.csdn.net/weixin_43696529/article/details/104129483的AQS中共享模式获取锁的介绍
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//尝试释放
doReleaseShared();//如果释放成功,则唤醒下一个节点,具体也参考AQS一文
return true;
}
return false;
}
尝试释放许可(共享模式)
protected final boolean tryReleaseShared(int releases) {
for (;;) {//自旋
//获取当前同步状态
int current = getState();
//获取释放后的同步状态
int next = current + releases;
//溢出
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//CAS更新同步状态
if (compareAndSetState(current, next))
return true;
}
}
protected int tryAcquireShared(int acquires) {
for (;;) {
//首先判断有同步队列有没有线程在等待,如果有则返回-1,获取失败
if (hasQueuedPredecessors())
return -1;
//如果没有等待,则计算获取许可后的剩余许可,并CAS更新之
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
其他方法:
acquireUninterruptibly
:非中断获取,获取失败也进入AQS队列
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
tryAcquire
:直接调用非公平模式的获取,不调用acquire,因此获取成功或失败都直接返回,不需要排队
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
另外acquire和release都支持获取和释放n个许可。
intavailablePermits() //返回此信号量中当前可用的许可证数。
intgetQueueLength() //返回正在等待获取许可证的线程数。
booleanhasQueuedThreads()//是否有线程正在等待获取许可证。
void reducePermits( int reduction) //减少 reduction 个许可证
Collection getQueuedThreads() //返回所有等待获取许可证的线程集合
package com.wml.test1.semphore;
import java.sql.Connection;
import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.*;
/**
* @author Wang
* @date 2020/1/2416:31
*/
public class SemphoreTest {
private final static int POOL_SIZE=10;
private final Semaphore connections;
//连接池
private static LinkedList<Integer>pool;
public SemphoreTest() {
pool=new LinkedList<>();
//初始化连接池
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(i);
}
this.connections = new Semaphore(POOL_SIZE);
}
//释放连接
public void release(Integer conn){
if(conn!=null) {
System.out.println("当前有"+connections.getQueueLength()+"个线程等待数据库连接!!"
+"可用连接数:"+connections.availablePermits());
synchronized (pool) {
pool.addLast(conn);
}
connections.release();
}
}
//获取连接
public Integer acquire() throws InterruptedException {
connections.acquire();
Integer conn;
synchronized (pool){
conn=pool.removeFirst();
}
return conn;
}
public static void main(String[] args) {
SemphoreTest dbPool=new SemphoreTest();
ExecutorService pool = new ThreadPoolExecutor(50,100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
//50个线程获取连接
for (int i = 0; i < 50; i++) {
pool.execute(()->{
try {
Integer conn=dbPool.acquire();
System.out.println(Thread.currentThread().getName()
+"--------获取数据库连接");
Thread.sleep(1000);
System.out.println("归还连接");
dbPool.release(conn);
} catch (InterruptedException e) {
}
});
}
}
}
结果:
pool-1-thread-1--------获取数据库连接
pool-1-thread-2--------获取数据库连接
pool-1-thread-3--------获取数据库连接
pool-1-thread-5--------获取数据库连接
pool-1-thread-4--------获取数据库连接
pool-1-thread-6--------获取数据库连接
pool-1-thread-7--------获取数据库连接
pool-1-thread-8--------获取数据库连接
pool-1-thread-9--------获取数据库连接
pool-1-thread-10--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-1--------获取数据库连接
归还连接
归还连接
归还连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-5--------获取数据库连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-3--------获取数据库连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-2--------获取数据库连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-4--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-7--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-6--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-9--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-8--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-10--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-1--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-5--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-4--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-2--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-3--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-7--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
归还连接
当前有0个线程等待数据库连接!!可用连接数:1
pool-1-thread-9--------获取数据库连接
pool-1-thread-6--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-8--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-10--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-1--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-5--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-4--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-2--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-3--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-7--------获取数据库连接
归还连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-9--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-8--------获取数据库连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-6--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-10--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-1--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-5--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-2--------获取数据库连接
归还连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-3--------获取数据库连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-7--------获取数据库连接
pool-1-thread-4--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-9--------获取数据库连接
归还连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-6--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-8--------获取数据库连接
当前有0个线程等待数据库连接!!可用连接数:0
pool-1-thread-10--------获取数据库连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:0
归还连接
当前有0个线程等待数据库连接!!可用连接数:1
归还连接
当前有0个线程等待数据库连接!!可用连接数:2
归还连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:3
当前有0个线程等待数据库连接!!可用连接数:3
归还连接
当前有0个线程等待数据库连接!!可用连接数:5
归还连接
当前有0个线程等待数据库连接!!可用连接数:6
归还连接
归还连接
当前有0个线程等待数据库连接!!可用连接数:7
当前有0个线程等待数据库连接!!可用连接数:8
归还连接
当前有0个线程等待数据库连接!!可用连接数:9