多线程介绍进程：进程指正在运行的程序。确切的来说，当一个程序进入内存运行，即变成一个进程，进程是处于运行过程中的程序，并且具有一定独立功能。线程：线程是进程中的一个执行单元，负责当前进程中程序的执行，一个进程中至少有一个线程。一个进程中是可以有多个线程的，这个应用程序也可以称之为多线程程序。简而言之：一个程序运行后至少有一个进程，一个进程中可以包含多个线程什么是多线程呢？即就是一个程序中有多个线程在同时执行。通过下图来区别单线程程序与多线程程序的不同：单线程程序：即，若有多个任务只能依次执行。当上一个任务执行结束后，下一个任务开始执行。如，去网吧上网，网吧只能让一个人上网，当这个人下机后，下一个人才能上网。多线程程序：即，若有多个任务可以同时执行。如，去网吧上网，网吧能够让多个人同时上网。程序运行原理分时调度：所有线程轮流使用 CPU 的使用权，平均分配每个线程占用 CPU 的时间。抢占式调度：优先让优先级高的线程使用 CPU，如果线程的优先级相同，那么会随机选择一个(线程随机性)，Java使用的为抢占式调度。抢占式调度详解大部分操作系统都支持多进程并发运行，现在的操作系统几乎都支持同时运行多个程序。比如：现在我们上课一边使用编辑器，一边使用录屏软件，同时还开着画图板，dos窗口等软件。此时，这些程序是在同时运行，”感觉这些软件好像在同一时刻运行着实际上，CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言，某个时刻，只能执行一个线程，而 CPU的在多个线程间切换速度相对我们的感觉要快，看上去就是在同一时刻运行。其实，多线程程序并不能提高程序的运行速度，但能够提高程序运行效率，让CPU的使用率更高。线程的状态线程可以处于下列状态之一：NEW：至今尚未启动的线程处于这种状态RUNNABLE：正在 Java 虚拟机中执行的线程处于这种状态BLOCKED：受阻塞并等待某个监视器锁的线程处于这种状态WAITING：无限期地等待另一个线程来执行某一特定操作的线程处于这种状态TIMED_WATING：等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。TERHINATED已退出的线程处于这秘状态在给定时间点上，一个线程只能处于一种状态。这些状态是虚拟机状态，它们并没有反映所有操作系统线程状态主线程当我们在dos命令行中输入java空格类名回车后，启动JVM，并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码，一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码，那么在循环之后的其他代码是不会被马上执行的。如下代码演示：class Demo{ String name; Demo(String name){ this.name = name; } void show() { for (int i=1;i<=10000 ;i++ ) { System.out.println("name="+name+",i="+i); } } }class TestDemo { public static void main(String[] args) { Demo d = new Demo("小强"); Demo d2 = new Demo("旺财"); d.show(); d2.show(); System.out.println("Hello World!"); } }若在上述代码中show方法中的循环执行次数很多，这时在d.show();下面的代码是不会马上执行的，并且在dos窗口会看到不停的输出name=小强,i=值，这样的语句。为什么会这样呢？原因是：[font color="red"]jvm启动后，必然有一个执行路径(线程)从main方法开始的，一直执行到main方法结束，这个线程在java中称之为主线程。[/font]当程序的主线程执行时，如果遇到了循环而导致程序在指定位置停留时间过长，则无法马上执行下面的程序，需要等待循环结束后能够执行。那么，能否实现一个主线程负责执行其中一个循环，再由另一个线程负责其他代码的执行，最终实现多部分代码同时执行的效果？能够实现同时执行，通过Java中的多线程技术来解决该问题Thread类如何创建线程呢？通过API中搜索，查到Thread类。通过阅读Thread类中的描述。Thread是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。创建新执行线程有两种方法。将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。创建对象，开启线程。run方法相当于其他线程的main方法。声明一个实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象，传入到某个线程的构造方法中，开启线程。创建线程方式一继承Thread类创建线程的步骤：定义一个类继承Thread重写run方法创建子类对象，就是创建线程对象调用start方法，开启线程并让线程执行，同时还会告诉jvm去调用run方法/** * 这是自定义线程类，继承Thread类 * 重写run方法 */ public class SubThread extends Thread { /** * 重写run方法，完成该线程执行的逻辑 */ @Override public void run() { for (int i=0; i<50; i++) { System.out.println(getName()+"循环第"+i+"次"); } } }/** * 测试类，创建和启动一个线程 * 创建Theard子类对象 * 子类对象调用方法start()，让线程程序执行JVM调用线程中的run */ public class ThreadDemo { public static void main(String[] args) { //创建自定义线程对象 SubThread st = new SubThread(); //开启新线程 st.start(); //在主方法中执行for循环 for (int i=0; i<50; i++) { System.out.println("main循环第"+i+"次"); } } }线程对象调用 run方法和调用start方法区别：线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程，并让jvm调用run方法在开启的线程中执行。1、我们为什么要继承Thread类，并调用其的start方法才能开启线程呢？那是因为Thread类用来描述线程，具备线程应该有功能。2、为什么不直接创建Thread类的对象呢？Thread t1 = new Thread();t1.start();这样做没有错，但是该start调用的是Thread类中的run方法，而这个run方法没有做什么事情，更重要的是这个run方法中并没有定义我们需要让线程执行的代码。对于之前所讲的主线程，它的任务定义在main函数中。自定义线程需要执行的任务都定义在run方法中。Thread类run方法中的任务并不是我们所需要的，只有重写这个run方法。既然Thread类已经定义了线程任务的编写位置（run方法），那么只要在编写位置（run方法）中定义任务代码即可。所以进行了重写run方法动作。多线程执行时，在栈内存中，其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈，当执行线程的任务结束了，线程自动在栈内存中释放了，但是当所有的执行线程都结束了，那么进程就结束了。获取线程名称开启的线程都会有自己的独立运行栈内存，那么这些运行的线程的名字是什么呢？该如何获取呢？既然是线程的名字，按照面向对象的特点，是哪个对象的属性和谁的功能，那么我们就去找那个对象就可以了。查阅Thread类的API文档发现有个方法是获取当前正在运行的线程对象。还有个方法是获取当前线程对象的名称。/** * 这是自定义线程类，继承Thread类 * 重写run方法 * * 获取线程名字的方法 String getName() */ public class SubThread extends Thread { /** * 重写run方法，完成该线程执行的逻辑 */ @Override public void run() { System.out.println(super.getName()); } }/** * 测试类，创建和启动一个线程 * 每个线程都有自己的名字 * 运行方法main主线程，默认名字就是"main" * 其他新建的子线程，默认名字是"Thread-x" * * JVM开启主线程，运行方法main，主线程也是线程必然也是继承于Thread类 * Theard类当中，静态方法static Thread currentThread()返回正在执行的线程对象 */ public class ThreadDemo { public static void main(String[] args) { //创建自定义线程对象 SubThread st = new SubThread(); //开启新线程 st.start(); //通用获取线程名称的方式 System.out.println(Thread.currentThread().getName()); } }设置线程名称/** * 这是自定义线程类，继承Thread类 * 重写run方法 * * 设置线程名字的方法 setName(String name) */ public class SubThread extends Thread { public SubThread(){} public SubThread(String threadName){ setName(threadName); } /** * 重写run方法，完成该线程执行的逻辑 */ @Override public void run() { System.out.println(super.getName()); } }/** * 测试类，创建和启动一个线程 * 每个线程都有自己的名字 * 运行方法main主线程，默认名字就是"main" * 其他新建的子线程，默认名字是"Thread-x" * * JVM开启主线程，运行方法main，主线程也是线程必然也是继承于Thread类 * Theard类当中，静态方法static Thread currentThread()返回正在执行的线程对象 */ public class ThreadDemo { public static void main(String[] args) { //创建自定义线程对象 SubThread st = new SubThread("new-thread-01"); //开启新线程 st.start(); //通用获取线程名称的方式 System.out.println(Thread.currentThread().getName()); } }创建线程方式—实现Runnable接口创建线程的另一种方法是声明实现 Runnable 接口的类。该类然后实现 run 方法。然后创建Runnable的子类对象，传入到某个线程的构造方法中，开启线程。为何要实现Runnable接口，Runable是啥玩意呢？查看Runnable接口说明文档：Runnable接口用来指定每个线程要执行的任务。包含了一个 run 的无参数抽象方法，需要由接口实现类重写该方法。创建线程的步骤：定义类实现Runnable接口；覆盖接口中的run方法；创建Thread类的对象；将Runnable接口的子类对象作为参数传递给Thread类的构造函数；调用Thread类的start方法开启线程；/** * 实现线程程序的另一种方式，接口实现 * 实现接口Runnable,重写run方法 */ public class SubRunnable implements Runnable{ @Override public void run() { for(int i=0; i<5; i++) { System.out.println(i); } } }/** * 测试类，创建和启动一个线程 * 创建Thread类对象，在构造方法当中传递Runnable接口实现类 * 调用Thread类方法start()启动 */ public class ThreadDemo { public static void main(String[] args) { SubRunnable sr = new SubRunnable(); new Thread(sr).start(); } }为什么需要定一个类去实现Runnable接口呢？继承Thread类和实现Runnable接口有啥区别呢？实现Runnable接口，避免了继承Thread类的单继承局限性。覆盖Runnable接口中的run方法，将线程任务代码定义到run方法中。创建Thread类的对象，只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中，而这个run方法所属于Runnable接口的子类对象，所以将这个子类对象作为参数传递给Thread的构造函数，这样，线程对象创建时就可以明确要运行的线程的任务。实现Runnable的好处：第二种方式实现Runnable接口避免了单继承的局限性，所以较为常用。实现Runnable接口的方式，更加的符合面向对象，线程分为两部分，一部分线程对象，一部分线程任务。继承Thread类，线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象，既是线程对象，有又有线程任务。实现runnable接口，将线程任务单独分离出来封装成对象，类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。线程的匿名内部类使用使用线程的内匿名内部类方式，可以方便的实现每个线程执行不同的线程任务操作/** * 使用匿名内部类，实现多线程程序 * 前提：继承或者接口的实现 * new 父类或者接口(){ * 重写抽象的方法 * } */ public class ThreadDemo{ public static void main(String[] args) { //继承方式 XXX extends Thread{public void run(){ ... }} new Thread(){ @Override public void run() { System.out.println(getName()); } }.start(); //实现接口 XXX implements Runnable(){public void run(){ ... }} new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }){}.start(); } }线程池线程池概念线程池，其实就是一个容纳多个线程的容器，其中的线程可以反复使用，省去了频繁创建线程对象的操作，无需反复创建线程而消耗过多资源。我们详细的解释一下为什么要使用线程池？在java中，如果每个请求到达就创建一个新线程，开销是相当大的。在实际使用中，创建和销毁线程花费的时间和消耗的系统资源都相当大，甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外，活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程，可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足，需要采取一些办法来限制任何给定时刻处理的请求数目，尽可能减少创建和销毁线程的次数，特别是一些资源耗费比较大的线程的创建和销毁，尽量利用已有对象来进行服务。线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程，线程创建的开销就被分摊到了多个任务上了，而且由于在请求到达时线程已经存在，所以消除了线程创建所带来的延迟。这样，就可以立即为请求服务，使用应用程序响应更快。另外，通过适当的调整线程中的线程数目可以防止出现资源不足的情况。使用线程池方式--Runnable接口通常，线程池都是通过线程池工厂创建，再调用线程池中的方法获取线程，再通过线程去执行任务方法。[font color="red"]Executors：线程池创建工厂类[/font]public static ExecutorService newFixedThreadPool(int nThreads)：返回线程池对象[font color="red"]ExecutorService：线程池类[/font]Future<?> submit(Runnable task)：获取线程池中的某一个线程对象，并执行[font color="red"]Future接口：用来记录线程任务执行完毕后产生的结果。线程池创建与使用[/font]使用线程池中线程对象的步骤：创建线程池对象创建Runnable接口子类对象提交Runnable接口子类对象关闭线程池/** * JDK5的新特性 线程池技术 * 实现一个线程池，使用util包下的concurrent工厂类Executors中的静态方法创建线程对象，并设定线程的个数 * static ExecutorService newFixedThreadPool(int 个数) 返回线程池对象 * 返回的是ExecutorService接口的实现类(线程池的对象) * 接口实现类对象，调用方法submit(Runnable r)提交一个线程执行的任务 */ public class ThreadPoolDemo { public static void main(String[] args) { //调用工厂类的静态方法，创建线程池对象 //返回的是线程池对象，即ExecutorService接口 ExecutorService es = Executors.newFixedThreadPool(2); //调用接口实现类对象es中的方法submit提交执行任务 //将Runnable的实现类对象传递即可 es.submit( new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"正在执行新任务"); } } ); //销毁线程池的方法(一般不会使用) es.shutdown(); } }使用线程池方式—Callable接口Callable接口：与Runnable接口功能相似，用来指定线程的任务。其中的call()方法，用来返回线程任务执行完毕后的结果，call方法可抛出异常。ExecutorService：线程池类Future submit(Callable task)：获取线程池中的某一个线程对象，并执行线程中的call()方法Future接口：用来记录线程任务执行完毕后产生的结果。线程池创建与使用使用线程池中线程对象的步骤：创建线程池对象创建Callable接口子类对象提交Callable接口子类对象关闭线程池import java.util.concurrent.*; /** * JDK5的新特性 线程池技术 实现Callable接口 * 实现步骤： * Executor工厂类静态方法newFixedThreadPool创建线程池对象 * 线程池实现对象ExecutorService接口实现类，调用submit方法提交线程 * submit(Callable c) */ public class ThreadPoolDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService es = Executors.newFixedThreadPool(2); //提交线程任务的方法submit将返回一个Future接口的实现类 Future<String> f = es.submit( /** * Callable接口实现类，作为线程提交任务出现 * 使用方法返回值 */ new Callable<String>() { @Override public String call() throws Exception { return Thread.currentThread().getName()+"正在执行新任务"; } } ); //获取返回值 System.out.println(f.get()); } }案例：多线程实现异步计算import java.util.concurrent.Callable; /** * Callable接口的实现类 */ public class CountDemo implements Callable<Integer> { private int a; private int b; public CountDemo(int a, int b) { this.a = a; this.b = b; } @Override public Integer call() throws Exception { int result = 0; for (int i=a; i<=b; i++){ result=result+i; } return result; } }import java.util.concurrent.*; /** * 使用多线程技术进行求和计算 * 要求两个线程处理，一个线程计算1...100的和，另一个计算101...200的和 */ public class ThreadPoolDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService es = Executors.newFixedThreadPool(2); //线程一：计算1...100的和 Future<Integer> result1 = es.submit(new CountDemo(1,100)); //线程二：计算101...200的和 Future<Integer> result2 = es.submit(new CountDemo(101,200)); int finalResult = result1.get() + result2.get(); System.out.println("最终结果为："+ finalResult); } }自定义线程池：ThreadPoolExecutor内置的线程池, 不推荐生产使用：Executors.newCachedThreadPool();Executors.newFixedThreadPool(10);Executors.newScheduledThreadPool(10);Executors.newSingleThreadExecutor();上述的方法不推荐在生产当中使用，在[font color="red"]生产场景下必须使用ThreadPoolExecutor构建线程池[/font]，这样可以明确线程池的运行规则，创建符合自己业务场景需要的线程池，避免资源耗尽的风险。以下是一个简单的示例：构造函数corePoolSize：指定了线程池中的线程数量，它的数量决定了添加的任务是开辟新的线程去执行，还是放到workQueue任务队列中去。maximumPoolSize：指定了线程池中的最大线程数量，这个参数会根据你使用的workQueue任务队列的类型，决定线程池会开辟的最大线程数量。keepAliveTime：当线程池中空闲线程数量超过corePoolSize时，多余的线程会在多长时间内被摧毁。unit：keepAliveTime的单位。workQueue：任务队列，被添加到线程池中，但尚未被执行的任务；分为直接提交队列，有界任务队列，无界任务队列，优先任务队列。threadFactory：线程工厂，用于创建线程。handler：拒绝策略，当任务太多来不及处理的时候，会用这种策略拒绝任务。corePoolSize和maximumPoolSizeThreadPoolExecutor executorPool = new ThreadPoolExecutor( 5, //corePoolSize 10, //maximumPoolSize 3, //keepAliveTime TimeUnit.SECONDS, //unit new ArrayBlockingQueue<Runnable>(50) );上边代码意思是有5个核心线程数，10个最大线程数，任务队列是50个线程。在运行时，JVM首先为前5个新任务创建新线程，此时再来任务就放入任务队列中，直到任务队列已放满，此时再来新任务，JVM就会创建新线程,直到此时线程池中达到10个线程了就停止创建，即达到了最大线程数，此时再来新任务就会使用配置的拒绝策略新任务的提交。workQueue任务队列分为直接提交队列，有界任务队列，无界任务队列，优先任务队列。直接提交队列设置SynchronousQueue队列，SynchronousQueue是一个特殊的BlockingEueue，它没有容量，每执行一个插入操作就会阻塞，需要再执行一个删除操作才能被唤醒，反之每一个删除操作也都要等待对应的插入操作。import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MICROSECONDS, new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 3; i++) { pool.execute( new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ); } pool.shutdown(); } } ------输出结果--------- pool-1-thread-2 pool-1-thread-1 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.if010.thread.ThreadPool$1@5e2de80c rejected from java.util.concurrent.ThreadPoolExecutor@1d44bcfa[Running, pool size = 2, active threads = 1, queued tasks = 0, completed tasks = 0] at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) at com.if010.thread.ThreadPool.main(ThreadPool.java:16)从输出结果当中看到，当任务队列为SynchronousQueue时，创建的线程数量大于maximumPoolSize时，直接执行了拒绝策略抛出异常。使用SynchronousQueue队列，提交的任务不会被保存，会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize时，则尝试创建新的线程，如果达到maximumPoolSize设置的最大值，则会根据设置的handler执行拒绝策略。有界任务队列使用ArrayBlockingQueue实现import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(10), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 3; i++) { pool.execute( new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ); } pool.shutdown(); } } ------输出结果------- pool-1-thread-1 pool-1-thread-1 pool-1-thread-1使用ArrayBlockingQueue有界任务队列，当有新的任务需要执行的时候，线程池会创建新的线程，直到创建的线程数量达到corePoolSize时，会将新的任务加入到等待队列中。当等待队列满了的时候，也会继续创建线程，直到线程数量达到maximumPoolSize设置的最大线程数量时，则会执行拒绝策略。在这种情况下，线程数量的上限与有界任务队列的状态有直接的关系，如果有界队列初始容量较大或者没有达到超负荷的状态，线程数将一直维持在corePoolSize以下，反之当任务队列满了时，则会以maximumPoolSize为最大线程数上限。无界任务队列使用LinkedBlockingQueue实现。import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 3; i++) { pool.execute( new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ); } pool.shutdown(); } } ------输出结果------- pool-1-thread-1 pool-1-thread-1 pool-1-thread-1使用无界任务队列，线程池的任务队列可以无限制的添加新任务，而线程池创建的最大线程数量就是corePoolSize设置的数量，也就是说此时maximumPoolSize参数是无效的。当有新的任务加入时，则会直接进入等待队列，所以你一定要注意任务提交与处理直接的协调，要防止等待队列中的任务由于无法及时处理而一直增长，导致资源耗尽。优先任务队列使用PriorityBlockingQueue实现。import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MICROSECONDS, new PriorityBlockingQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 10; i++) { pool.execute(new ThreadTask(i)); } pool.shutdown(); } } class ThreadTask implements Runnable, Comparable<ThreadTask> { private int priority; public int getPriority() { return priority; } public void setPriority(int priority) { this.priority = priority; } public ThreadTask() { } public ThreadTask(int priority) { this.priority = priority; } //当前对象与其它对象比较，当优先级大时返回-1，优先级小时返回1 //priority值越小优先级越高 @Override public int compareTo(ThreadTask o) { return this.priority > o.priority ? -1 : 1; } @Override public void run() { try { //阻塞线程，使后续任务进入缓存队列 Thread.sleep(1000); System.out.println("当前线程优先级：" + this.priority + ",线程名字：" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } ------输出结果------- 当前线程优先级：0,线程名字：pool-1-thread-1 当前线程优先级：9,线程名字：pool-1-thread-1 当前线程优先级：8,线程名字：pool-1-thread-1 当前线程优先级：7,线程名字：pool-1-thread-1 当前线程优先级：6,线程名字：pool-1-thread-1 当前线程优先级：5,线程名字：pool-1-thread-1 当前线程优先级：4,线程名字：pool-1-thread-1 当前线程优先级：3,线程名字：pool-1-thread-1 当前线程优先级：2,线程名字：pool-1-thread-1 当前线程优先级：1,线程名字：pool-1-thread-1除了第一个任务直接创建线程执行外，其它的任务都被放入了优先任务队列，按照优先级进行重新排序执行，且线程池的线程数一直为corePoolSize，也就是一个，说明此时maximumPoolSize设置无效。也就是说PriorityBlockingQueue是一个特殊的无界队列，无论添加了多少个任务，线程池创建的线程数也不会超过corePoolSize设置的数量。threadFactory线程池中线程是通过ThreadPoolExecutor中的ThreadFactory线程工厂创建。通过自定义ThreadFactory可以按需要对线程池中创建的线程进行一些特殊设置，比如命名，优先级。import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(5), new ThreadFactory() { @Override public Thread newThread(Runnable r) { System.out.println("线程 " + r.hashCode() + " 创建"); //线程命名 Thread th = new Thread(r, "线程名字 " + r.hashCode()); return th; } }, new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 10; i++) { pool.execute( new Runnable() { @Override public void run() { //输出执行线程的名称 System.out.println("执行中的线程名字：" + Thread.currentThread().getName()); } } ); } pool.shutdown(); } } ------输出结果------- 线程 1627674070 创建 线程 1360875712 创建 执行中的线程名字：线程名字 1627674070 线程 1625635731 创建 执行中的线程名字：线程名字 1360875712 执行中的线程名字：线程名字 1627674070 执行中的线程名字：线程名字 1360875712 执行中的线程名字：线程名字 1627674070 执行中的线程名字：线程名字 1627674070 执行中的线程名字：线程名字 1625635731 执行中的线程名字：线程名字 1360875712 执行中的线程名字：线程名字 1625635731 执行中的线程名字：线程名字 1627674070handler为防止资源被耗尽，任务队列都会选择创建有界任务队列，但是这种模式下如果出现任务队列已满并且线程池创建的线程数已达到最大线程数时，就需要指定ThreadPoolExecutor的RejectedExecutionHandler参数提供拒绝策略，来处理线程池超载情况。ThreadPoolExecutor自带的拒绝策略如下：AbortPolicy策略：该策略直接抛出异常，阻止系统正常工作CallerRunsPolicy策略：如果线程池的线程数量达到上限，该策略会把任务队列中的任务放在调用者线程中运行DiscardOledestPolicy策略：该策略会丢弃任务队列中最老的一个任务，也就是当前任务队列中最先被添加进去的DiscardPolicy策略：该策略会丢弃无法处理的任务，不予任何处理以上内置的策略都实现了RejectedExecutionHandler接口，当然也可以自定义拒绝策略import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(5), Executors.defaultThreadFactory(), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println(r.toString() + "执行了拒绝策略"); } }); for (int i = 0; i < 10; i++) { pool.execute( new Runnable() { @Override public void run() { try { //让线程阻塞，使后续任务进入后续队列 Thread.sleep(1000); //输出执行线程的名称 System.out.println("执行中的线程名字：" + Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } ); } pool.shutdown(); } } ------输出结果------- com.if010.thread.ThreadPool$2@60e53b93执行了拒绝策略 com.if010.thread.ThreadPool$2@5e2de80c执行了拒绝策略 com.if010.thread.ThreadPool$2@1d44bcfa执行了拒绝策略 执行中的线程名字：pool-1-thread-1 执行中的线程名字：pool-1-thread-2 执行中的线程名字：pool-1-thread-1 执行中的线程名字：pool-1-thread-2 执行中的线程名字：pool-1-thread-2 执行中的线程名字：pool-1-thread-1 执行中的线程名字：pool-1-thread-2当任务加入了休眠阻塞，执行需要花费一定时间，导致会有一定的任务被丢弃，从而执行自定义的拒绝策略。ThreadPoolExecutor扩展beforeExecute：线程池中的任务运行前执行afterExecute：线程池中的任务运行完毕后执行terminated：线程池退出后执行通过这三个接口，可以监控每个任务的开始和结束时间import java.util.concurrent.*; public class ThreadPool { private static ExecutorService pool; public static void main(String[] args) { pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(5), new ThreadFactory() { @Override public Thread newThread(Runnable r) { System.out.println("线程 " + r.hashCode() + " 创建"); //线程命名 Thread th = new Thread(r, "线程 " + r.hashCode()); return th; } }, new ThreadPoolExecutor.CallerRunsPolicy()) { @Override protected void beforeExecute(Thread t, Runnable r) { System.out.println("准备执行 " + ((ThreadTask) r).getTaskName()); } @Override protected void afterExecute(Runnable r, Throwable t) { System.out.println("执行完毕 " + ((ThreadTask) r).getTaskName()); } @Override protected void terminated() { System.out.println("线程池退出"); } }; for (int i = 0; i < 10; i++) { pool.execute(new ThreadTask("Task" + i)); } pool.shutdown(); } } class ThreadTask implements Runnable { private String taskName; public String getTaskName() { return taskName; } public void setTaskName(String taskName) { this.taskName = taskName; } public ThreadTask(String taskName) { this.taskName = taskName; } @Override public void run() { //输出执行线程的名称 System.out.println("任务名称：" + this.getTaskName() + " 线程名称" + Thread.currentThread().getName()); } } ------输出结果------- 线程 1627674070 创建 线程 1360875712 创建 准备执行 Task0 任务名称：Task0 线程名称线程 1627674070 线程 1625635731 创建 准备执行 Task1 执行完毕 Task0 任务名称：Task1 线程名称线程 1360875712 准备执行 Task2 线程 1580066828 创建 任务名称：Task2 线程名称线程 1627674070 执行完毕 Task2 执行完毕 Task1 准备执行 Task8 任务名称：Task8 线程名称线程 1580066828 执行完毕 Task8 准备执行 Task5 任务名称：Task5 线程名称线程 1580066828 执行完毕 Task5 准备执行 Task7 任务名称：Task7 线程名称线程 1625635731 执行完毕 Task7 准备执行 Task6 准备执行 Task4 准备执行 Task3 任务名称：Task3 线程名称线程 1360875712 任务名称：Task4 线程名称线程 1627674070 执行完毕 Task4 任务名称：Task6 线程名称线程 1580066828 执行完毕 Task6 准备执行 Task9 任务名称：Task9 线程名称线程 1625635731 执行完毕 Task9 执行完毕 Task3 线程池退出对于这三个方法的重写，可以对线程池中线程的运行状态进行监控，在其执行前后打印相关信息。使用shutdown方法可以比较安全的关闭线程池，当调用该方法后，线程池不再接受后续添加的任务，但是此时线程池不会马上退出，而是等到添加到线程池中的任务都已经完成处理后，才会退出。线程分配流程线程安全如果有多个线程在同时运行，而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的，而且其他的变量的值也和预期的是一样的，就是线程安全的。我们通过一个案例，演示线程的安全问题电影院要卖票，我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”，本次电影的座位共10个(本场电影只能卖10张票)。/** * 模拟电影院的售票窗口，实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票) * 窗口，线程对象来模拟 * 票，Runnable接口子类来模拟 */ public class MovieTestDemo { public static void main(String[] args) { //创建Runnable接口实现类对象 Runnable t = new Tickets(); //创建3个Thread类对象，传递Runnable接口实现类 Thread t0 = new Thread(t); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t0.start(); t1.start(); t2.start(); } } class Tickets implements Runnable{ //定义出10张票 private int tickets = 10; @Override public void run() { while (true){ //对票数进行判断，大于0则可以出售，变量--操作 if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票"); } else { System.out.println("售票结束"); break; } } } } ---------输出结果------ Thread-1 出售第10张票 Thread-0 出售第10张票 Thread-2 出售第9张票 Thread-0 出售第8张票 Thread-1 出售第7张票 Thread-2 出售第6张票 Thread-1 出售第5张票 Thread-0 出售第4张票 Thread-2 出售第3张票 Thread-1 出售第2张票 Thread-0 出售第1张票 售票结束 Thread-2 出售第0张票 售票结束 Thread-1 出售第-1张票 售票结束运行结果发现：上面程序出现了问题票出现了重复的票错误的票 0、-1其实，线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作，而无写操作，一般来说，这个全局变量是线程安全的；若有多个线程同时执行写操作，一般都需要考虑线程同步，否则的话就可能影响线程安全。线程安全处理Synchronizedjava中提供了线程同步机制，它能够解决上述的线程安全问题。线程同步的方式有两种：同步代码块同步方法同步代码块同步代码块: 在代码块声明上 加上synchronizedsynchronized (锁对象) { 可能会产生线程安全问题的代码 }同步代码块中的锁对象可以是任意的对象；但多个线程时，要使用同一个锁对象才能够保证线程安全。使用同步代码块，对电影院卖票案例中Ticket类进行如下代码修改：/** * 模拟电影院的售票窗口，实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票) * 窗口，线程对象来模拟 * 票，Runnable接口子类来模拟 */ public class MovieTestDemo { public static void main(String[] args) { //创建Runnable接口实现类对象 Runnable t = new Tickets(); //创建3个Thread类对象，传递Runnable接口实现类 Thread t0 = new Thread(t); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t0.start(); t1.start(); t2.start(); } } class Tickets implements Runnable{ //定义出10张票 private int tickets = 10; Object lock = new Object(); @Override public void run() { while (true){ synchronized (lock) { //对票数进行判断，大于0则可以出售，变量--操作 if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票"); } else { System.out.println("售票结束"); break; } } } } }同步方法同步方法：在方法声明上加上synchronizedpublic synchronized void method(){ //可能会产生线程安全问题的代码 }同步方法中的锁对象是 thispublic void method(){ synchronized(this) { //可能会产生线程安全问题的代码 } }使用同步方法，对电影院卖票案例中Ticket类进行如下代码修改：/** * 模拟电影院的售票窗口，实现多个窗口同时卖"功夫熊猫3"这场电影票(多个窗口一起卖这10张票) * 窗口，线程对象来模拟 * 票，Runnable接口子类来模拟 */ public class MovieTestDemo { public static void main(String[] args) { //创建Runnable接口实现类对象 Runnable t = new Tickets(); //创建3个Thread类对象，传递Runnable接口实现类 Thread t0 = new Thread(t); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t0.start(); t1.start(); t2.start(); } } class Tickets implements Runnable{ //定义出10张票 private int tickets = 10; @Override public void run() { while (true){ if (buyTickets() == false){ System.out.println("售票结束"); return; } } } public synchronized boolean buyTickets(){ if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票"); return true; } else { return false; } } }静态同步方法: 在方法声明上加上static synchronized，public static synchronized void method(){ //可能会产生线程安全问题的代码 }静态同步方法中的锁对象是 类名.classpublic static void method(){ synchronized(MovieTestDemo.class) { //可能会产生线程安全问题的代码 } }死锁同步锁使用的弊端：[font color="red"]当线程任务中出现了多个同步(多个锁)时，如果同步中嵌套了其他的同步。这时容易引发一种现象：程序出现无限等待，这种现象我们称为死锁。[/font]这种情况能避免就避免掉。synchronzied(A锁){ synchronized(B锁){ //肯能产生线程安全问题的代码 } }实现死锁：public class TestLock { public static void main(String[] args) { LockDemo lockDemo = new LockDemo(); Thread t0 = new Thread(lockDemo); Thread t1 = new Thread(lockDemo); t0.start();t1.start(); } } class LockDemo implements Runnable{ private int i=0; @Override public void run() { while (true){ if (i%2 == 0){ //先拿A锁，再那B锁 synchronized (LockA.class){ System.out.println(Thread.currentThread().getName()+"号玩家在 if 已拿到 A 锁"); synchronized (LockB.class){ System.out.println(Thread.currentThread().getName()+"号玩家在 if 已拿到 B 锁"); } } } else { //先拿B锁，再那A锁 synchronized (LockB.class){ System.out.println(Thread.currentThread().getName()+"号玩家在 else 已拿到 B 锁"); synchronized (LockA.class){ System.out.println(Thread.currentThread().getName()+"号玩家在 else 已拿到 A 锁"); } } } i++; } } } class LockA{ private LockA(){} public static final LockA locka = new LockA(); } class LockB{ private LockB(){} public static final LockB lockb = new LockB(); }Lock接口查阅API，查阅Lock接口描述，Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。Lock提供了一个更加面对对象的锁，在该锁中提供了更多的操作锁的功能。我们使用Lock接口，以及其中的lock()方法和unlock()方法替代同步，对电影院卖票案例中Ticket类进行如下代码修改：import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 使用JDK1.5之后的接口Lock，替换同步代码块，实现线程的安全性 * Lock接口方法： * lock() 获取锁 * unlock() 释放锁 * 实现类ReentrantLock */ public class MovieTestDemo { public static void main(String[] args) { //创建Runnable接口实现类对象 Runnable t = new Tickets(); //创建3个Thread类对象，传递Runnable接口实现类 Thread t0 = new Thread(t); Thread t1 = new Thread(t); Thread t2 = new Thread(t); t0.start(); t1.start(); t2.start(); } } class Tickets implements Runnable{ //定义出售的票源 private int tickets = 10; //在类成员位置，创建Lock接口实现类对象 private Lock lock = new ReentrantLock(); @Override public void run() { while (true){ //调用Lock接口方法lock获取锁 lock.lock(); try { if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + " 出售第" + tickets-- + "张票"); } else { System.out.println("售票结束"); return; } } catch (Exception e){ } finally { //释放锁，调用Lock接口方法unlock lock.unlock(); } } } }等待唤醒机制在开始讲解等待唤醒机制之前，有必要搞清一个概念——线程之间的通信：多个线程在处理同一个资源，但是处理的动作（线程的任务）却不相同。通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。等待唤醒机制所涉及到的方法：wait（） :等待，将正在执行的线程释放其执行资格 和 执行权，并存储到线程池中。notify（）：唤醒，唤醒线程池中被wait（）的线程，一次唤醒一个，而且是任意的。notifyAll（）： 唤醒全部：可以将线程池中的所有wait() 线程都唤醒。其实，所谓唤醒的意思就是让 线程池中的线程具备执行资格。必须注意的是，这些方法都是在 同步中才有效。同时这些方法在使用时必须标明所属锁，这样才可以明确出这些方法操作的到底是哪个锁上的线程。仔细查看JavaAPI之后，发现这些方法 并不定义在 Thread中，也没定义在Runnable接口中，却被定义在了Object类中，为什么这些操作线程的方法定义在Object类中？因为这些方法在使用时，必须要标明所属的锁，而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。接下里，我们先从一个简单的示例入手:如上图说示，输入线程向Resource中输入name ,sex , 输出线程从资源中输出，先要完成的任务是：当input发现Resource中没有数据时，开始输入，输入完成后，叫output来输出。如果发现有数据，就wait();当output发现Resource中没有数据时，就wait(); 当发现有数据时，就输出，然后，叫醒input来输入数据。下面代码，模拟等待唤醒机制的实现：/** * 定义资源类，有两个成员变量name，sex * 同时有两个线程,对资源中的变量操作 * 线程一对name,age赋值 * 线程二对name,age输出 */ public class Resource { public String name; public String sex; public boolean flag = false; } /** * 输入的线程，对资源对象Resource中的成员变量赋值 */ class Input implements Runnable{ private Resource r; public Input(Resource r) { this.r = r; } @Override public void run() { int i = 0; while (true) { synchronized (r) { if (r.flag){ try { r.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } else { if (i % 2 == 0) { r.name = "小王"; r.sex = "男"; } else { r.name = "小林"; r.sex = "女"; } i++; r.flag = true; r.notify(); } } } } } /** * 输出线程，对Resource的成员变量进行打印输出 */ class Output implements Runnable{ private Resource r; public Output(Resource r) { this.r = r; } @Override public void run() { while (true){ synchronized (r) { if (r.flag) { System.out.println(r.name + "\t" + r.sex); r.flag = false; r.notify(); } else { try { r.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } } } /** * 开启输入线程和输出线程 */ class TestDome{ public static void main(String[] args) { Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread t_in = new Thread(in); Thread t_out = new Thread(out); t_in.start();t_out.start(); } }这里要注意的是wait和notify需要使用锁对象进行调用，否则会报以下错误：Exception in thread "Thread-1" java.lang.IllegalMonitorStateException at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at com.if010.thread.Output.run(Resource.java:73) at java.lang.Thread.run(Thread.java:750)
JDBC概述JDBC（Java Data Base Connectivity,java数据库连接）是一种用于执行SQL语句的Java API，可以为多种关系数据库提供统一访问，它由一组用Java语言编写的类和接口组成。是Java访问数据库的标准规范JDBC提供了一种基准,据此可以构建更高级的工具和接口，使数据库开发人员能够编写数据库应用程序。JDBC需要连接驱动，驱动是两个设备要进行通信，满足一定通信数据格式，数据格式由设备提供商规定，设备提供商为设备提供驱动软件，通过软件可以与该设备进行通信。JDBC原理Java提供访问数据库规范称为JDBC，而生产厂商提供规范的实现类称为驱动。JDBC是接口，驱动是接口的实现，没有驱动将无法完成数据库连接，从而不能操作数据库！每个数据库厂商都需要提供自己的驱动，用来连接自己公司的数据库，也就是说驱动一般都由数据库生成厂商提供。JDBC开发步骤注册驱动获得连接获得语句执行平台执行sql语句处理结果释放资源驱动下载官网下载：https://dev.mysql.com/downloads/connector/j/这里我们使用的是MySQL的驱动mysql-connector-j-8.0.33导入驱动jar包创建lib目录，用于存放当前项目需要的所有jar包，选择jar包，右键执行build path -> Add to Build PathAPI详解：注册驱动代码：Class.forName("com.mysql.cj.jdbc.Driver");JDBC规范定义驱动接口：java.sql.Driver，MySql驱动包提供了实现类：com.mysql.jdbc.DriverDriverManager工具类，提供注册驱动的方法 registerDriver()，方法的参数是java.sql.Driver，所以我们可以通过如下语句进行注册:DriverManager.registerDriver(new com.mysql.jdbc.Driver());以上代码不推荐使用，存在两方面不足硬编码，后期不易于程序扩展和维护驱动会被注册两次。通常开发我们使用Class.forName() 加载一个使用字符串描述的驱动类。如果使用Class.forName()将类加载到内存，该类的静态代码将自动执行。通过查询com.mysql.jdbc.Driver源码，我们发现Driver类“主动”将自己进行注册public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } …… }API详解：获得链接代码：Connection con = DriverManager.getConnection(“jdbc:mysql://localhost:3306/mydb”,”root”,”root”);获取连接需要方法 DriverManager.getConnection(url,username,password)，三个参数分别表示，url 需要连接数据库的位置（网址），user用户名，password 密码url比较复杂，下面是mysql的url：jdbc:mysql://localhost:3306/testJDBC规定url的格式由三部分组成，每个部分中间使用冒号分隔。第一部分是jdbc，这是固定的；第二部分是数据库名称，那么连接mysql数据库，第二部分当然是mysql了；第三部分是由数据库厂商规定的，我们需要了解每个数据库厂商的要求，mysql的第三部分分别由数据库服务器的IP地址（localhost）、端口号（3306），以及DATABASE名称(test)组成。API详解：获得语句执行平台String sql = "某SQL语句";获取Statement语句执行平台：Statement stmt = con.createStatement();常用方法：int executeUpdate(String sql); --执行insert update delete语句ResultSet executeQuery(String sql); --执行select语句boolean execute(String sql); --执行select返回true 执行其他的语句返回falseAPI详解：处理结果集[font color="red"]注意：执行insert、update、delete无需处理[/font]ResultSet实际上就是一张二维的表格，我们可以调用其boolean next()方法指向某行记录，当第一次调用next()方法时，便指向第一行记录的位置，这时就可以使用ResultSet提供的getXXX(int col)方法(与索引从0开始不同个，列从1开始)来获取指定列的数据：rs.next(); //指向第一行 rs.getInt(1); //获取第一行第一列的数据常用方法：Object getObject(int index) / Object getObject(String name) --获得任意对象String getString(int index) / Object getObject(String name) --获得字符串int getInt(int index) / Object getObject(String name) --获得整形double getDouble(int index) / Object getObject(String name) --获得双精度浮点型API详解：释放资源与IO流一样，使用后的东西都需要关闭！关闭的顺序是先得到的后关闭，后得到的先关闭。rs.close(); stmt.close(); con.close();开发示例import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; /** * JDBC操作数据库的步骤 * 1、注册驱动 * 告知JVM使用哪个数据库的驱动 * 2、获得连接 * 使用JDBC中的类，完成对数据库的连接 * 3、获得语句执行平台 * 通过连接对象获取对SQL语句的执行者对象 * 4、执行sql语句 * 使用执行者对象，向数据库执行SQL语句，获取到数据库执行后的结果 * 5、处理结果 * 6、释放资源 */ public class JDBCDemo { public static void main(String[] args) throws Exception { //1、注册驱动（反射技术，将驱动类加入到内容） //使用java.sql.DriverManager类静态方法，registerDriver Driver(driver) //Driver是一个接口，传递参数应该是MySQL驱动的实现类 //DriverManager.registerDriver(new Driver()); //驱动类源码，注册2次驱动程序 Class.forName("com.mysql.cj.jdbc.Driver"); //2、获得数据库连接 DriverManager类中静态方法 //static Connection getConnection(String url, String username, String password) //返回值Connection接口的实现类，实现类是MySQL驱动里提供的 //url 数据库地址 jdbc:mysql://IP地址:端口号/数据库名称 String url = "jdbc:mysql://127.0.0.1:3306/test"; String username = "root"; String password = "123456"; Connection conn = DriverManager.getConnection(url, username, password); //3、获得语句执行平台，通过数据库连接对象Connection，获取到获取到SQL语句的执行者对象 //Connection对象调用方法 Statement createStatement() 获取Statement对象，将SQL语句发送到数据库 //返回值Statement接口的实现类，实现类是MySQL驱动里提供的 Statement stat = conn.createStatement(); //4、执行sql语句 //通过执行者对象调用方法执行SQL语句，获取结果 //int executeUpdate(String sql) 仅限于执行insert、update、delete这三种SQL语句 //返回值int是操作成功了多少行数据 int row = stat.executeUpdate("INSERT INTO user(username,password) VALUES ('admin','123456');"); System.out.println("成功执行：" + row + "条数据"); //ResultSet executeQuery(String sql) 仅限于执行select这种SQL语句 //返回值ResultSet接口的实现类，实现类是MySQL驱动里提供的 ResultSet rs = stat.executeQuery("SELECT * FROM user WHERE username='admin';"); //5、处理结果 //ResultSet接口方法boolean next()，返回true则为有结果，反之false则为没有结果 while(rs.next()) { //获取每列数据，使用ResultSet接口的方法getXXX System.out.println( rs.getString("username") + " " + rs.getString("password") ); } //6、释放资源 rs.close(); stat.close(); conn.close(); } }SQL注入问题假设有登录案例SQL语句如下：SELECT * FROM 用户表 WHERE NAME = 用户输入的用户名 AND PASSWORD = 用户输的密码;此时，当用户输入正确的账号与密码后，查询到了信息则让用户登录。但是当用户输入的账号为XXX 密码为：XXX’ OR ‘a’=’a时，则真正执行的代码变为：SELECT * FROM 用户表 WHERE NAME = ‘XXX’ AND PASSWORD =’ XXX’ OR ’a’=’a’;此时，上述查询语句时永远可以查询出结果的。那么用户就直接登录成功了，显然我们不希望看到这样的结果，这便是SQL注入问题，为此，我们使用PreparedStatement预处理对象来解决对应的问题。PreparedStatement预处理对象使用PreparedStatement预处理对象时，建议每条sql语句所有的实际参数，都使用逗号分隔。String sql = "insert into sort(sid,sname) values(?,?)";; PreparedStatement预处理对象代码： PreparedStatement psmt = conn.prepareStatement(sql)执行SQL语句:int executeUpdate(); --执行insert update delete语句.ResultSet executeQuery(); --执行select语句.boolean execute(); --执行select返回true 执行其他的语句返回false.设置实际参数void setXxx(int index, Xxx xx) 将指定参数设置为给定Java的xx值。在将此值发送到数据库时，驱动程序将它转换成一个 SQL Xxx类型值。例如：setString(2, "家用电器") 把SQL语句中第2个位置的占位符？替换成实际参数 "家用电器"executeQuery方法查询记录：selectimport java.sql.*; /** * Statement接口实现类，用作执行SQL语句，返回对应结果 * 有一个子接口PreparedStatement，SQL预编译存储，多次高效执行SQL语句 * PreparedStatement的实现类在数据库的驱动当中，是Connection连接数据库对象的方法 * PreparedStatement preparedStatement(String sql) */ public class PreparedStatementSelectDemo { public static void main(String[] args) throws Exception { //注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //获得连接 Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/test", "root", "123456" ); //调用Connection接口方法PreparedStatement，获取PreparedStatement接口的实现类 //方法中的参数，SQL语句中的全部参数采用？(占位符) //调用PreparedStatement的set方法设置占位符的参数 String sql = "select * from user where username=? and password=?"; PreparedStatement pstat = conn.prepareStatement(sql); pstat.setObject(1,"admin"); pstat.setObject(2,"admin"); //执行sql语句 ResultSet rs = pstat.executeQuery(); //处理结果 while(rs.next()) { System.out.println( rs.getString("username") + " " + rs.getString("password") ); } //释放资源 rs.close(); pstat.close(); conn.close(); } }executeUpdate方法插入记录：insertimport java.sql.*; public class PreparedStatementInsertDemo { public static void main(String[] args) throws Exception { //注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //获得连接 Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/test", "root", "123456" ); //PreparedStatement接口实现数据添加 String sql = "INSERT INTO user(username,password) VALUES (?,?);"; PreparedStatement pstat = conn.prepareStatement(sql); pstat.setString(1,"admin"); pstat.setString(2,"123456"); //执行sql语句 int result = pstat.executeUpdate(); //处理结果 System.out.println("成功执行：" + result + "条数据"); //释放资源 pstat.close(); conn.close(); } }更新记录：updateimport java.sql.*; public class PreparedStatementUpdateDemo { public static void main(String[] args) throws Exception { //注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //获得连接 Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/test", "root", "123456" ); //PreparedStatement接口实现数据更新 String sql = "update user set username=?,password=? where id=?"; PreparedStatement pstat = conn.prepareStatement(sql); pstat.setObject(1,"admin"); pstat.setString(2,"123456"); pstat.setInt(3,1); //执行sql语句 int result = pstat.executeUpdate(); //处理结果 System.out.println("成功执行：" + result + "条数据"); //释放资源 pstat.close(); conn.close(); } }删除记录：deleteimport java.sql.*; public class PreparedStatementDeleteDemo { public static void main(String[] args) throws Exception { //注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //获得连接 Connection conn = DriverManager.getConnection( "jdbc:mysql://127.0.0.1:3306/test", "root", "123456" ); //PreparedStatement接口实现数据删除 String sql = "delete from user where id=?;"; PreparedStatement pstat = conn.prepareStatement(sql); pstat.SetInt(1,1); //执行sql语句 int result = pstat.executeUpdate(); //处理结果 System.out.println("成功执行：" + result + "条数据"); //释放资源 pstat.close(); conn.close(); } }自建JDBC工具类“获得数据库连接”操作，将在以后的增删改查所有功能中都存在，可以封装工具类JDBCUtils。提供获取连接对象的方法，从而达到代码的重复利用。该工具类提供方法：public static Connection getConnection()import java.sql.*; /** * 实现JDBC工具类 * 定义方法，直接返回数据库连接对象 */ public class JDBCUtils { private JDBCUtils(){} private static Connection conn; static { try { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e){ System.out.println("数据库驱动注册失败！"); } String url = "jdbc:mysql://127.0.0.1:3306/test"; String username = "root"; String password = "123456"; conn = DriverManager.getConnection(url, username, password); } catch (Exception e){ throw new RuntimeException(e+"数据库连接失败！"); } } /** * 定义静态方法，返回数据库的连接对象 */ public static Connection getConnection(){ return conn; } /** * 定义静态方法，实现executeQuery方面的释放资源 * 关闭顺序 ResultSet -> Statement -> Connection * @param conn 数据库连接对象 * @param stat 数据预处理对象 * @param rs 数据结果集 */ public static void cloes(Connection conn, Statement stat, ResultSet rs){ if (rs != null){ try{ rs.close(); } catch (SQLException e){ System.out.println("ResultSet释放失败！"); } } if (stat != null){ try{ stat.close(); } catch (SQLException e){ System.out.println("Statement释放失败！"); } } if (conn != null){ try{ conn.close(); } catch (SQLException e){ System.out.println("Connection释放失败！"); } } } /** * 定义静态方法，实现executeUpdate方面的释放资源 * 关闭顺序 Statement -> Connection * @param conn * @param stat */ public static void cloes(Connection conn, Statement stat){ if (stat != null){ try{ stat.close(); } catch (SQLException e){ System.out.println("Statement释放失败！"); } } if (conn != null){ try{ conn.close(); } catch (SQLException e){ System.out.println("Connection释放失败！"); } } } }import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class TestJDBCUtils { public static void main(String[] args) throws Exception { Connection conn = JDBCUtils.getConnection(); String sql = "select * from user"; PreparedStatement pst = conn.prepareStatement(sql); ResultSet rs = pst.executeQuery(); while (rs.next()){ System.out.println(rs.getInt("id")+" "+rs.getString("username")); } JDBCUtils.cloes(conn,pst,rs); } }使用properties配置文件开发中获得连接的4个参数（驱动、URL、用户名、密码）通常都存在配置文件中，方便后期维护，程序如果需要更换数据库，只需要修改配置文件即可。通常情况下，我们习惯使用properties文件，此文件我们将做如下要求：文件位置：任意，建议src下文件名称：任意，扩展名为properties文件内容：一行一组数据，格式是“key=value”.key命名自定义，如果是多个单词，习惯使用点分隔。例如：jdbc.drivervalue值不支持中文，如果需要使用非英文字符，将进行unicode转换创建配置文件在项目根目录下，文件夹”config“，然后再创建文件“db.properties”url=jdbc:mysql://localhost:3306/test user=root password=123456加载配置文件对应properties文件处理，开发中也使用Properties对象进行。我们将采用加载properties文件获得流，然后使用Properties对象进行处理import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.sql.*; import java.util.Properties; /** * 实现JDBC工具类 * 定义方法，连接数据库的方法和释放资源的方法 * 加载properties配置文件，IO读取文件键值对存储到集合 * 从集合中以键值对方式获取数据库连接信息，完成数据库的连接 * 返回数据库连接对象 */ public class JDBCUtils { private JDBCUtils(){} private static Connection conn; private static String driver; private static String url; private static String username; private static String password; static { try { //使用Properties处理流,使用load()方法加载指定的流 Properties dbconfig = new Properties(); Reader rd = new FileReader("./config/db.properties"); dbconfig.load(rd); //获取配置信息，键值对 driver = dbconfig.getProperty("driver"); url = dbconfig.getProperty("url"); username = dbconfig.getProperty("username"); password = dbconfig.getProperty("password"); try { try { Class.forName(driver); } catch (ClassNotFoundException e){ System.out.println("数据库驱动注册失败！"); } conn = DriverManager.getConnection(url, username, password); } catch (Exception e){ throw new RuntimeException(e+"数据库连接失败！"); } } catch (IOException e){ throw new RuntimeException(e+"数据库配置信息读取失败！"); } } /** * 定义静态方法，返回数据库的连接对象 */ public static Connection getConnection(){ return conn; } /** * 定义静态方法，实现executeQuery方面的释放资源 * 关闭顺序 ResultSet -> Statement -> Connection * @param conn 数据库连接对象 * @param stat 数据预处理对象 * @param rs 数据结果集 */ public static void cloes(Connection conn, Statement stat, ResultSet rs){ if (rs != null){ try{ rs.close(); } catch (SQLException e){ System.out.println("ResultSet释放失败！"); } } if (stat != null){ try{ stat.close(); } catch (SQLException e){ System.out.println("Statement释放失败！"); } } if (conn != null){ try{ conn.close(); } catch (SQLException e){ System.out.println("Connection释放失败！"); } } } /** * 定义静态方法，实现executeUpdate方面的释放资源 * 关闭顺序 Statement -> Connection * @param conn * @param stat */ public static void cloes(Connection conn, Statement stat){ if (stat != null){ try{ stat.close(); } catch (SQLException e){ System.out.println("Statement释放失败！"); } } if (conn != null){ try{ conn.close(); } catch (SQLException e){ System.out.println("Connection释放失败！"); } } } }DBUtils如果只使用JDBC进行开发，我们会发现冗余代码过多，为了简化JDBC开发，本案例我们讲采用apache commons组件一个成员：DBUtils。DBUtils就是JDBC的简化开发工具包。需要项目导入commons-dbutils-1.6.jar才能够正常使用DBUtils工具。驱动下载官网下载：https://commons.apache.org/proper/commons-dbutils/download_dbutils这里我们使用commons-dbutils-1.6-bin概述DBUtils是java编程中的数据库操作实用工具，小巧简单实用，DBUtils封装了对JDBC的操作，简化了JDBC操作，可以少写代码Dbutils三个核心功能介绍QueryRunner中提供对sql语句操作的API.ResultSetHandler接口，用于定义select操作后，怎样封装结果集.DbUtils类，它就是一个工具类,定义了关闭资源与事务处理的方法QueryRunner核心类实现添加、更新、删除操作以下示例使用上述提到的JDBCUtils工具类获取数据库连接对象，自定义的工具类传递插入记录：insertimport org.apache.commons.dbutils.QueryRunner; import java.sql.Connection; import java.sql.SQLException; /** * 使用QueryRunner类，实现对数据表的 * insert、update、delete * 调用QueryRunner类的方法update(Connection conn, String sql, Object...param) * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 */ public class QueryRunnerDemo { public static void main(String[] args) throws SQLException { //创建QueryRunner对象 QueryRunner queryRunner = new QueryRunner(); //定义SQL语句 String sql = "INSERT INTO user(username,password) VALUES (?,?);"; //将占位符的实际参数以数组的方式存储 Object[] param = {"admin","123456"}; //获取数据库连接 Connection conn = JDBCUtils.getConnection(); //调用QueryRunner类的方法update执行SQL语句 int row = queryRunner.update(conn, sql, param); //释放资源，并输出执行结果数 conn.close(); System.out.println("成功执行：" + row + "条数据"); } }更新记录：updateimport org.apache.commons.dbutils.QueryRunner; import java.sql.Connection; import java.sql.SQLException; /** * 使用QueryRunner类，实现对数据表的 * insert、update、delete * 调用QueryRunner类的方法update(Connection conn, String sql, Object...param) * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 */ public class QueryRunnerDemo { public static void main(String[] args) throws SQLException { //创建QueryRunner对象 QueryRunner queryRunner = new QueryRunner(); //定义SQL语句 String sql = "UPDATE user SET password=? WHERE id=?;"; //将占位符的实际参数以数组的方式存储 Object[] param = {"123456", 1}; //获取数据库连接 Connection conn = JDBCUtils.getConnection(); //调用QueryRunner类的方法update执行SQL语句 int row = queryRunner.update(conn, sql, param); //释放资源，并输出执行结果数 conn.close(); System.out.println("成功执行：" + row + "条数据"); } }删除记录：delete如果删除没有符合条件的数据，返回的数字为0import org.apache.commons.dbutils.QueryRunner; import java.sql.Connection; import java.sql.SQLException; /** * 使用QueryRunner类，实现对数据表的 * insert、update、delete * 调用QueryRunner类的方法update(Connection conn, String sql, Object...param) * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 */ public class QueryRunnerDemo { public static void main(String[] args) throws SQLException { //创建QueryRunner对象 QueryRunner queryRunner = new QueryRunner(); //定义SQL语句 String sql = "DELETE from user WHERE id=?;"; //将占位符的实际参数以数组的方式存储 Object[] param = {1}; //获取数据库连接 Connection conn = JDBCUtils.getConnection(); //调用QueryRunner类的方法update执行SQL语句 int row = queryRunner.update(conn, sql, param); //释放资源，并输出执行结果数 conn.close(); System.out.println("成功执行：" + row + "条数据"); } }实现查询操作ResultSetHandler结果集处理类实现类描述ArrayHandler将结果集中的第一条记录封装到一个Object[]数组中，数组中的每一个元素就是这条记录中的每一个字段的值ArrayListHandler将结果集中的每一条记录都封装到一个Object[]数组中，将这些数组在封装到List集合中BeanHandler将结果集中第一条记录封装到一个指定的javaBean中BeanListHandler将结果集中每一条记录封装到指定的javaBean中，将这些javaBean在封装到List集合中ColumnListHandler将结果集中指定的列的字段值，封装到一个List集合中ScalarHandle它是用于单数据。例如select count(*) from 表 操作MapHandler将结果集第一行封装到Map集合中,Key列名, Value该列数据MapListHandler将结果集第一行封装到Map集合中,Key列名, Value该列数据,Map集合存储到List集合JavaBeanJavaBean就是一个类(概念类)，在开发中常用封装数据。具有如下特性需要实现接口：java.io.Serializable ，通常实现接口这步骤省略了，不会影响程序提供私有字段：private 类型 字段名提供getter/setter方法提供无参构造ArrayHandler与ArrayListHandler查询import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ArrayHandler; import org.apache.commons.dbutils.handlers.ArrayListHandler; import java.sql.Connection; import java.sql.SQLException; import java.util.List; /** * 使用QueryRunner类，实现对数据表的查询操作 * 调用QueryRunner类的方法query(Connection conn, String sql, ResultSetHandler r, Object...param) * ResultSetHandler r 结果集的处理方式，传递ResultSetHandler接口的实现类 * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 * 注意：query方法返回的结果是 <T>T 泛型，具体返回值类型，跟随结果集处理方式的变化 */ public class QueryRunnerDemo2 { private static Connection conn = JDBCUtils.getConnection(); private static String sql = "SELECT * FROM user;"; /** * ResultSetHandler接口的实现类ArrayHandler查询方法 * 将结果集的第一行存储到Object[]类型对象数组中 */ public static void arrayHandler() throws SQLException { QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递连接对象、SQL语句、结果集处理方式的实现类,返回对象数组 Object[] result = queryRunner.query(conn, sql, new ArrayHandler()); //可以通过数组长度判断是否有结果，如果数据长度为0，则表示无结果，若大于0，则表示有结果 System.out.println("结果数组长度："+result.length); //输出结果 for(Object obj : result){ System.out.print(obj+"\t"); } } /** * ResultSetHandler接口的实现类ArrayListHandler查询方法 * 将结果集的每一行封装到对象数组当中，对象数据会存储到List集合中 */ public static void arrayListHandler() throws SQLException{ QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递连接对象、SQL语句、结果集处理方式的实现类,返回List集合类型,结合里装的是对象数组 List<Object[]> result = queryRunner.query(conn, sql, new ArrayListHandler()); //可以通过集合大小判断是否有结果，如果集合大小为0，则表示无结果，若大于0，则表示有结果 System.out.println("结果集合大小："+result.size()); //输出结果 for(Object[] objs : result){ for (Object obj : objs) { System.out.print(obj+"\t"); } System.out.println(); } } public static void main(String[] args) throws SQLException { arrayHandler(); arrayListHandler(); } }BeanHandler与BeanListHandler查询import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.BeanListHandler; import java.sql.Connection; import java.sql.SQLException; import java.util.List; /** * 使用QueryRunner类，实现对数据表的查询操作 * 调用QueryRunner类的方法query(Connection conn, String sql, ResultSetHandler r, Object...param) * ResultSetHandler r 结果集的处理方式，传递ResultSetHandler接口的实现类 * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 * 注意：query方法返回的结果是 <T>T 泛型，具体返回值类型，跟随结果集处理方式的变化 */ public class QueryRunnerDemo2 { private static Connection conn = JDBCUtils.getConnection(); private static String sql = "SELECT * FROM user;"; /** * ResultSetHandler接口的实现类BeanHandler查询方法 * 将结果集的第一行封装成JavaBean对象 * 注意：被封装成JavaBean对象，必须要有空参构造器 */ public static void beanHandler() throws SQLException { QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递结果集实现类BeanHandler //BeanHandler User u = queryRunner.query(conn, sql, new BeanHandler<User>(User.class)); //可以通过对象是否为null判断是否有查询到数据 if (u == null){ System.out.println("查询不到相关数据"); } //输出结果 System.out.println(u); } /** * ResultSetHandler接口的实现类BeanListHandler查询方法 * 将结果集的每一行封装成JavaBean对象，再将JavaBean对象封装到List集合当中 */ public static void beanListHandler() throws SQLException{ QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递连接对象、SQL语句、结果集处理方式的实现类,返回List集合类型,结合里装的是对象数组 List<User> result = queryRunner.query(conn, sql, new BeanListHandler<User>(User.class)); //可以通过集合大小判断是否有结果，如果集合大小为0，则表示无结果，若大于0，则表示有结果 System.out.println("结果集合大小："+result.size()); //输出结果 for(User u : result){ System.out.println(u); } } public static void main(String[] args) throws SQLException { beanHandler(); beanListHandler(); } }ColumnListHandler与ScalarHandler查询import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ColumnListHandler; import org.apache.commons.dbutils.handlers.ScalarHandler; import java.sql.Connection; import java.sql.SQLException; import java.util.List; /** * 使用QueryRunner类，实现对数据表的查询操作 * 调用QueryRunner类的方法query(Connection conn, String sql, ResultSetHandler r, Object...param) * ResultSetHandler r 结果集的处理方式，传递ResultSetHandler接口的实现类 * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 * 注意：query方法返回的结果是 <T>T 泛型，具体返回值类型，跟随结果集处理方式的变化 */ public class QueryRunnerDemo2 { private static Connection conn = JDBCUtils.getConnection(); private static String sql = "SELECT * FROM user;"; /** * ResultSetHandler接口的实现类ColumnListHandler查询方法 * 将结果集的指定列存储到List集合当中，List<Object>每个列的数据类型都不同 */ public static void columnListHandler () throws SQLException { QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递结果集实现类ColumnListHandler List<Object> result = queryRunner.query(conn, sql, new ColumnListHandler<Object>("username")); //可以通过集合大小判断是否有结果，如果集合大小为0，则表示无结果，若大于0，则表示有结果 System.out.println("结果集合大小："+result.size()); //输出结果 for (Object username : result) { System.out.println(username); } } /** * ResultSetHandler接口的实现类ScalarHandler查询方法，适用于单数据结果 */ public static void scalarHandler() throws SQLException{ QueryRunner queryRunner = new QueryRunner(); sql = "SELECT COUNT(*) FROM user;"; //调用方法query执行查询，传递连接对象、SQL语句、结果集处理方式的实现类 Long result = queryRunner.query(conn, sql, new ScalarHandler<Long>()); //输出结果 System.out.println(result); } public static void main(String[] args) throws SQLException { columnListHandler(); scalarHandler(); } }MapHandler与MapListHandler查询import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.MapHandler; import org.apache.commons.dbutils.handlers.MapListHandler; import java.sql.Connection; import java.sql.SQLException; import java.util.List; import java.util.Map; /** * 使用QueryRunner类，实现对数据表的查询操作 * 调用QueryRunner类的方法query(Connection conn, String sql, ResultSetHandler r, Object...param) * ResultSetHandler r 结果集的处理方式，传递ResultSetHandler接口的实现类 * Object...param 可变参数，Object类型，为SQL中出现的？占位符进行赋值，没有占位符可不传参数 * 注意：query方法返回的结果是 <T>T 泛型，具体返回值类型，跟随结果集处理方式的变化 */ public class QueryRunnerDemo2 { private static Connection conn = JDBCUtils.getConnection(); private static String sql = "SELECT * FROM user;"; /** * ResultSetHandler接口的实现类MapHandler查询方法 * 将结果集的第一行结果封装到Map集合当中 * Map<key:value> key->列 value->数据 */ public static void mapHandler() throws SQLException { QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递结果集实现类MapHandler，继承于LinkedHashMap,有序 Map<String,Object> result = queryRunner.query(conn, sql, new MapHandler()); //可以通过集合是否为null判断是否有结果,如果为null直接输出，则会报错 if (result != null){ System.out.println("结果集合大小："+result.size()); //输出结果 for (String key : result.keySet()){ System.out.println(key+":"+result.get(key)); } } } /** * ResultSetHandler接口的实现类MapListHandler查询方法 * 将结果集的每一行结果封装到一个Map集合当中，再将Map集合添加到List集合当中 * Map<key:value> key->列 value->数据 */ public static void mapListHandler() throws SQLException{ QueryRunner queryRunner = new QueryRunner(); //调用方法query执行查询，传递连接对象、SQL语句、结果集处理方式的实现类,返回List集合类型,结合里装的是Map集合 //继承于LinkedHashMap,有序 List<Map<String,Object>> result = queryRunner.query(conn, sql, new MapListHandler()); //输出结果 for (Map<String,Object> map : result) { for (String key : map.keySet()) { System.out.print(key+":"+map.get(key)+"\t"); } System.out.println(); } } public static void main(String[] args) throws SQLException { mapHandler(); mapListHandler(); } }连接池实际开发中“获得连接”或“释放资源”是非常消耗系统资源的两个过程，为了解决此类性能问题，通常情况我们采用连接池技术，来共享连接Connection。这样我们就不需要每次都创建连接、释放连接了，这些操作都交给了连接池连接池概述概念用池来管理Connection，这样可以重复使用Connection。有了池，所以我们就不用自己来创建Connection，而是通过池来获取Connection对象。当使用完Connection后，调用Connection的close()方法也不会真的关闭Connection，而是把Connection“归还”给池。池就可以再利用这个Connection对象了。规范Java为数据库连接池提供了公共的接口：javax.sql.DataSource，各个厂商需要让自己的连接池实现这个接口。这样应用程序可以方便的切换不同厂商的连接池！常见的连接池：DBCP、C3P0DBCP连接池开发DBCP也是一个开源的连接池，是Apache Common成员之一，在企业开发中也比较常见，tomcat内置的连接池驱动下载Commons-DBCP官网下载：https://commons.apache.org/proper/commons-dbcp/download_dbcp.cgiCommons-Pool官网下载：https://commons.apache.org/proper/commons-dbcp/download_pool.cgiCommons-Logging官网下载：https://commons.apache.org/proper/commons-logging/download_logging.cgi这里我们使用commons-dbcp2-2.9.0-bin、commons-pool2-2.11.1-bin、commons-logging-1.2-bin常见配置项属性描述是否必须设置driverClassName数据库驱动名称是url数据库的地址是username用户名是password密码是maxActive最大连接数量否minIdle最小空闲连接否maxIdle最大空闲连接否initialSize初始化连接否参考文档：http://commons.apache.org/proper/commons-dbcp/configuration.html编写实现类import org.apache.commons.dbcp2.BasicDataSource; import java.sql.Connection; import java.sql.SQLException; /** * 在连接池dbcp包中定义了BasicDataSource类，实现了数据源的规范接口javax.sql.DataSource */ public class DataSourceDemo { public static void main(String[] args) throws SQLException { //创建DataSource接口的实现类对象BasicDataSource BasicDataSource dataSource = new BasicDataSource(); //设置连接数据库最基本的4个参数,driver、url、username、password dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test"); dataSource.setUsername("root"); dataSource.setPassword("123456"); //调用对象方法getConnection获取数据库连接 Connection conn = dataSource.getConnection(); } }编写工具类import org.apache.commons.dbcp2.BasicDataSource; import javax.sql.DataSource; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.util.Properties; /** * 使用DBCP实现数据库连接池 * 连接池配置读取db.properties文件 */ public class JDBCUtils2 { private static BasicDataSource dataSource = new BasicDataSource(); static { try { //使用Properties处理流,使用load()方法加载指定的流 Properties dbConfig = new Properties(); Reader rd = new FileReader("./config/db.properties"); dbConfig.load(rd); //读取db.properties文件对连接池进行配置 dataSource.setDriverClassName(dbConfig.getProperty("driver")); dataSource.setUrl(dbConfig.getProperty("url")); dataSource.setUsername(dbConfig.getProperty("username")); dataSource.setPassword(dbConfig.getProperty("password")); dataSource.setInitialSize(Integer.parseInt(dbConfig.getProperty("InitialSize"))); dataSource.setMaxIdle(Integer.parseInt(dbConfig.getProperty("MaxIdle"))); dataSource.setMinIdle(Integer.parseInt(dbConfig.getProperty("MinIdle"))); } catch (IOException e) { throw new RuntimeException(e+"数据库配置信息读取失败！"); } } public static DataSource getConnection(){ return dataSource; } }import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ArrayListHandler; import java.sql.SQLException; import java.util.List; /** * 测试JDBCUtils2工具类 * JDBCUtils2工具类提供的是一个DataSource接口的数据源 * QueryRunner类构造方法可以接收DataSource接口的实现类，然后调用update、query方法，无需传递他们Connection对象 */ public class JDBCUtils2Test { private static QueryRunner queryRunner = new QueryRunner(JDBCUtils2.getConnection()); public static void main(String[] args) { insert(); select(); } public static void insert(){ String sql = "INSERT INTO user(username,password) VALUES (?,?)"; Object[] param = {"admin","123123"}; try { int result = queryRunner.update(sql,param); System.out.println("【提示】成功插入"+result+"条数据"); } catch (SQLException e){ throw new RuntimeException(e+"数据添加失败!"); } } public static void select(){ String sql = "SELECT * FROM user"; List<Object[]> result = null; try { result = queryRunner.query(sql, new ArrayListHandler()); } catch (SQLException e) { throw new RuntimeException(e+"查询异常或错误"); } if (result.size() != 0) { for (Object[] objs : result) { for (Object obj : objs){ System.out.print(obj+"\t"); } System.out.println(); } } else { System.out.println("查询不到相关记录"); } } }关于报错如果数据库服务没有正常运行连接不上，则会提示以下报错信息：java.sql.SQLException: Cannot create PoolableConnectionFactory (Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.) at org.apache.commons.dbcp2.BasicDataSource.createPoolableConnectionFactory(BasicDataSource.java:653) at org.apache.commons.dbcp2.BasicDataSource.createDataSource(BasicDataSource.java:531) at org.apache.commons.dbcp2.BasicDataSource.getConnection(BasicDataSource.java:731) at com.if010.test.DataSourceDemo.main(DataSourceDemo.java:23) Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure如果数据库名称输入有误连接不上，则会提示以下报错信息：java.sql.SQLException: Cannot create PoolableConnectionFactory (Unknown database 'atest') at org.apache.commons.dbcp2.BasicDataSource.createPoolableConnectionFactory(BasicDataSource.java:653) at org.apache.commons.dbcp2.BasicDataSource.createDataSource(BasicDataSource.java:531) at org.apache.commons.dbcp2.BasicDataSource.getConnection(BasicDataSource.java:731) at com.if010.test.DataSourceDemo.main(DataSourceDemo.java:23)如果数据库连接的用户名或密码输入有误导致连接不上，则会提示以下报错信息：java.sql.SQLException: Cannot create PoolableConnectionFactory (Access denied for user 'roota'@'172.17.0.1' (using password: YES)) at org.apache.commons.dbcp2.BasicDataSource.createPoolableConnectionFactory(BasicDataSource.java:653) at org.apache.commons.dbcp2.BasicDataSource.createDataSource(BasicDataSource.java:531) at org.apache.commons.dbcp2.BasicDataSource.getConnection(BasicDataSource.java:731) at com.if010.test.DataSourceDemo.main(DataSourceDemo.java:23)如果SQL语句中的占位符与传入参数的个数不对，则会提示以下报错信息：Exception in thread "main" java.sql.SQLException: Wrong number of parameters: expected 2, was given 1 Query: DELETE from user SET password=? WHERE id=?; Parameters: [123456] at org.apache.commons.dbutils.AbstractQueryRunner.rethrow(AbstractQueryRunner.java:392) at org.apache.commons.dbutils.QueryRunner.update(QueryRunner.java:491) at org.apache.commons.dbutils.QueryRunner.update(QueryRunner.java:404) at com.if010.test.QueryRunnerDemo.main(QueryRunnerDemo.java:24)如果SQL语句有错误，或者传入占位符的参数类型与SQL语句不对应，则会提示以下报错信息：Exception in thread "main" java.sql.SQLException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SET password=1 WHERE id=1' at line 1 Query: DELETE from user SET password=? WHERE id=?; Parameters: [1, 1] at org.apache.commons.dbutils.AbstractQueryRunner.rethrow(AbstractQueryRunner.java:392) at org.apache.commons.dbutils.QueryRunner.update(QueryRunner.java:491) at org.apache.commons.dbutils.QueryRunner.update(QueryRunner.java:404) at com.if010.test.QueryRunnerDemo.main(QueryRunnerDemo.java:24)关于BeanHandler中传入的类没有空参构造，则会提示以下报错信息：Exception in thread "main" java.sql.SQLException: Cannot create com.if010.test.User: com.if010.test.User Query: SELECT * FROM user; Parameters: [] at org.apache.commons.dbutils.AbstractQueryRunner.rethrow(AbstractQueryRunner.java:392) at org.apache.commons.dbutils.QueryRunner.query(QueryRunner.java:351) at org.apache.commons.dbutils.QueryRunner.query(QueryRunner.java:226) at com.if010.test.QueryRunnerDemo2.beanHandler(QueryRunnerDemo2.java:30) at com.if010.test.QueryRunnerDemo2.main(QueryRunnerDemo2.java:63)
网络的一些了解网络通信协议链路层：链路层是用于定义物理传输通道，通常是对某些网络连接设备的驱动协议，例如针对光纤、网线提供的驱动。网络层：网络层是整个TCP/IP协议的核心，它主要用于将传输的数据进行分组，将分组数据发送到目标计算机或者网络。传输层：主要使网络程序进行通信，在进行网络通信时，可以采用TCP协议，也可以采用UDP协议。应用层：主要负责应用程序的协议，例如HTTP协议、FTP协议等。IP地址和端口要想使网络中的计算机能够进行通信，必须为每台计算机指定一个标识号，通过这个标识号来指定接受数据的计算机或者发送数据的计算机。在TCP/IP协议中，这个标识号就是IP地址，它可以唯一标识一台计算机，目前，IP地址广泛使用的版本是IPv4，它是由4个字节大小的二进制数来表示，如：00001010000000000000000000000001。由于二进制形式表示的IP地址非常不便记忆和处理，因此通常会将IP地址写成十进制的形式，每个字节用一个十进制数字(0-255)表示，数字间用符号“.”分开，如 “192.168.1.100”,但是有一个地址比较特别，那就是“127.0.0.1”，这个回环地址，代表本机（也就是自己本身）。随着计算机网络规模的不断扩大，对IP地址的需求也越来越多，IPV4这种用4个字节表示的IP地址面临枯竭，因此IPv6 便应运而生了，IPv6使用16个字节表示IP地址，它所拥有的地址容量约是IPv4的8×1028倍，达到2128个（算上全零的），这样就解决了网络地址资源数量不够的问题。通过IP地址可以连接到指定计算机，但如果想访问目标计算机中的某个应用程序，还需要指定端口号。在计算机中，不同的应用程序是通过端口号区分的。端口号是用两个字节（16位的二进制数）表示的，它的取值范围是0-65535，其中，[font color="red"]0-1023之间的端口号用于一些知名的网络服务和应用，用户的普通应用程序需要使用1024以上的端口号[/font]，从而避免端口号被另外一个应用或服务所占用。接下来通过一个图例来描述IP地址和端口号的作用，如下图所示。从上图中可以清楚地看到，位于网络中一台计算机可以通过IP地址去访问另一台计算机，并通过端口号访问目标计算机中的某个应用程序。InetAddress类了解了IP地址的作用，我们看学习下JDK中提供了一个InetAdderss类，该类用于封装一个IP地址，并提供了一系列与IP地址相关的方法，下表中列出了InetAddress类的一些常用方法。上图中，列举了InetAddress的四个常用方法。其中，前两个方法用于获得该类的实例对象，第一个方法用于获得表示指定主机的InetAddress对象，第二个方法用于获得表示本地的InetAddress对象。通过InetAddress对象便可获取指定主机名，IP地址等，接下来通过一个案例来演示InetAddress的常用方法，如下所示。import java.io.IOException; import java.net.InetAddress; /** * 表示互联网中的IP地址 * java.net.InetAddress * 静态方法 * static InetAddress getLocalHost() 获取本地主机IP地址和主机名，返回值InetAddress对象 * static InetAddress getByName(String hostName) 传递主机名，获取IP地址对象 * 非静态方法 * String getHostName() 获取主机IP地址 * String getHostAddress() 获取主机名 */ public class InetAddressDome { public static void main(String[] args) throws IOException { InetAddress local = InetAddress.getLocalHost(); InetAddress remote = InetAddress.getByName("if010.com"); System.out.println("本机的主机名：" + local.getHostName()); System.out.println("本机的IP地址：" + local.getHostAddress()); System.out.println("IF010的主机名为：" + remote.getHostName()); System.out.println("IF010的IP地址：" + remote.getHostAddress()); } } ---------- 输出结果 ---------- 本机的主机名：Kims-MacBook.local 本机的IP地址：127.0.0.1 IF010的主机名为：if010.com IF010的IP地址：27.152.185.64UDP、TCP协议在介绍TCP/IP结构时，提到传输层的两个重要的高级协议，分别是UDP和TCP，其中UDP是User Datagram Protocol的简称，称为用户数据报协议，TCP是Transmission Control Protocol的简称，称为传输控制协议。UDP协议UDP是无连接通信协议，即在数据传输时，数据的发送端和接收端不建立逻辑连接。简单来说，当一台计算机向另外一台计算机发送数据时，发送端不会确认接收端是否存在，就会发出数据，同样接收端在收到数据时，也不会向发送端反馈是否收到数据。由于使用UDP协议消耗资源小，通信效率高，且数据传输一般限制在64KB内，所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议，因为这种情况即使偶尔丢失一两个数据包，也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时，由于UDP的面向无连接性，不能保证数据的完整性，因此在传输重要数据时不建议使用UDP协议。UDP的交换过程如下图所示。TCP协议TCP协议是面向连接的通信协议，即在传输数据前先在发送端和接收端建立逻辑连接，然后再传输数据，它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端，由客户端向服务端发出连接请求，每次连接的创建都需要经过“三次握手”。第一次握手，客户端向服务器端发出连接请求，等待服务器确认第二次握手，服务器端向客户端回送一个响应，通知客户端收到了连接请求第三次握手，客户端再次向服务器端发送确认信息，确认连接。整个交互过程如下图所示。由于TCP协议的面向连接特性，它可以保证传输数据的安全性，所以是一个被广泛采用的协议，例如在下载文件时，如果数据接收不完整，将会导致文件数据丢失而不能被打开，因此，下载文件时必须采用TCP协议。UDP通信DatagramPacket前面介绍了UDP是一种面向无连接的协议，因此，在通信时发送端和接收端不用建立连接。UDP通信的过程就像是货运公司在两个码头间发送货物一样。在码头发送和接收货物时都需要使用集装箱来装载货物，UDP通信也是一样，发送和接收的数据也需要使用“集装箱”进行打包，为此JDK中提供了一个DatagramPacket类，该类的实例对象就相当于一个集装箱，用于封装UDP通信中发送或者接收的数据。想要创建一个DatagramPacket对象，首先需要了解一下它的构造方法。在创建发送端和接收端的DatagramPacket对象时，使用的构造方法有所不同，接收端的构造方法只需要接收一个字节数组来存放接收到的数据，而发送端的构造方法不但要接收存放了发送数据的字节数组，还需要指定发送端IP地址和端口号。接下来根据API文档的内容，对DatagramPacket的构造方法进行逐一详细地讲解。使用该构造方法在创建DatagramPacket对象时，指定了封装数据的字节数组和数据的大小，没有指定IP地址和端口号。很明显，这样的对象只能用于接收端，不能用于发送端。因为发送端一定要明确指出数据的目的地(ip地址和端口号)，而[font color="red"]接收端不需要明确知道数据的来源，只需要接收到数据即可[/font]。使用该构造方法在创建DatagramPacket对象时，不仅指定了封装数据的字节数组和数据的大小，还指定了数据包的目标IP地址（addr）和端口号（port）。该对象通常用于发送端，因为在[font color="red"]发送数据时必须指定接收端的IP地址和端口号[/font]，就好像发送货物的集装箱上面必须标明接收人的地址一样。上面我们讲解了DatagramPacket的构造方法，接下来对DatagramPacket类中的常用方法进行详细地讲解，如下表所示。DatagramSocketDatagramPacket数据包的作用就如同是“集装箱”，可以将发送端或者接收端的数据封装起来。然而运输货物只有“集装箱”是不够的，还需要有码头。在程序中需要实现通信只有DatagramPacket数据包也同样不行，为此JDK中提供的一个DatagramSocket类。DatagramSocket类的作用就类似于码头，使用这个类的实例对象就可以发送和接收DatagramPacket数据包，发送数据的过程如下图所示。在创建发送端和接收端的DatagramSocket对象时，使用的构造方法也有所不同，下面对DatagramSocket类中常用的构造方法进行讲解。该构造方法用于创建发送端的DatagramSocket对象，在创建DatagramSocket对象时，并没有指定端口号，此时，系统会分配一个没有被其它网络程序所使用的端口号。该构造方法既可用于创建接收端的DatagramSocket对象，又可以创建发送端的DatagramSocket对象，在创建接收端的DatagramSocket对象时，必须要指定一个端口号，这样就可以监听指定的端口。上面我们讲解了DatagramSocket的构造方法，接下来对DatagramSocket类中的常用方法进行详细地讲解。UDP通信设计讲解了DatagramPacket和DatagramSocket的作用，接下来通过一个案例来学习一下它们在程序中的具体用法。要实现UDP通信需要创建一个发送端程序和一个接收端程序，很明显，在通信时只有接收端程序先运行，才能避免因发送端发送的数据无法接收，而造成数据丢失。因此，首先需要来完成接收端程序的编写。UDP发送端import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /** * 实现UDP协议的发送端 * 实现封装数据的类 java.net.DatagramPacket 将你的数据包装 * 实现数据传输的类 java.net.DatagramSocket 将你的数据包发送出去 * * 实现步骤： * 1.创建 DatagramPacket 对象，封装数据、接收端的IP地址、接收端的端口 * 2.创建 DatagramSocket 对象 * 3.调用 DatagramSocket 类的方法 send(DatagramPacket dp)，发送数据包 * 4.关闭资源 * * DatagramPacket构造方法： * DatagramPacket(byte[] buf, int length, InetAddress address, int port) * * DatagramSocket构造方法: * DatagramSocket() 空参数 * 方法: send(DatagramPacket d) */ public class UDPSend { public static void main(String[] args) throws IOException { //创建数据包对象,封装要发送的数据,接收端IP,端口 byte[] date = "你好,我是一个UDP数据包".getBytes(); //创建InetAddress对象,封装接收端的IP地址 InetAddress inet = InetAddress.getByName("127.0.0.1"); DatagramPacket dp = new DatagramPacket(date, date.length, inet,6000); //创建DatagramSocket对象,数据包的发送和接收对象 DatagramSocket ds = new DatagramSocket(); //调用ds对象的方法send,发送数据包 ds.send(dp); //关闭资源 ds.close(); } }UDP接收端import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; /** * 实现UDP协议的接收端 * 实现封装数据的类 java.net.DatagramPacket 将数据包拆封 * 实现数据传输的类 java.net.DatagramSocket 接收数据包 * * 实现步骤： * 1.创建 DatagramSocket 对象，绑定端口号，要和发送端端口号一致 * 2.创建字节数组，接收发来的数据 * 3.创建数据包对象 DatagramPacket * 4.调用 DatagramSocket 类的方法 receive(DatagramPacket dp) 接收数据，数据放在数据包中 * 5.拆包 * 发送的IP地址 * 数据包对象DatagramPacket方法getAddress()获取的是发送端的IP地址对象 * 返回值是InetAddress对象 * 接收到的字节个数 * 数据包对象DatagramPacket方法 getLength() * 发送方的端口号 * 数据包对象DatagramPacket方法 getPort()发送端口 * 6.关闭资源 */ public class UDPReceive { public static void main(String[] args) throws IOException { //创建数据包传输对象 DatagramSocket 绑定端口号 DatagramSocket ds = new DatagramSocket(6000); //创建字节数组,最大64KB byte[] data = new byte[1024*64]; //创建数据包对象,传递字节数组 DatagramPacket dp = new DatagramPacket(data, data.length); //调用ds对象的方法receive传递数据包 ds.receive(dp); //获取发送端的IP地址对象 String ip = dp.getAddress().getHostAddress(); //获取发送的端口号 int port = dp.getPort(); //获取接收到的字节个数 int length = dp.getLength(); System.out.println(new String(data,0,length)+"..."+ip+":"+port); //关闭资源 ds.close(); } } ---------- 输出结果 ---------- 你好,我是一个UDP数据包...127.0.0.1:56092模拟简单聊天室案例聊天室客户端import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Scanner; /** * 实现键盘输入聊天客户端，使用UPD协议发送消息 */ public class ChatClient { public static void main(String[] args) throws IOException { Scanner sc = new Scanner(System.in); InetAddress inet = InetAddress.getByName("127.0.0.1"); DatagramSocket ds = new DatagramSocket(); while (true) { System.out.println("请输入你要发送的消息："); String message = sc.nextLine(); byte[] date = message.getBytes(); DatagramPacket dp = new DatagramPacket(date, date.length, inet, 6000); ds.send(dp); } } } ---------- 输出结果 ---------- 请输入你要发送的消息： 你好聊天室服务端import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; /** * 实现键盘输入聊天服务端，使用UPD协议接收消息 * 保持持续接收 */ public class ChatService { public static void main(String[] args) throws IOException { DatagramSocket ds = new DatagramSocket(6000); byte[] data = new byte[1024*64]; while (true) { DatagramPacket dp = new DatagramPacket(data, data.length); ds.receive(dp); String ip = dp.getAddress().getHostAddress(); int port = dp.getPort(); int length = dp.getLength(); System.out.println("接收到来自：" + ip + ":" + port); System.out.println(new String(data, 0, length)); } } } ---------- 输出结果 ---------- 接收到来自：127.0.0.1:52285 你好TCP通信TCP通信同UDP通信一样，都能实现两台计算机之间的通信，通信的两端都需要创建socket对象。区别在于，UDP中只有发送端和接收端，不区分客户端与服务器端，计算机之间可以任意地发送数据。而TCP通信是严格区分客户端与服务器端的，在通信时，必须先由客户端去连接服务器端才能实现通信，服务器端不可以主动连接客户端，并且服务器端程序需要事先启动，等待客户端的连接。在JDK中提供了两个类用于实现TCP程序，[font color="red"]一个是ServerSocket类，用于表示服务器端，一个是Socket类，用于表示客户端[/font]。通信时，首先创建代表服务器端的ServerSocket对象，该对象相当于开启一个服务，并等待客户端的连接，然后创建代表客户端的Socket对象向服务器端发出连接请求，服务器端响应请求，两者建立连接开始通信。ServerSocket通过前面的学习知道，在开发TCP程序时，首先需要创建服务器端程序。JDK的java.net包中提供了一个ServerSocket类，该类的实例对象可以实现一个服务器段的程序。通过查阅API文档可知，ServerSocket类提供了多种构造方法，接下来就对ServerSocket的构造方法进行逐一地讲解。使用该构造方法在创建ServerSocket对象时，就可以将其绑定到一个指定的端口号上（参数port就是端口号）。接下来学习一下ServerSocket的常用方法，如表所示。ServerSocket对象负责监听某台计算机的某个端口号，在创建ServerSocket对象后，需要继续调用该对象的accept()方法，接收来自客户端的请求。当执行了accept()方法之后，服务器端程序会发生阻塞，直到客户端发出连接请求，accept()方法才会返回一个Scoket对象用于和客户端实现通信，程序才能继续向下执行。Socket讲解了ServerSocket对象可以实现服务端程序，但只实现服务器端程序还不能完成通信，此时还需要一个客户端程序与之交互，为此JDK提供了一个Socket类，用于实现TCP客户端程序。通过查阅API文档可知Socket类同样提供了多种构造方法，接下来就对Socket的常用构造方法进行详细讲解。使用该构造方法在创建Socket对象时，会根据参数去连接在指定地址和端口上运行的服务器程序，其中参数host接收的是一个字符串类型的IP地址。该方法在使用上与第二个构造方法类似，参数address用于接收一个InetAddress类型的对象，该对象用于封装一个IP地址。在以上Socket的构造方法中，最常用的是第一个构造方法。接下来学习一下Socket的常用方法，如表所示。方法声明功能描述int getPort()该方法返回一个int类型对象，该对象是Socket对象与服务器端连接的端口号InetAddress getLocalAddress()该方法用于获取Socket对象绑定的本地IP地址，并将IP地址封装成InetAddress类型的对象返回void close()该方法用于关闭Socket连接，结束本次通信。在关闭socket之前，应将与socket相关的所有的输入/输出流全部关闭，这是因为一个良好的程序应该在执行完毕时释放所有的资源InputStream getInputStream()该方法返回一个InputStream类型的输入流对象，如果该对象是由服务器端的Socket返回，就用于读取客户端发送的数据，反之，用于读取服务器端发送的数据OutputStream getOutputStream()该方法返回一个OutputStream类型的输出流对象，如果该对象是由服务器端的Socket返回，就用于向客户端发送数据，反之，用于向服务器端发送数据在Socket类的常用方法中，getInputStream()和getOutStream()方法分别用于获取输入流和输出流。当客户端和服务端建立连接后，数据是以IO流的形式进行交互的，从而实现通信。接下来通过一张图来描述服务器端和客户端的数据传输，如下图所示TCP通讯设计简单的TCP网络程序设计TCP的服务端import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * 实现TCP服务端，接收客户端数据 * 实现TCP服务端程序的类 java.net.ServerSocket * * 构造方法： * ServerSocket(int port) 传递端口号 * * Socket accept() * 必须要获得客户端的套接字Socket，才能读取流信息！！！ */ public class TCPService { public static void main(String[] args) throws IOException { //创建ServerSocket对象，监听6666端口 ServerSocket serverSocket = new ServerSocket(6666); //调用ServerSocket对象的方法accept()，用于获取客户端Socket对象 Socket clientSocket = serverSocket.accept(); //通过客户端Socket对象，获取字节输入流(客户端发来的数据) InputStream inputStream = clientSocket.getInputStream(); byte[] data = new byte[1024]; int len = inputStream.read(data); System.out.println(new String(data,0,len)); //向客户端进行数据回复，字节输出流，通过客户端Socket对象获取字节输出流 OutputStream outputStream = clientSocket.getOutputStream(); outputStream.write("Server is OK!".getBytes()); //关闭资源 clientSocket.close(); serverSocket.close(); } } ---------- 输出结果 ---------- Server, Are you ok?TCP的客户端import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /** * 实现TCP客户端，连接到服务器，并进行数据交换 * 实现TCP客户端程序的类 java.net.Socket * * 构造方法： * Socket(String host, int port) 传递服务器IP和端口号 * 注意：构造方法只要运行，就会和服务器进行连接，连接失败则会抛出异常,所以需要保证服务端是否处于监听 * * OutputStream getOutputStream() 返回套接字的输出流 * 作用：将数据输出，输出到服务器 * * InputStream getInputStream() 返回套接字的输入流 * 作用：从服务端读取数据 * * ！！！客户端与服务端进行数据交换必须使用套接字对象Socket中获取IO流，自己new流是不行的！！！ */ public class TCPClient { public static void main(String[] args) throws IOException { //创建Socket对象，连接服务器 Socket socket = new Socket("127.0.0.1",6666); //通过客户端的套接字对象Socket方法，获取字节输出流，将数据写向服务器 OutputStream outputStream = socket.getOutputStream(); outputStream.write("Server, Are you ok?".getBytes()); //等待服务器回复数据，使用Socket对象中的字节输入流 byte[] data = new byte[1024]; InputStream inputStream = socket.getInputStream(); int len = inputStream.read(data); System.out.println(new String(data,0,len)); socket.close(); } } ---------- 输出结果 ---------- Server is OK!TCP文件上传案例TCP文件上传案例分析目前大多数服务器都会提供文件上传的功能，由于文件上传需要数据的安全性和完整性，很明显需要使用TCP协议来实现。接下来通过一个案例来实现文件上传的功能。如下图所示。代码实现TCP文件上传的服务端import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Random; /** * 实现TCP协议进行文件上传的服务端 * 实现步骤： * 1、创建ServerSocket套接字对象，监听6666端口 * 2、使用ServerSocket套接字对象的accrpt()方法获取客户端的连接Socket对象 * 3、客户端的连接Socket对象获取字节输入流，读取客户端发送的文件 * 4、创建File对象，绑定上传文件夹 * 判断文件夹是否存在，不存在则自动创建 * 5、创建字节输出流，数据目的为File对象所在的文件夹 * 6、字节流读取文件，字节流将文件写入到文件夹当中 * 7、将上传成功的消息写会客户端 * 8、关闭资源 */ public class TCPService { public static void main(String[] args) throws IOException { //创建ServerSocket套接字对象，监听6666端口 ServerSocket serverSocket = new ServerSocket(6666); //获取客户端的连接Socket对象 Socket clentSocket = serverSocket.accept(); System.out.println( "收到来自：" + clentSocket.getLocalAddress() + ":" + clentSocket.getPort() ); //通过客户端连接对象，获取字节输入流，以便读取客户端发来的文件 InputStream inputStream = clentSocket.getInputStream(); //创建File对象，将文件夹封装，并判断文件夹是否存在 File uploadDir = new File("./upload"); if (!uploadDir.exists()){ uploadDir.mkdir(); } //防止文件名重名问题，自定义规则：毫秒值+6位随机数 String filename = System.currentTimeMillis() + new Random().nextInt(999999)+".jpg"; //创建字节输出流，将文件写入到目的文件夹当中 FileOutputStream fos = new FileOutputStream(uploadDir+File.separator+filename); //读写字节数组 int len = 0; byte[] data = new byte[1024*1024]; while ((len = inputStream.read(data)) != -1){ fos.write(data,0,len); } //通过客户端的连接Socket对象获取字节输出流，发回上传成功消息 clentSocket.getOutputStream().write("上传成功".getBytes()); //关闭资源 fos.close(); clentSocket.close(); serverSocket.close(); } }TCP文件上传的客户端package com.if010.uploadfile; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /** * 实现TCP协议进行文件上传的客户端 * 实现步骤： * 1、创建Socket套接字连接服务器 * 2、通过Socket获取字节输出流，写入文件 * 3、使用自己的流对象，读取文件数据源 * FileInputStream * 4、读取文件，使用字节输出流将文件写入服务器 * 5、通过Socket套接字获取字节输入流，读取服务器返回的结果消息 * 6、关闭资源 */ public class TCPClient { public static void main(String[] args) throws IOException { //使用Socket对象，连接服务器 Socket socket = new Socket("127.0.0.1",6666); //获取字节输出流，文件写到服务器 OutputStream outputStream = socket.getOutputStream(); //创建字节输入流，读取本机的文件数据 FileInputStream fileInputStream = new FileInputStream("/Users/kim/Documents/Code/resource/Avatar.jpg"); //开始读写字节数组 int len = 0; byte[] data = new byte[1024*1024]; while ((len = fileInputStream.read(data)) != -1){ outputStream.write(data,0,len); } //给服务器写终止序列,否则服务端会一直等待消息发送 socket.shutdownOutput(); //获取字节输入流，读取服务器返回的结果消息 InputStream inputStream = socket.getInputStream(); len = inputStream.read(data); System.out.println(new String(data,0,len)); //关闭资源 fileInputStream.close(); socket.close(); } }改良实现多线程上传创建Upload类方法，将上面的TCPServer实现方法写入run中，然后再创建多线程版本的TCPThreadServer类，只要收到新的链接就掉一次run方法实现多线程。import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.util.Random; public class Upload implements Runnable { private Socket clentSocket; public Upload(Socket socket) { this.clentSocket = socket; } @Override public void run() { try { System.out.println( "收到来自：" + clentSocket.getLocalAddress() + ":" + clentSocket.getPort() ); //通过客户端连接对象，获取字节输入流，以便读取客户端发来的文件 InputStream inputStream = clentSocket.getInputStream(); //创建File对象，将文件夹封装，并判断文件夹是否存在 File uploadDir = new File("./upload"); if (!uploadDir.exists()){ uploadDir.mkdir(); } //防止文件名重名问题，自定义规则：毫秒值+6位随机数 String filename = System.currentTimeMillis() + new Random().nextInt(999999)+".jpg"; //创建字节输出流，将文件写入到目的文件夹当中 FileOutputStream fos = new FileOutputStream(uploadDir+File.separator+filename); //读写字节数组 int len = 0; byte[] data = new byte[1024*1024]; while ((len = inputStream.read(data)) != -1){ fos.write(data,0,len); } //通过客户端的连接Socket对象获取字节输出流，发回上传成功消息 clentSocket.getOutputStream().write("上传成功".getBytes()); //关闭资源 fos.close(); clentSocket.close(); } catch (IOException e) { } } }import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TCPThreadServer { public static void main(String[] args) throws IOException { //创建ServerSocket套接字对象，监听6666端口 ServerSocket serverSocket = new ServerSocket(6666); //循环获取客户端的连接Socket对象，只要获取到就跑Upload的run上传方法 while (true) { Socket clentSocket = serverSocket.accept(); new Thread(new Upload(clentSocket)).start(); } } }
安装依赖yum install -y openssl-devel openssl-static zlib-devel lzma tk-devel xz-devel bzip2-devel ncurses-devel gdbm-devel readline-devel sqlite-devel gcc libffi-devel 下载Python3.7.0wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz 开始安装1.解压安装包tar -xvf Python-3.7.0.tgz 2.将解压后文件夹迁移至/usr/local，并进入mv Python-3.7.0 /usr/local cd /usr/local/Python-3.7.0/ 3.执行配置文件并编译安装./configure make make install 4.在/usr/bin路径下生成python3的软链接ln -s /usr/local/Python-3.7.0/python /usr/bin/python3 验证安装[root@cn-sz python]# python3 --version Python 3.7.0 [root@cn-sz python]# pip3 --version pip 22.1.1 from /usr/local/lib/python3.7/site-packages/pip (python 3.7)