首页 > 编程语言 >Java多线程第三篇-多线程的代码相关案例

Java多线程第三篇-多线程的代码相关案例

时间:2024-12-25 22:28:25浏览次数:6  
标签:第三篇 Java SingleLazy void 队列 线程 new 多线程 public

文章目录

一.单例模式

单例模式是非常经典的设计模式,在我们写代码的时候,在单例模式下只能有一个对象,但是在日常开发中,我们很有可能忘记这个事情,所以这时候需要编译器来监督我们完成此工作,确保对象不会出现多个。在之前的代码过程中,也遇到过需要类似需要编译器来进行强制监督的关键词final、interferce、@Overide 、 throws等。

1.1 饿汉模式

在定义类的时候就已经创建了对象。

class Singleton{//实例 饿汉模式 这里在类加载就创建
    //线程安全,因为该类在多线程中无论怎样都只是new了一次
    private static Singleton instance=new Singleton();

    //通过此方法获取到实例,后续如果使用这个类的实例,都通过getInstance方法进行获取
    public static Singleton getInstance(){
        return instance;
    }
    //防止重复new出instance,将其设为private,则无法new出
    private Singleton(){}
}
public class Test {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        Singleton s3 = Singleton.getInstance();
    }
}


1.2 懒汉模式

在定义类的时候没有创建对象,而是在方法调用的时候初次对对象进行创建

class SingleLazy{//懒汉模式
    private static SingleLazy instance= null;

    //在首次调用instance的时候才开始创建实例,不调用不创建实例
    //而饿汉模式因为在getinstanfe中有进行判断条件,当在多线程的环境下,线程都是一起执行的,这里的判断都生效,则new了多个对象,则是不安全
    //如果想要解决该线程不安全问题需要给该类的方法中加锁
    public static SingleLazy getInstance() {
        //保证条件是整体才是线程安全,需要将整体的每个部分都包含,锁的对象,需要起到合理的锁竞争的效果。
        if (instance == null) {
            synchronized (instance) {
                if (instance == null) instance = new SingleLazy();
            }
        }
        return instance;
    }
    private SingleLazy(){}
}
public class Test {
    public static void main(String[] args) {
        SingleLazy s1=SingleLazy.getInstance();
        SingleLazy s2=SingleLazy.getInstance();
        SingleLazy s3=SingleLazy.getInstance();
        SingleLazy s4=SingleLazy.getInstance();
    }
}



上述两个类都属于单例模式,而不一样的是创建对象的时机不一样。
在我们在多线程开发的时候,如果在多线程中调用饿汉单例模式,因为类在开始时就创建了对象(只能创建一个),这是没有问题的。

在懒汉模式中,我们将类成员定义为null,初次创建对象是在条件中进行的,而当我们在条件中时,因为多线程是同时执行,我们这时候可能会因为多个条件成立而进入从而new出多个对象,这样会导致线程不安全。

如何避免避免这个情况发生?
这时候我们就可以通过加锁的形式,将这个对象进行上锁(synchronized),注意,上锁是一个整体,不是只锁一部分,这里的if也需要锁,如果不锁if则依然先进入if语句等待,这时候条件还是成立

这里还需要考虑一件事情,就是如果加锁之后,每次都需要进行加锁,这样会导致我们线程的速度很慢,这里我们需要在附加一个条件
在这里插入图片描述
举例:如果线程1和线程2在执行,线程1和线程2是否为空,都为空,进入,但是注意这里第一个if条件进去是上锁的,这时候我们的线程2就需要进行等待线程1释放,当释放完成后,线程2进入if条件,这时候instance已经不为空了,这时候直接释放返回结果。


1.3 指令重排序

指令重排序是在逻辑不变的情况下,编译器为了优化执行的效率,则需要对指令重排序。
创建一个对象的时候需要的步骤
1.首先申请开辟一块内存
2.在内存中构造对象
3.把内存的地址赋值给成员

