Java基础知识02- 线程
多线程通信 :什么是多线程通信 ?怎么进行通信 ?
多线程通信就是多个线程同时操作同一个全局变量,但是操作的动作不同。
1. 线程安全:为什么出现?怎么解决?
当多个线程同时操作同一个共享的全局变量时,可能会受到其他线程干扰 发生数据冲突的问题;
解决:
一: sychronized 不能手动的开锁 关锁 ;
(1)sychronized代码块 (同步代码块)
(2)sychronized方法(同步函数)
(3)sychronized 静态 方法(静态同步函数(方法))
二 : 并发包lock可以手动开锁 关锁;
(1)ReentrantLock. 可重入锁,代码通过lock()方法获取锁,但必须调用unlock()方法释放锁
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock()
lock.unlock()
AtomicInteger
class Mythread extends Thread{
//private static volatile int count = 0 ;
private static AtomicInteger atomicInteger= new AtomicInteger(0);
public void run() {
for (int i = 0; i < 1000; i++) {
//count++;
atomicInteger.incrementAndGet();
}
//System.out.println(Thread.currentThread().getName()+":"+count);
System.out.println(Thread.currentThread().getName()+":"+atomicInteger);
}
}
在 java 程序中怎么保证多线程的运行安全?
(1)线程安全性问题体现在 -- > 线程的三个特性 :
* a) 原子性:一个操作要么不受任何因素的影响全部执行 要么不执行,
其实就是保证数据一致 这也是是线程安全的一部分(通过sychronized和lock,atomic 来保证)
* b) 可见性: 当多个线程同时访问一个变量时 一个线程修改了这个变量的值 ,
其他线程能够立即看到修改后的值(synchronized,volatile,AtomicInteger);
* c) 有序性:程序执行的顺序按照代码的先后顺序执行
(2)导致原因:
* 缓存导致的可见性问题
* 线程切换带来的原子性问题
* 编译优化带来的有序性问题
(3)解决办法:
* JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
* synchronized、volatile、LOCK,可以解决可见性问题
* Happens-Before 规则可以解决有序性问题
Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则.
二. 实现线程的方法有几种方式 ???
有4种:
1).继承Thread ,重写run方法
2). 实现Runnable接口,重写run方法
(可以避免由于java的单继承特性而带来的局限,适合多个线程去处理同一个资源的情况)
3).实现callable接口,重写call方法(有返回值 允许抛异常)
4). 使用线程池【减少创建新线程的时间,重复利用线程池中的线程,降低资源消耗,可以有返回值】
继承Thread类创建线程类
定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
public class TestThread extends Thread {
public void run() {
System.out.println("123");
}
========================================================
public static void main(String[] args){
Thread t = new TestThread();
t.start();
thread.sleep(1000);//让主线程沉睡
}
}
实现Runnable接口创建线程类
定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动该线程。
public class MyRunable implements Runnable {
Integer num=10;
public void run() {
num--;
System.out.println(Thread.currentThread().getName()+"+线程start+"+num);
}
}
========================================================
@SpringBootTest //
public class ThreadTest {
@Test //实现Runable接口创建线程
public void test2() throws InterruptedException {
MyRunable runable = new MyRunable();
new Thread(runable).start();
new Thread(runable).start();
//让主线程沉睡
Thread.sleep(1000);
}
}
通过Callable接口和Future创建线程
①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。
②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值
③、使用FutureTask对象作为Thread对象启动新线程。
④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。
public class MyCallable<I extends Number> implements Callable<Integer> {
Integer num=10;
@Override
public Integer call() throws Exception {
num--;
System.out.println(Thread.currentThread().getName()+"线程start");
Thread.sleep(1000);
return num;
}
}
========================================================
@SpringBootTest //
public class ThreadTest {
@Test //实现Callable接口 创建线程
public void test3() throws ExecutionException, InterruptedException {
MyCallable<Integer> myCallable = new MyCallable<>();
FutureTask<Integer> task1 = new FutureTask<>(myCallable);
FutureTask<Integer> task2 = new FutureTask<>(myCallable);
new Thread(task1).start();
new Thread(task2).start();
Integer num1 = task1.get();
Integer num2 = task2.get();
System.out.println(num1);
System.out.println(num2);
}
}
使用Executor框架来创建线程池
Executor框架是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。
@SpringBootTest //
public class ThreadTest {
@Test //利用线程池创建线程
public void test4() throws ExecutionException, InterruptedException {
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(3);
MyRunable runable = new MyRunable();
pool.submit(runable);
pool.submit(runable);
pool.submit(runable);
pool.submit(runable);
pool.submit(runable);
//关闭所有的线程
pool.shutdown();
}
}
三种方式比较:
Thread: 继承方式, 不建议使用, 因为Java是单继承的,
继承了Thread就没办法继承其它类了,不够灵活;
Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制;
Callable: Thread和Runnable都是重写的run()方法并且没有返回值,
Callable是重写的call()方法并且有返回值,并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行。
当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,
一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程。
Runnable和Callable的区别
Callable规定的方法是call(),Runnable规定的方法是run()
Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
call方法可以抛出异常,run方法不可以
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。
它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。
通过Future对象可以了解任务执行情况,可取消任务的执行,
还可获取执行结果。
Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,
所以三种实现方式本质上都是Runnable实现
二 多线程
34. 并行和并发有什么区别?
* 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
* 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
* 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
35. 进程和线程的区别?
进程是一个应用程序,可以包含多个线程
一个进程中的多个线程可以并发执行;
(1)进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。
(2)线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。
同一进程中的多个线程之间可以并发执行。
守护线程(deamon)是什么?
守护线程(即daemon thread),是个服务线程,准确地来说就是服务其他的线程。
线程分为两种 守护线程(后台线程) 和 用户线程(前台线程)
主线程或者jvm线程挂了,守护线程也会被停止掉,gc其实也是守护线程。
// 将线程标识为守护线程的方法
thread.setDaemon(true);
5. 什么是java内存模型 ?
java内存模型简称 jmm,定义了一个线程对另一个线程可见,共享变量存在主内存中,
每个线程都有自己本地的私有内存,当多个线程同时访问一个数据时,
可能本地内存没有及时刷新到主内存,所以就会发生线程安全的问题。
三 线程池
####1. 什么是线程池
线程池就是提前创建若干个线程,如果有任务需要处理,
线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。
####2. 线程池的好处。
第一:降低资源消耗。
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。
当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
但是,要做到合理利用
线程池,必须对其实现原理了如指掌。
#####3. Java中创建线程池有以下的方式,
1、使用ThreadPoolExecutor类
2、使用Executors类
(1)newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
(2)newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
(3)newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
(4)newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
#####4 . Executors各个方法的弊端
(1)newFixedThreadPool、newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的[内存],甚至OOM。
(2)newCachedThreadPool、newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
43. 线程池都有哪些状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:
44. 线程池中 submit()和 execute()方法有什么区别?
- 接收的参数不一样
- submit有返回值,而execute没有
- submit方便Exception处理
多线程的使用场景:
常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)
servlet多线程
FTP下载,多线程操作文件
数据库用到的多线程
分布式计算
tomcat,tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法
后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
自动作业处理:比如定期备份日志、定期备份数据库
异步处理:如发微博、记录日志
页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
数据库的数据分析(待分析的数据太多),数据迁移
多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成
desktop应用开发,一个费时的计算开个线程,前台加个进度条显示
swing编程
38. 线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
* 创建状态
在生成线程对象,并没有调用该对象的start方法,
这是线程处于创建状态。
* 就绪状态
当调用了线程对象的start方法之后,该线程就进入了就绪状态,
但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。
在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
* 运行状态
线程调度程序将处于就绪状态的线程设置为当前线程,
此时线程就进入了运行状态,开始运行run函数当中的代码。
* 阻塞状态
线程正在运行的时候,被暂停,通常是为了等待某个时间的发生
(比如说某项资源就绪)之后再继续运行。
sleep,suspend,wait等方法都可以导致线程阻塞。
* 死亡状态
如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。
对于已经死亡的线程,无法再使用start方法令其进入就绪
39. sleep() 和 wait() 有什么区别?
sleep():(让正在竞争锁的线程 进入休眠;未释放对象的机锁;)
方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,
让出执行机会给其他线程,等到休眠时间结束后,
线程进入就绪状态和其他线程一起竞争cpu的执行时间。
因为sleep() 是static静态的方法,他不能改变对象的机锁,
当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,
但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():(中断线程的执行,使线程等待;同时释放对象的机锁 ;)
wait()是Object类的方法,当一个线程执行到wait方法时,
它就进入到一个和该对象相关的等待池,同时释放对象的机锁,
使得其他线程能够访问,可以通过
notify,notifyAll方法来唤醒等待的线程
sleep与wait的区别
sleep在Thread类中,wait在Object类中
sleep不会释放锁,wait会释放锁
wait需要notify或者notifyAll来通知
40. notify()和 notifyAll()有什么区别?
* 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,
等待池中的线程不会去竞争该对象的锁。
* 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),
被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
也就是说,调用了notify后只要一个线程会由等待池进入锁池,
而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
* 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,
它还会留在锁池中,唯有线程再次调用 wait()方法,
它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,
直到执行完了 synchronized 代码块,它会释放掉该对象锁,
这时锁池中的线程会继续竞争该对象锁。
41. 线程的 run()和 start()有什么区别?
(1)调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();
直接调用 run() 方法,无法达到启动多线程的目的,
相当于主线程线性执行 Thread 对象的 run() 方法。
(2)一个线程对线的 start() 方法只能调用一次,
多次调用会抛出 java.lang.IllegalThreadStateException 异常;
run() 方法没有限制。
46. 多线程锁的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:
无状态锁;
偏向锁;
轻量级锁;
重量级锁状态;
这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁升级的图示过程:
47. 什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
此时称系统处于死锁状态,这些永远在互相等待的进程称为死锁进程。
是操作系统层面的一个错误,是进程死锁的简称。
48. 怎么防止死锁?
死锁的四个必要条件:
- 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;
- 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但又对自己获得的资源保持不放;
- 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放;
- 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系;
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
49. ThreadLocal 是什么?有哪些使用场景?
线程局部变量:是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。
Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。
但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
50.说一下 synchronized 底层实现原理?
synchronized可以保证方法或者代码块在运行时,
同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
* 普通同步方法,锁是当前实例对象
* 静态同步方法,锁是当前类的class对象
* 同步方法块,锁是括号里面的对象
51. synchronized 和 volatile 的区别是什么?
* (1)Volatile 关键字的作用 是保证变量在多个线程之间可见。
synchronized 的作用是锁定某个变量,确保线程安全。
* (2) volatile 只能修饰变量上
synchronized可以修饰 变量、方法、和类
* (3) volatile仅能实现变量的修改可见性,不能保证原子性;
synchronized则可以保证变量的修改可见性和原子性
* (4) volatile不会造成线程的阻塞;
synchronized可能会造成线程的阻塞。
* (5) volatile标记的变量不会被编译器优化;
synchronized标记的变量可以被编译器优化。
52. synchronized 和 Lock 有什么区别?
* 首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
* synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
* synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;
b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),
否则容易造成线程死锁;
* 用synchronized关键字的两个线程1和线程2,
如果当前线程1获得锁,线程2线程等待。
如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,
如果尝试获取不到锁,线程可以不用一直等待就结束了;
* synchronized的锁可重入、不可中断、非公平,
而Lock锁可重入、可判断、可公平(两者皆可);
* Lock锁适合大量同步的代码的同步问题,
synchronized锁适合代码少量的同步问题。
53. synchronized 和 ReentrantLock 区别是什么?
(1)synchronized是和if、else、for、while一样的关键字,
ReentrantLock是类,这是二者的本质区别。
(2)既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,
可以被继承、可以有方法、可以有各种各样的类变量:
* ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
* ReentrantLock可以获取各种锁的信息
* ReentrantLock可以灵活地实现多路通知
(3)二者的锁机制其实也是不一样的:
ReentrantLock底层调用的是Unsafe的park方法加锁,
synchronized操作的应该是对象头中mark word。
54. 说一下 atomic 的原理?
(1)Atomic包中的类基本的特性:
即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
(2)Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。
我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,
这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,
例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。
55. 分布式环境下,怎么保证线程安全。
1、时间戳:
分布式环境下是无法保证时序的,无论是通过远程接口同步调用或者异步消息,
很容易造成某些对时序性有要求的业务在高并发时产生错误。
对于这类问题,常用的方法是:
采用时间戳的方式。系统A给系统B发送变更时需要带上一个时间戳,
B通过与已经存在的时间戳进行变更,这样的方式比较简单,
关键在于调用的一方要有保证时间戳的时序有效性;
2、串行化:
有时候可以牺牲性能和扩张性来使用串行化,满足对数据一致性的需求。
3、数据库:
分布式的环境中共享资源不能通过Java里同步方法或者加锁来保证线程安全,
但是数据库是分布式各服务器的共享点,可以通过数据库的高可靠一致性来满足需求;
4、行锁:
5、统一触发途径:
当一个数据可能被多个触发点或多个业务涉及到,就有并发问题产生的隐患,
因此可以通过前期架构和业务世界尽量统一触发途径,
触发途径少减少了并发的可能性,也有利于并发问题的分析和判断。
56. 介绍下CAS(无锁技术)。
(1)CAS(compare and swap),即比较并替换,
实现并发算法时常用到的一种技术,CAS是通过unsafe类的compareAndSwap方法实现的;
(2)CAS的思想:三个参数,一个当前内存值V,旧的预期值A,
即将更新的值B,当且仅当预期值和内存值相同时将内存值修改为即将更新的值并返回,
否则什么都不做,返回false。
*缺点:
即就是ABA问题,也就是如果变量A初次读取的是A,并且在准备赋值的时候检查到它还是A,那能证明它没有被修改过吗?
很显然是不能的,如果这段时间它被修改为B然后又被修改为A,那么CAS就会认为它没有被修改过,
针对这种情况,java并发包提供了一个带有标记的原子引用类,AtomicStampedReference,
它可以通过看着呢会变量值的版本来保证CAS的正确性。
64. Object有哪些公用方法?
【Object是所有类的父类,任何类都默认继承Object()】
1、clone()创建并返回此对象的副本,
clone 保护方法,实现对象的浅复制,
只有实现了Cloneable接口才可以调用该方法,否则抛CloneNotSupportedException异常。
2、equals()判断,
equals 在Object中与==是一样的,子类一般需要重写该方。
3、getclass()返回object的运行类
4、hashcode()返回对象的哈希码值,
hashCode 该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。
这个方法在一些具有哈希功能的Collection中用到。
5、notify()唤醒正在等待对象监听器的单个进程
6、notifyAll()唤醒正在等待对象监听器的所有进程
7、wait():
导致当前正在执行的线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。
wait 使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,
也就是具有该对象的锁。 wait() 方法一直等待,直到获得锁或者被中断。
wait(long timeout) 设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生:
(1)、其他线程调用了该对象的notify方法。
(2)、其他线程调用了该对象的notifyAll方法。
(3)、其他线程调用了interrupt中断该线程。
(4)、时间间隔到了。
(5)、此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8、toString()返回此对象的字符串表示形式;
9、finalize()当垃圾收集确定不需要该对象时,垃圾回收器调用该方法
10 getClass final方法,获得运行时类型
11 notify 唤醒在该对象上等待的某个线程。
12 notifyAll 唤醒在该对象上等待的所有线程。
数据库连接池的工作机制:
服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接。
客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其标记为忙。
如果当前没有空闲连接,池驱动程序就新建一定数量的连接, 新建连接的数量有配置参数决定。
当使用的池连接调用完成后, 池驱动程序将此连接标记为空闲,其他调用就可以使用这个连接。