• 05. Java多线程机制


    一.并发编程的基础概念

    1.CPU核心数和线程数的关系

    六个核心数---->1:1 一个核心 就是一个线程 以前

    超线程技术---->(1:2) 六个核心数=12个线程 现在

    ARM32,ARM64,x86 x64 //安卓处理器

    2.CPU时间片轮转机制

    进程:操作系统管理的最小单元;

    线程:是CPU调度的最小单元;

    进程>线程: 一个进程至少一个线程

    如果一个进程,还有一个线程没有杀死,进程还存活(线程依附于进程);

    进程A{线程1 线程2 线程3 ...}

    进程B{线程1 线程2 线程3 ...}

    进程C{} 挂起

    早期: 一核一线程 随机执行A,B,C 轮转调度运行; 线程的状态必须是可执行的才会被调度

    操作系统调度执行...CPU...单核线程1 执行线程(可执行) CPU时间片轮转RR调度(Round-Robin)

    上下文切换 每次上下文切换会20000个时间周期 例如从线程5切换到6,将5的临时数据保存,获取6的数据

    3.并发和并行

    并行:指应用能够同时执行不同的任务, 比如有4条跑道,那么就能同时有4个选手并行; (同时进行)

    并发:应用能够交替执行不同的任务,并发要加个时间单位内吞吐量,例如一条跑道上10s内跑过几个选手; (交替进行)

    高并发的优点:

    1> 充分利用CPU的资源;

    2> 加快响应用户的时间;

    3> 可使代码模块化,异步化,简单化;

    4.高并发编程的注意事项

    1>线程间的安全性

    在同一个进程中的多线程是资源共享的,即可访问同一内存地址中的一个变量,如:每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的,若有多线程同时执行写操作,一般都需要考虑线程同步,否则影响线程安全;

    2>线程之间的死锁

    为解决线程之间的安全性引入了 Java 的锁机制,但可能产生 死锁 问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭,他们都需获得两个锁:共享刀和共享叉的锁。假如线程 A 获得了刀,而线程 B 获得了叉。线程 A 就会进入阻塞状态来等待获得叉,而线程 B 则阻塞来等待线程 A 所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生

    3>线程太多会将服务器资源耗尽导致死机当机

    线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及 CPU 的“过渡切换”,造成系统的死机,这时可使用资源池,例如数据库连接池,只要线程需要使用一个数据库连接,它就从池中取出一个,使用后再将它返回池中,资源池也称资源库;

    二. 进程和线程(Process & Thread)

    1.进程(process)(单项工程)

    是一个正在执行中的程序,每一个进程都有一个执行顺序,该顺序是一个执行路径,

    或者叫一个控制单元,它是操作系统分配资源的基本单位,拥有独立的内存空间与系统资源;

    2.线程(Thread)(工人)

    1)是进程内部一个独立的执行路径,控制着进程的执行;

    2)一个进程至少有一个线程,也可以包含多条线程;

    3)是进程中执行运算的最小单位,是系统独立调度和分派CPU的基本单位;

    4)真正在处理器上运行的是线程;

    3.线程和进程的关系:

    1. 资源和调度

    进程是程序运行资源分配的最小单位;线程是CPU调度(程序执行)的最小单位,必须依赖于进程而存在;

    线程有自己的栈和局部变量,多线程共享同一进程的地址空间(堆);

    1. 地址空间资源:

    不同进程的地址空间是相互独立的,有一个进程的崩溃后,不会对其他进程产生影响;

    线程只是进程中的一条执行路径,同一进程中的各线程共享同一地址空间,如果一条线程崩溃,会影响同个进程中的其他线程;

    1. 通信关系

    进程间的通信要用操作系统提供的进程间通信机制即IPC机制,

    而同一进程的各线程可直接读写全局变量通信,需要线程同步和互斥手段的辅助,以保证数据的一致性;

    1. 并发性

    多进程之间可以并发执行,多线程之间也可以并发执行,而且同一进程中的多个线程间也可并发执行;

    1. 系统开销:

    由于创建或撤销进程要分配或回收资源,所以线程切换的开销远比进程切换的开销小,速度快

    三. JAVA多线程基础

    1.多线程简介

    1>多线程是什么

    多线程是指在一个进程中创建多个线程,每个线程完成一个任务(如下载或请求网络)支持事务并发和多任务处理;

    2> 为什么要用线程

    1)多线程使程序的响应速度更快;

    2)程序设计更简单;

    3)提高资源的利用率,不独立拥有资源,减少系统时空开销;

    3> JAVA多线程机制

    JVM启动时会有一个进程java.exe,该进程中至少有一个线程负责java程序的执行,而且这个线程运行的代码存在于main方法中,该线程称为主线程,而JVM启动不止一个线程,还有负责垃圾回收机制的线程GC;(子线程与主线程交叉进行);

    4>线程使用步骤

    定义线程-->创建线程-->启动线程-->终止线程

    5> 守护线程

    可通过调用thread.setDaemon(true)将线程设置为Daemon(守护)线程,它为完成支持性工作的线程,因它主要被用作程序中后台调度以及支持性工作,如GC(垃圾回收)线程就是守护线程,当一个JVM中不存在非守护线程时,JVM将会退出程序,这时注意在JVM退出时,守护线程中的finally块并不一定会被执行,所以在构建守护线程时不能在finally块中执行关闭或清理资源的代码逻辑;

    main_thread work_thread Daemon_Thread

    MainActivity-------------|----------------------->|--------------->|

    6>Java 程序中的多线程

    一个 Java 程序从 main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上 Java 程序天生就是多线程程序,因为执行 main() 方法的是一个名称为 main 的线程。

    [6] Monitor Ctrl-Break //监控 Ctrl-Break 中断信号的

    [5] Attach Listener //内存 dump,线程 dump,类信息统计,获取系统属性等

    [4] Signal Dispatcher // 分发处理发送给 JVM 信号的线程

    [3] Finalizer // 调用对象 finalize 方法的线程

    [2] Reference Handler//清除 Reference 的线程

    [1] main //main 线程,用户程序入口

    2.线程的创建与开启

    在java中,所有线程对象,都必须是Thread类或者Thread类子类的实例,JAVA使用线程执行体来容纳任务代码

    1>创建线程常用的方式

    1) 继承Thread类,

    2) 实现Runnable接口;

    3) 通过Callable和Future接口创建线程;

    4) 使用Executor

    1) 继承Thread

    1)定义Thread类的子类,并重写该类的run()方法,run的方法体即线程需要完成的任务,故把run方法称为线程执行体;

    2)创建Thread子类的实例,即创建线程对象;

    3)调用线程对象的start()方法来启动该线程;

    *1.Thread.currentThread(),是Thread类的静态方法,该方法总是返回当前正在执行的线程对象;

    *2.getName():该方法返当前正在执行的线程的名称,在默认情况下主线程的名称为main,其他为Thread-n;

    *3.可以通过setName(String name)设置当前线程的名字; 主线程的线程执行体不是由run()来确定,而是由main()来定;

    *4.通过继承Thread类实现多线程的,每个线程都要创建不同子类对象故不能共享成员变量,线程的执行时抢占式的;

    2) 实现Runnable接口

    1)定义Runnable接口的实现类,并重写该接口的run方法,

    2)创建Runnable实现类的实例对象obj

    3)并以obj作为Thread的target来创建Thread类,该对象才是真正的线程对象;

    4)调用线程对象的start()方法来启动该线程; Thread2只是Runnable接口的子实现类, Thread为真正创建线程的对象;

    Thread 与 Runnable 的区别

    1) Thread 是对线程的抽象; 做事的角色.

    2) Runnable 是对任务的抽象; 要做的事情

    1. /**
    2. *类说明:新启线程的方式
    3. */
    4. public class NewThread {
    5. /*扩展自Thread类*/
    6. private static class UseThread extends Thread{
    7. @Override
    8. public void run() {
    9. super.run();
    10. // do my work;
    11. System.out.println("I am extends Thread");
    12. for (int i = 0; i < 5; i++) {
    13. System.out.println(Thread.currentThread().getName() + "执行" + i);
    14. }
    15. }
    16. }
    17. /*实现Runnable接口*/
    18. private static class UseRunnable implements Runnable{
    19. @Override
    20. public void run() {
    21. // do my work;
    22. System.out.println("I am implements Runnable");
    23. for (int i = 0; i < 5; i++) {
    24. System.out.println(Thread.currentThread().getName() + "执行" + i);
    25. }
    26. }
    27. }
    28. public static void main(String[] args) {
    29. UseThread useThread = new UseThread();
    30. useThread.start();
    31. UseRunnable useRunnable = new UseRunnable();
    32. Thread thread = new Thread(useRunnable);
    33. thread.start();
    34. }
    35. }

    运行结果:

    线程1和线程2输出的成员变量i是连续的,也就说明通过该方式创建的线程,可以使多线程共享线程类的实例变量,

    因为这里的多线程都是用了同一个target实例变量,但当你使用上述代码运行时,也会出现线程安全问题(资源上锁);

    1. I am extends Thread
    2. Thread-0执行0
    3. Thread-0执行1
    4. Thread-0执行2
    5. Thread-0执行3
    6. Thread-0执行4
    7. I am implements Runnable
    8. Thread-1执行0
    9. Thread-1执行1
    10. Thread-1执行2
    11. Thread-1执行3
    12. Thread-1执行4

    3) Future接口传入Callable

    Future接口并将Callable传入创建线程 是第二种的变种

    从JAVA5开始,Java提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法,

    介意作为线程执行体,call()比run()功能强大在:

    1>call()方法可以有返回值;

    2>call()方法可以声明抛出异常;

    由于Callable接口为JAVA新增接口,且不是Runnable接口的子接口,所以callable对象不能作为Thread的target,

    于是Java5提供了Future接口来代表Callable接口里的call()方法的返回值;

    3>在Future接口中定义如下几个公共方法来控制与它关联的Callable任务;

    *1.boolean cancel(boolean MayInterruptIfRunning) 取消Future里关联的Callable任务;

    *2.V get() 返回Callable任务里call()方法的返回值,该方法将在子线程执行完毕后返回task结果;

    *3.V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值,该方法让程序最多阻塞

    timeout 和 unit指定的时间,如果经过指定时间后,Callable任务依然没有返回值,将会抛出TimeoutException异常;

    *4.boolean isCanceled():如果Callable任务正常完成前被取消, 则返回true;

    *5.boolean isDone():判断任务是否已完成, 如果Callable任务已经完成,则返回true;

    创建及启动多线程步骤

    1)创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且有返回值,在创建Callable实现类的实例;

    2)使用FutureTask类包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;

    3)使用FutureTask对象作为Thread对象的target创建并启动新线程;

    4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值;

    在执行多个任务时,使用Java标准库提供的线程池是非常方便的。我们提交的任务只需要实现Runnable接口,就可让线程池去执行:

    1. class Task implements Runnable {
    2. public String result;
    3. public void run() {
    4. this.result = longTimeCalculation();
    5. }
    6. }

    Runnable 接口有个问题,它的方法没有返回值, 如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不变,所以,Java标准库还提供了一个callable 接口, 和 Runnable 接口比,它多了一个返回值;

    1. class Task implements Callable<String> {
    2. public String call() throws Exception {
    3. return longTimeCalculation();
    4. }
    5. }

    并且 callable 接口是一个泛型接口, 可以返回指定类型的结果;

    现在的问题是,如何获取异步执行的结果?如果仔细看 ExecutorService.submit() 方法,可以看到,它返回了一个Future类型, 一个 Future 类型的实例代表一个未来能获取结果的对象;

    1. ExecutorService executor = Executors.newFixedThreadPool(4);
    2. // 定义任务:
    3. Callable<String> task = new Task();
    4. // 提交任务并获得Future:
    5. Future<String> future = executor.submit(task);
    6. // 从Future获取异步执行返回的结果:
    7. String result = future.get(); // 可能阻塞

    当我们提交一个Callable 任务后,我们会同时获得一个Future对象,然后,我们在主线程某个时刻调用Future对象的get()方法,就可以获得异步执行的结果。在调用get()时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么get()会阻塞,直到任务完成后才返回结果。

    Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,所以FutureTask可以作为Thread类的Target,同时解决Callable对象不能作为Thread类的target这一问题;

    代码示例

    1. public void setThread() {
    2. //FutureTask 实现了Runnable与Future接口,call()返回值类型与创建FutureTask对象是<>中类型一致;
    3. FutureTask task= new FutureTask<>(new Callable() {
    4. @Override
    5. public Integer call() {
    6. int i = 0;
    7. for (; i < 10; i++)
    8. Log.d("Thread", Thread.currentThread().getName() + "循环变量i=>" + i);
    9. return i;
    10. }
    11. });
    12. //将task作为Thread 的target传入构造Thread线程对象
    13. new Thread(task).start();
    14. try {
    15. //打印结果为 子线程的返回值 10; 即必须等到子线程结束以后,才会有返回值
    16. Log.d("Thread", "子线程的返回值" + task.get());
    17. } catch (Exception e) {
    18. e.printStackTrace();
    19. }
    20. }

    4) 线程池实现Executor接口

    (java.util.concurrent.Executor接口)降低了创建线程和销毁线程时间开销和资源浪费:

    1. //返回的实际上是ExecutorService,而ExecutorService是Executor的子接口
    2. Executor threadPool = Executors.newCachedThreadPool();
    3. threadPool.execute(new Runnable() {
    4. @Override
    5. public void run() {
    6. Log.d("Thread", Thread.currentThread().getName() + "is running");
    7. }
    8. });

    5) 两种创建方式对比

    综上所述,JAVA中创建线程的三种方法可分为类:

    一类是继承了Thread类实现多线程,另一类是实现Runnable接口或Callable接口,线程池就是Executor封装管理;

    两类多线程实现方式的对比

    1)通过继承Thread类实现多线程:

    优点:实现简单且获取当前进程无需调用Thread.currentThread()方法,直接使用this即可获取当前进程;

    缺点:多个进程不能共享一份资源;

    2)通过实现Runnable接口或者Callable接口实现多线程:

    优点:

    *1.线程类只是实现了接口.还可以继承其他类;

    *2.多个线程可使用同一个target对象,适合多个线程处理同一份资源的情况;

    缺点:

    *1.通过这种方式实现多线程,相较于第一类方式,编程较复杂;

    *2.要访问当前线程,必须调用Thread.currentThread()方法;

    3)通过线程池创建线程 对线程的管理更加高效(详见Android-ThreadPool);

    综上:一般采用第二类方法实现多线程;

    2> 线程状态(生命周期)

    线程的生命周期包括五个状态(如下图所示)

    Java中线程的状态分为6种:

    1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

    2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,

    其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,

    获取CPU的使用权,此时处于就绪状态(ready)就绪状态的线程在获得CPU时间片后变为运行中状态(running)

    3. 阻塞(BLOCKED):表示线程阻塞于锁。

    4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。1

    5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

    6. 终止(TERMINATED):表示该线程已经执行完毕。

    3> sleep 与 wait有什么区别

    sleep是休眠, 等休眠时间一过,才有执行资格,注意只是又有资格,并不代表马上就会被执行,执行起来取决于系统调度;

    wait是等待,需要等待唤醒,唤醒后,才有执行的资格,注意只是又有资格,并不代表马上就会被执行,执行起来取决于系统调度;

    sleep是无条件休眠, 是线程的方法 不释放锁 不依赖同步方法

    wait是某些原因与条件需要等待(例如资源不满足); 是object的方法 释放锁 依赖同步方法

    4> 等待唤醒机制

    wait(); 等待/冻结 :可以将线程冻结,释放CPU执行资格,释放CPU执行权,并把此线程临时存储到线程池

    notify(); 唤醒线程池里面 任意一个线程,没有顺序;

    notifyAll(); 唤醒线程池里面,全部的线程;

    使用等待唤醒注意事项:

    1.使用来wait();冻结,就必须要被其他方notify();,否则一直wait()冻结,所以等待与唤醒是配合一起使用的;

    2.wait(); notify(); notifyAll();等方法必须被synchronized(锁) {包裹起来},必须要有同步锁🔒,否则毫无意义;

    3.必须持有同一把锁🔒,因为lockA.wait() 只能使用 lockA.notify() / lockA.notifyAll();它们是使用同一把锁🔒的;

    等待:🔒锁.wait(); //等待区域

    唤醒:🔒锁.notify();//通知区域

    唤醒:🔒锁.notifyAll(); 开源框架 wait的比较多,大部分都是 notifyALl,要保证全部唤醒,无法明确唤醒哪一个;

    5> 停止线程

    1.stop(过时了,禁止使用) 5kb 下载到 3kb 危险的行为, 线程机制里面的碎片来不及释放, 暴力行为, 暴力行为,永不使用;

    2.让run函数执行完毕 才是正解 通过 thread.intercept() 控制Thread的isIntercepted() 结束线程;

    中断标志位的判断: thread.isIntercepted(); Thread.interrupted();//可检测线程中断标志位状态, 会重置标志位;

    sleep()会将中断信号清除,这时要在catch中在发一次intercept()方法;

    3. jdk线程是协作式的, 不是抢占式的;

    1) Thread类结束线程

    1. /**
    2. *类说明:如何安全中断线程
    3. */
    4. public class EndThread {
    5. private static class UseThread extends Thread{
    6. public UseThread(String name) {
    7. super(name);
    8. }
    9. @Override
    10. public void run() {
    11. String threadName = Thread.currentThread().getName();
    12. System.out.println(threadName+" interrupt flag ="+isInterrupted());
    13. while(!isInterrupted()){ // 可检测到中断标志位: interrupt flag =true (不重置中断标志位 true)
    14. // while(!Thread.interrupted()){ // 可检测到中断标志位: interrupt flag = false (重置标志位)
    15. // while(true){
    16. System.out.println(threadName+" is running");
    17. System.out.println(threadName+"inner interrupt flag ="
    18. +isInterrupted());
    19. }
    20. System.out.println(threadName+" interrupt flag ="+isInterrupted());
    21. }
    22. }
    23. public static void main(String[] args) throws InterruptedException {
    24. Thread endThread = new UseThread("endThread");
    25. endThread.start();
    26. Thread.sleep(20);
    27. endThread.interrupt();//中断线程,其实设置线程的标识位true
    28. }
    29. }

    2) Runnable类 结束线程

    1. /**
    2. *类说明:实现接口Runnable的线程如何中断
    3. */
    4. public class EndRunnable {
    5. private static class UseRunnable implements Runnable{
    6. @Override
    7. public void run() {
    8. while(!Thread.currentThread().isInterrupted()) {
    9. System.out.println(Thread.currentThread().getName()
    10. + " I am implements Runnable.");
    11. }
    12. System.out.println(Thread.currentThread().getName()
    13. +" interrupt flag is "+Thread.currentThread().isInterrupted());
    14. }
    15. }
    16. public static void main(String[] args) throws InterruptedException {
    17. UseRunnable useRunnable = new UseRunnable();
    18. Thread endThread = new Thread(useRunnable,"endThread");
    19. endThread.start();
    20. Thread.sleep(20);
    21. endThread.interrupt();
    22. }
    23. }

    3) 阻塞方法中抛出InterruptedException异常后,如果需要继续中断,需要手动再中断一次

    1. /**
    2. *类说明:阻塞方法中抛出InterruptedException异常后,如果需要继续中断,需要手动再中断一次
    3. */
    4. public class HasInterruptException {
    5. private static class UseThread extends Thread{
    6. public UseThread(String name) {
    7. super(name);
    8. }
    9. @Override
    10. public void run() { //run方法只是 一个成员方法,可以被反复调用, 只是对线程任务的一个封装;
    11. while(!isInterrupted()) {
    12. try {
    13. Thread.sleep(100); //阻塞线程会将isInterrupt 将标志状态变为false, 保留程序员释放资源的时间
    14. } catch (InterruptedException e) {
    15. System.out.println(Thread.currentThread().getName()
    16. +" in InterruptedException interrupt flag is "
    17. +isInterrupted());
    18. //资源释放
    19. interrupt(); //手动调用interrupt 来真正中断线程
    20. e.printStackTrace();
    21. }
    22. System.out.println(Thread.currentThread().getName()
    23. + " I am extends Thread.");
    24. }
    25. System.out.println(Thread.currentThread().getName()
    26. +" interrupt flag is "+isInterrupted());
    27. }
    28. }
    29. public static void main(String[] args) throws InterruptedException {
    30. Thread endThread = new UseThread("HasInterrputEx");
    31. endThread.start(); //star 方法只能调用一次
    32. Thread.sleep(500);
    33. endThread.interrupt();
    34. }
    35. }

    6>star 与 run 方法的 区别

    star 方法 是Thread 的 方法, 每次只能调用一次, run方法 只对线程任务的一个方法封装 属于普通方法, 可多次调用;

    7> join()改变线程执行顺序

    join()把指定的线程加入当前线程,可将两个交替执行的线程合并为顺序执行的线程,如在线程B中调用了线程A线程的

    join()方法, 直到线程A执行完毕后,才会继续执行线程B, T1,T2,T3 按顺序执行 T3.run(t2.join(),T2.run(T1.join()))

    8> yield()让出线程的执行权

    yield()使当前线程让出CPU的占用权,但让出的时间是不可设定的,也不会释放锁资源,

    所有执行yield()的线程,有可能进入到可执行状态后马上又被执行;

    9> setPriority() 线程的优先级

    通过setPriority(int)来修改线程优先级,优先级范围为1~10,默认是5,线程优先级越高分配的时间片数量就越多.设置优先级时,针对频繁阻塞(休眠或I/O操作)的线程需设置较高优先级,而偏重计算(需较多CPU时间或偏运算)的线程则应设置较低的优先级以确保处CPU不会被独占.在不同的JVM及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

    四.等待超时实现数据库连接池

    1. public class DBPool {
    2. /*容器,存放连接*/
    3. private static LinkedList pool = new LinkedList();
    4. /*限制了池的大小=20*/
    5. public DBPool(int initialSize) {
    6. if (initialSize > 0) {
    7. for (int i = 0; i < initialSize; i++) {
    8. pool.addLast(SqlConnectImpl.fetchConnection());
    9. }
    10. }
    11. }
    12. /*释放连接,通知其他的等待连接的线程*/
    13. public void releaseConnection(Connection connection) {
    14. if (connection != null) {
    15. synchronized (pool){
    16. pool.addLast(connection);
    17. //通知其他等待连接的线程
    18. pool.notifyAll();
    19. }
    20. }
    21. }
    22. /*获取*/
    23. // 在mills内无法获取到连接,将会返回null 1S
    24. public Connection fetchConnection(long mills)
    25. throws InterruptedException {
    26. synchronized (pool){
    27. //永不超时
    28. if(mills<=0){
    29. while(pool.isEmpty()){
    30. pool.wait();
    31. }
    32. return pool.removeFirst();
    33. }else{
    34. /*超时时刻*/
    35. long future = System.currentTimeMillis()+mills;
    36. /*等待时长*/
    37. long remaining = mills;
    38. while(pool.isEmpty()&&remaining>0){
    39. pool.wait(remaining);
    40. /*唤醒一次,重新计算等待时长*/
    41. remaining = future-System.currentTimeMillis();
    42. }
    43. Connection connection = null;
    44. if(!pool.isEmpty()){
    45. connection = pool.removeFirst();
    46. }
    47. return connection;
    48. }
    49. }
    50. }
    51. }
  • 相关阅读:
    一篇五分生信临床模型预测文章代码复现——Figure 4-6 临床模型构建(四)
    单片机中在制定通讯协议时候,一定加入容错和重发机制
    C#上位机与S7-200Smart通信注意事项
    【数据库入门】关系型数据库为什么这么受欢迎?
    C语言第四章第2节用if语句实现选择结构学习导案
    面向对象编程(高级部分)——类变量和类方法
    图书馆座位预约系统管理/基于微信小程序的图书馆座位预约系统
    报错 | RegExp2 is not defined
    解决Docker启动之npm版本不兼容问题
    Unity | API鉴权用到的函数汇总
  • 原文地址:https://blog.csdn.net/x910131/article/details/126022954