而上述代码中,如果线程1和线程2在执行的过程中,线程1可能会在编译器中为了优化从而将创建对象的步骤打乱,由原来的123,可能修改为132步骤,直接将将一个地址给到instance,这时候给到t2的是一个非空的非法对象,这时候因为不为空,t2直接返回该结果,这时候t2可以访问到属性方法,就会导致出现bug。
在这里插入图片描述


1.4 解决方法volatile

通过volatile来修饰取消因为JVM引发的高效率指令重排序,这时候就可以解决此问题。

class SingleLazy{//懒汉模式
    private static volatile SingleLazy instance= null;

    //在首次调用instance的时候才开始创建实例,不调用不创建实例
    //而饿汉模式因为在getinstanfe中有进行判断条件,当在多线程的环境下,线程都是一起执行的,这里的判断都生效,则new了多个对象,则是不安全
    //如果想要解决该线程不安全问题需要给该类的方法中加锁
    public static SingleLazy getInstance() {
        //保证条件是整体才是线程安全,需要将整体的每个部分都包含,锁的对象,需要起到合理的锁竞争的效果。
        if (instance == null) {
            synchronized (instance) {
                if (instance == null) instance = new SingleLazy();
            }
        }
        return instance;
    }
    private SingleLazy(){}
}
public class Test {
    public static void main(String[] args) {
        SingleLazy s1=SingleLazy.getInstance();
        SingleLazy s2=SingleLazy.getInstance();
        SingleLazy s3=SingleLazy.getInstance();
        SingleLazy s4=SingleLazy.getInstance();
    }
}

1.如果是懒汉模式单例,首先需要对成员进行修饰volatile
2.双重条件判断
3.进行上锁 synchronized


二.阻塞队列

特殊对队列

  • 线程安全
  • 带有阻塞特性
    • 如果队列为空,继续出队列,就会发生阻塞,阻塞到其他线程往列里添加元素为止
    • 如果队列为满,继续入队列,也会发生阻塞,阻塞到其他线程从队列中取走元素为止。

这里我们的客户端发起请求,服务器进行接收,而大多数的服务器不只有一个,以分布式的服务器进行接收,但是其他的服务器在发起请求时另一个服务器接收到了,但是另一个服务器出现bug,这时候将接收到的响应返回给服务器,造成影响,耦合过高。
也有可能服务器A分布给其他服务器因为其他服务器接收到的每秒钟的请求量太大,单个访问消耗的硬件资源不同,高并发量太大就出现bug。
在这里插入图片描述
这时候我们就引出了阻塞队列来进行规范这一系列的问题(将阻塞队列封装成单独的服务程序,部署到特定的机器上,成为消息队列)。
如果通过消息队列来接收获取数据,这时候就会降低耦合,来减少两者的联系。
如果请求量高的情况下,可以通过在队列积压来进行阻塞,发送给服务器B的请求量能够处理,不出现bug,当请求量逐渐达到正常标准,服务器B就可以将积压的数据慢慢处理完成。
在这里插入图片描述

在Java的标准库中,有专门的阻塞队列来供开发者进行使用,但是它是由接口来实现的,所以在我们new一个对象时,需要通过Blocking数组或者Blocking链表来实现队列。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<String> blockingDeque= new LinkedBlockingDeque<>();
        blockingDeque.put("111");
        blockingDeque.put("222");
        blockingDeque.put("333");
        blockingDeque.put("444");
        String elem=blockingDeque.take();
        System.out.println(elem);
        elem=blockingDeque.take();
        System.out.println(elem);
        elem=blockingDeque.take();
        System.out.println(elem);
        elem=blockingDeque.take();
        System.out.println(elem);
        elem=blockingDeque.take();
        //阻塞队列第五次因空则进行阻塞
        System.out.println(elem);
    }
}

2.1 模拟实现阻塞队列(生产者消费者模型)

我们可以基于普通的循环队列,数组或者是链表的形式来实现阻塞队列。
阻塞队列,是当我们在put或者是take的时候,我们需要让两者进行上锁,使其中一个线程执行结束释放锁,另一个线程在进行执行。而在上锁之后,基于我们对阻塞特性,队列为空或者队列为满,需要一方对待,直到添加元素或者拿去元素之后被唤醒。(如果作为消费者,如果生产者没有生产出来这是不现实的或者生产者生产的足够多,没有人购买也是不现实的)

当在生活中我们购买产品的时候,都是在生产者生产出来我们在进行购买,我们基于这个进行这个逻辑进行实现,将生产者进行睡眠,使生产者的生产时间变慢。

class MyBlockingQueue {
    public String[] arr;
    //为了避免内存可见性,系统会进行读和写,加上volatile来进行避免
    public volatile int head;
    public  volatile int tail;
    public  volatile int size;
private final  Object locker=new Object();
    public MyBlockingQue() {
        this.arr = new String[5];
    }

    public void put(String elem) throws InterruptedException {
        synchronized (locker) {
            //当wait被唤醒时,还需要在检查一下是否size是满的
            while(size == arr.length){
                //队列满就会阻塞
                locker.wait();
            }
            arr[tail] = elem;
            tail++;
            if (tail == arr.length) tail = 0;
            size++;
            //唤醒take的wait阻塞状态
            locker.notify();
        }
    }
//put 和take是相互联系的,要么空要么满,如果一方为空或者是满,则需要一方进行唤醒。
    public String take() throws InterruptedException {
        synchronized(locker){
            while(size==0) {
               //队列拿取为空就会阻塞
                locker.wait();
            }
            String ret=arr[head];
            head++;
            if(head==arr.length)head=0;
            size--;
            //唤醒put的wait阻塞状态
            locker.notify();
            return ret;
        }
        }
    }
public class Test {
    public static void main(String[] args) {
//创建消费者和生产这模型
        MyBlockingQue myBlockingQue=new MyBlockingQue();
        //生产者
        Thread t1=new Thread(()->{
            int num = 1;
            while(true) {
                try {
                    myBlockingQue.put(num + "");
                    System.out.println("生产者产出:" + num);
                    num++;
                    //生产者每间隔500ms生成一次
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        //消费者
        Thread t2=new Thread(()->{
            while ((true)) {
                try {
                    System.out.println("消费者购买:" + myBlockingQue.take());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

三.定时器(日常开发常见的组建)

约定一个时间,时间到达之后,执行某一部分的代码逻辑。
在java的标准库的中就有实现定时器的方法,我们通过创建一个Timer类型的对象,然后通过schedule方法来进行定时器的推迟运行时间。

这里的定时器的方法schedule可以看到在java的标准库中包含两个参数,第一个参数就是我们需要执行的任务,第二个是我们要推迟的时间。
在这里插入图片描述
这里Timer是一个线程,并且我们可以同时进行多个任务。

import java.util.Timer;
import java.util.TimerTask;

public class Test {
    public static void main(String[] args) {
        //这里的Timer中也会创建一个线程。
        Timer timer=new Timer();

        //new出timerTask的方法
        timer.schedule(new TimerTask() {
           //重写run方法
            @Override
            public void run() {
                System.out.println("5000");
            }
            //推迟5000ms后启动
        },5000);
        
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("2000");
            }
        },2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);
        System.out.println("程序启动");
    }
}


3.1 创建并模拟定时器实现

1.首先我们需要创建一个Timer线程,来查询推迟程序运行的时间是否到达,这时候就可以开始执行程序。
2.我们需要有一个数据结构,用来存储所有的任务。
这里我们可以使用一个优先级队列,来进行存储。
3.还需要创建一个类,通过类的对象来描述一个任务(需要任务内容和时间)。

package Demo21;

import java.util.PriorityQueue;
import java.util.Timer;
import java.util.TimerTask;

class MyTimerTask implements Comparable<MyTimerTask>{
    //这里来创建Runnable和long类型的对象
    //通过runnable中的run方法来执行代码
    private Runnable runnable;
    //创建实例所需要的时间
    private long time;

    public MyTimerTask(Runnable runnable, long time) {
        this.runnable = runnable;
        //补充一个绝对时间,通过系统调用当前的时刻,并与自定义的推迟时间结合,算出一个推迟多久的执行时间
        this.time = time+System.currentTimeMillis();
    }
    //通过runnable来调用run方法
    public Runnable runnable(){
        return runnable;
    }
    //获取时间
    public long getTime(){
        return time;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //优先级队列比较大小,this-参数为最小值放在优先级队列队首
        return (int)(this.time-o.time);
    }
}
class MyTimer{
//  在构造方法中使用线程进行扫描,主线程和t线程“同时执行”。
    public MyTimer(){
        Thread t=new Thread(()->{
            while(true){
                //这里上锁是需要让线程进行等待
                synchronized (locker){
                    //这里如果为空,则需要进行等待
                    while(queue.isEmpty()){
                        try {
                            locker.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    //到了这里则队列不为空,就需要去释放队首,这里需要的条件是时间是否满足释放的要求
                    long time=System.currentTimeMillis();//这里获取最新的时间
                    MyTimerTask task=queue.peek(); //这里获取队首的元素
                    //比较当前时间和任务时间,当前时间到达任务时间,则释放队首
                    if(time>=task.getTime()){
                        //运行代码
                        task.runnable().run();
                        //释放队首元素
                        queue.poll();

                    }else{
                        //这里没有到达时间的情况下,就需要进行等待,但是等待也是需要有参数的,不能一直等待下去
                        //这里设置的等待时间应该是我们队首(最小的)时间
                        //这里每次poll出去重新获取到的时间,就是队首需要等待的时间
                        try {
                            locker.wait(task.getTime()-time);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }
        });
        t.start();
    }
    //创建一个优先级队列
    private PriorityQueue<MyTimerTask> queue=new PriorityQueue<>();
    //创建一把锁
    private final  Object locker=new Object();
    //通过schedule创建一个优先级队列,然后通过线程扫描来进行是否添加数据和时间
    public void schedule(Runnable runnable,long delay){
        synchronized (locker){
            queue.offer(new MyTimerTask(runnable,delay));
            //这里是用来唤醒两个wait的阻塞等待
            locker.notify();
        }
    }

}
public class Test {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行时间:1000");
            }
        }, 1000);

        myTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行时间:5000");
            }
        }, 5000);

        myTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行时间:3000");
            }
        }, 3000);

        myTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行时间:2000");
            }
        }, 2000);
    }
}

在这里插入图片描述


四.线程池

首先,线程的诞生是因为进程的创建/销毁过慢。
而为了进一步提升线程的创建/销毁频率,引出线程轻量级线程(将系统调用的过程省略,由人工手动调度)。
在JAVA中标准库内没有协程,可以引用第三方库来实现此功能,但是通过第三方库无法保证百分百的高效率执行,这时候我们就需要运用到线程池。

package Demo23;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        //工厂模式
        ExecutorService service= Executors.newCachedThreadPool();
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

上述通过线程进行操作则是通过调用API来去创建线程需要从内核态中操作(内核态对系统所有的操作进行服务)不可预期,不可控制
线程池优化频繁创建/销毁的场景。
线程池优化是通过用户态操作,用户态操作是可以预期的,可以控制的。

在Java中的标准库中有线程池的方法供我们使用。
在这里插入图片描述

newCachedThreadPool

对线程池进行缓存,此构造方法构造出来的对象是具有自适应能力(可以随着添加任务,线程中的线程会根据需要自动创建出来,创建出来之后不会着急销毁,会在线程池中保留一定时间)。

newSingleThreadExecutor

创建单个线程

newFixedThreadPool

固定创建带有参数的线程的线程池,不具有自适应能力

newScheduledThreadPool

和定时器,不是一个扫描线程负责执行任务,而是多个线程进行执行时间的任务

上述的线程池都是对一个类进行的封装ThreadPoolExecutor,通过这些方法来对这个类填写不同的参数进行构造线程池。

4.1线程池的构造方法的使用

在这里插入图片描述
core size核心线程数量

参数1为核心线程数量,参数类型int
核心线程就是主要进行工作的线程。

maximum size最大线程数量

参数2为最大线程数量,参数类型int
最大线程是核心线程与其他线程,其他线程用来辅助核心线程完成工作,提升效率,又避免了过多的系统开销。

keepAliveTime 保持存活的时间

参数3类型:long
参数3为其他线程在不进行辅助工作时,所存在的时间。

unit 单位

参数4类型:TimeUnit
参数4为存活时间的绝对时间,以ms、s、min来换算。

workQueue

参数5类型:BlockingQueue(阻塞队列)
这里的队列数据结构可以根据需求进行更改,灵活设置
参数5用来存放线程的中的任务。

threadFactory

参数5类型:ThreadFactory类
参数5为工厂模式,通过此模式来创建线程。(通过进行类方法static修饰,构成相同参数的工厂类方法)。

handler

参数6类型:RejectedExecutionHandler类(线程池的拒绝策略)
参数6表示一个线程池中容纳的任务数量又上限,如果超出上限,则会出现多种效果。
在这里插入图片描述

方法效果
AbortPolicy如果池中任务数量已经满了,再次添加即抛出异常
CallerRunsPolicy新添加的任务,又调用者进行执行
DiscardOldestPolicy丢弃任务中最老的任务
DiscardPolicy丢弃当前的任务

4.2 模拟实现线程池

package Demo24;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPoll {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    private final Object locker = new Object();

    public void submit(Runnable runnable) throws InterruptedException {
        //第五种策略阻塞
        queue.put(runnable);
    }

    public MyThreadPoll(int n) {
        //创建出n个线程,负责执行上述队列的任务
            for (int i = 0; i < n; i++) {
                Thread t = new Thread(() -> {
                    //让这个线程,从队列中消费任务,并进行执行
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                });
                t.start();
        }
    }
}
public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPoll myThreadPoll=new MyThreadPoll(10);
        for(int i=0;i<500;i++){
            //
            int num=i;
            myThreadPoll.submit(new Runnable() {
                @Override
                public void run() {
                    //变量捕获无法被进行修改,可以通过一个临时变量来捕获
                    System.out.println("执行任务:"+num);
                }
            });
        }
    }
}


五.锁策略(特点)

5.1悲观锁和乐观锁

对后续锁冲突是否激烈来进行预测。
悲观锁:如果预测下来锁的冲突概率大,需要的工作增加。
乐观锁:如果预测下来锁的冲突概率小,需要的工作减少。

5.2重量级锁和轻量级锁

轻量级锁:锁的开销比较小。
重量级锁:锁的开销比较大。

5.3自旋锁(Spin lock)和挂起等待锁

自旋锁:用户态实现,是一种轻量级锁的典型实现(通过while循环进行)。
挂起等待锁:通过API来进行实现,通过内核操作,属于重量级锁的典型实现。

5.4读写锁

读写锁是将加锁操作,分成读锁和谢锁。
两个线程加锁过程中
读加锁和读锁之间,不会产生竞争。
读锁和写锁之间,有竞争。
谢锁和写锁之间也有竞争。

5.5可重入锁和不可重入锁

一个线程针对同一把锁,连续的加锁两次,不会死锁,就是可重入锁,出现死锁,则是不可重入锁。

5.6公平锁和不公平锁

当很多个线程同时获取一把锁时,一个线程能够拿到锁,而其他的锁需要等待拿到锁的线程,当线程释放掉锁时,按着“先来后到”顺序进行分配,此时是公平锁。
非公平锁则是剩下的线程以“均等”的概率,重写竞争锁。
操作系统的API默认情况为非公平锁。

标签:第三篇,Java,SingleLazy,void,队列,线程,new,多线程,public
From: https://blog.csdn.net/weixin_60489641/article/details/144562596

相关文章

  • 南昌航空大学-软件学院-22207112-卢翔-JAVAPTA(7-8)博客
    目录前言PTA第七次作业设计与分析题目分析知识点解析调试过程改进建议PTA第八次作业设计与分析题目分析知识点解析调试过程改进建议踩坑心得总结学期总结前言PTA第七次作业设计与分析题目分析本题在家居强电电路模拟程序-2基础上新增了多个并联电路串联在一起的情况。需要虑......
  • 打印三角形金字塔 、debug、java的方法、命令行传参、可变参数20241225
    打印三角形金字塔debug20241225packagecom.pangHuHuStudyJava.struct;publicclassPrint_Tran{publicstaticvoidmain(String[]args){for(intj=0;j<5;j++){for(intr=5;r>j;r--){System.out.print(&#......
  • 黑马Java面试教程_P9_MySQL
    系列博客目录文章目录系列博客目录前言1.优化1.1MySQL中,如何定位慢查询?面试文稿1.2面试官接着问:那这个SQL语句执行很慢,如何分析(=如何优化)呢?面试文稿1.3了解过索引吗?(什么是索引)1.4继续问索引的底层数据结构了解过吗?面试文稿1.5什么是聚簇索引(聚集索......
  • 基于java的SpringBoot/SSM+Vue+uniapp的小型企业办公自动化系统的详细设计和实现(源码
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • [Java/压缩] Java读取Parquet文件
    序:契机生产环境有设备出重大事故,又因一关键功能无法使用,亟需将生产环境的原始MQTT报文(以parquet文件格式+zstd压缩格式落盘)DOWN到本地,读取并解析。本文聚焦在本地电脑,用java读取parquet文件相当多网络文档的读取代码无法正常运行,有必要记录一二,后续还需进一步......
  • Java基于SpringBoot的房屋租赁应收应付管理系统-java vue.js idea
    所需该项目可以在最下面查看联系方式,为防止迷路可以收藏文章,以防后期找不到项目介绍Java基于SpringBoot的房屋租赁应收应付管理系统-javavue.jsidea系统实现截图技术栈介绍JDK版本:jdk1.8+编程语言:java框架支持:springboot数据库:mysql版本不限数据库工......
  • Java基于SpringBoot的小说阅读平台的设计-java vue.js idea
    所需该项目可以在最下面查看联系方式,为防止迷路可以收藏文章,以防后期找不到项目介绍Java基于SpringBoot的小说阅读平台的设计-javavue.jsidea系统实现截图技术栈介绍JDK版本:jdk1.8+编程语言:java框架支持:springboot数据库:mysql版本不限数据库......
  • Java基于SpringBoot的宠物寄领养网站的设计与实现-java vue.js idea
    所需该项目可以在最下面查看联系方式,为防止迷路可以收藏文章,以防后期找不到项目介绍Java基于SpringBoot的宠物寄领养网站的设计与实现-javavue.jsidea系统实现截图技术栈介绍JDK版本:jdk1.8+编程语言:java框架支持:springboot数据库:mysql版本......
  • Java基于SpringBoot的小学家校互联平台视频-java vue.js idea
    所需该项目可以在最下面查看联系方式,为防止迷路可以收藏文章,以防后期找不到项目介绍Java基于SpringBoot的小学家校互联平台视频-javavue.jsidea系统实现截图技术栈介绍JDK版本:jdk1.8+编程语言:java框架支持:springboot数据库:mysql版本不限数据库工......
  • Java中SPI机制原理解析
    使用SPI机制前后的代码变化加载MySQL对JDBC的Driver接口实现在未使用SPI机制之前,使用JDBC操作数据库的时候,一般会写如下的代码://通过这行代码手动加载MySql对Driver接口的实现类Class.forName("com.mysql.jdbc.Driver")DriverManager.getConnection("jdbc:mysql://127.0.0.1......