引言

如果对 Java 中断没有一个全面的了解,可能会误以为被中断的线程将立马退出运行,但事实并非如此。

中断机制是如何工作的?捕获或检测到中断后,是抛出 InterruptedException 还是重设中断状态以及在方法中吞掉中断状态会有什么后果?

Thread.stop 与中断相比又有哪些异同?

什么情况下需要使用中断?

线程池中的异常如何处理?

中断处理的最佳实践?

线程中断基础知识

1、interrupt()

interrupt方法用于中断线程。调用该方法的线程的状态为将被置为"中断"状态。

注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。

2、interrupted() 和 isInterrupted()

首先看一下API中该方法的实现:public static boolean interrupted() { return currentThread().isInterrupted(true);}

该方法就是直接调用当前线程的isInterrupted(true)的方法。

然后再来看一下 API 中 isInterrupted 的实现:public boolean isInterrupted() {return isInterrupted(false);}

该方法却直接调用当前线程的isInterrupted(false)的方法。

因此这两个方法有两个主要区别:

interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)

这两个方法最终都会调用同一个方法-----isInterrupted(Boolean 参数),只不过参数固定为一个是true,一个是false;

注意: isInterrupted(Boolean 参数) 是 isInterrupted() 的重载方法。

由于第二个区别主要体现在调用的方法的参数上,让我们来看一看这个参数是什么含义

先来看一看被调用的方法 isInterrupted(boolean arg)(Thread类中重载的方法)的定义:private native boolean isInterrupted(boolean ClearInterrupted);

原来这是一个本地方法,看不到源码。不过没关系,通过参数名ClearInterrupted我们就能知道,这个参数代表是否要清除状态位。

如果这个参数为true,说明返回线程的状态位后,要清掉原来的状态位(恢复成原来情况)。这个参数为false,就是直接返回线程的状态位。

这两个方法很好区分,只有当前线程才能清除自己的中断位(对应interrupted()方法)

验证例子public class Interrupt {  public static void main(String[] args) throws Exception {

Thread t = new Thread(new Worker());

t.start();

Thread.sleep(200);

t.interrupt();

System.out.println("Main thread stopped.");  }

public static class Worker implements Runnable {  public void run() {

System.out.println("Worker started.");

try {

Thread.sleep(500);  } catch (InterruptedException e) {

System.out.println("Worker IsInterrupted: " +

Thread.currentThread().isInterrupted());  }

System.out.println("Worker stopped.");  }  }  }

内容很简答:主线程main启动了一个子线程Worker,然后让worker睡500ms,而main睡200ms,之后main调用worker线程的interrupt方法去中断worker,worker被中断后打印中断的状态。

下面是执行结果:Worker started.

Main thread stopped.

Worker IsInterrupted: false

Worker stopped.

Worker明明已经被中断,而isInterrupted()方法竟然返回了false,为什么呢?

InterruptedException 描述

在stackoverflow上搜索了一圈之后,发现有网友提到:可以查看抛出InterruptedException方法的JavaDoc(或源代码),于是我查看了Thread.sleep方法的文档,doc中是这样描述这个InterruptedException异常的:InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.

结论:interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。

注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。所以当一个线程处于中断状态时,如果再由wait、sleep以及jion三个方法引起的阻塞,那么JVM会将线程的中断标志重新设置为false,并抛出一个InterruptedException异常,然后开发人员可以中断状态位“的本质作用-----就是程序员根据try-catch功能块捕捉jvm抛出的InterruptedException异常来做各种处理,比如如何退出线程。总之interrupt的作用就是需要用户自己去监视线程的状态位并做处理。”

小结

Thread.currentThread().interrupt(); 这个用于清除中断状态,这样下次调用Thread.interrupted()方法时就会一直返回为true,因为中断标志已经被恢复了。

而调用isInterrupted()只是简单的查询中断状态,不会对状态进行修改。

interrupt()是用来设置中断状态的。返回true说明中断状态被设置了而不是被清除了。我们调用sleep、wait等此类可中断(throw InterruptedException)方法时,一旦方法抛出InterruptedException,当前调用该方法的线程的中断状态就会被jvm自动清除了,就是说我们调用该线程的isInterrupted 方法时是返回false。如果你想保持中断状态,可以再次调用interrupt方法设置中断状态。这样做的原因是,java的中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户。如果你捕获到中断异常,说明当前线程已经被中断,不需要继续保持中断位。

interrupted是静态方法,返回的是当前线程的中断状态。

例如,如果当前线程被中断(没有抛出中断异常,否则中断状态就会被清除),你调用interrupted方法,第一次会返回true。然后,当前线程的中断状态被方法内部清除了。第二次调用时就会返回false。如果你刚开始一直调用isInterrupted,则会一直返回true,除非中间线程的中断状态被其他操作清除了。

多线程中的异常处理

思考下面的问题在java启动的线程里可以抛出异常吗?

在启动的线程里可以捕捉异常吗?

如果可以捕捉异常,对于checked exception和unchecked exception,他们分别有什么的处理方式呢?

线程里抛出异常

我们可以尝试一下在线程里抛异常。

按照我们的理解,假定我们要在某个方法里抛异常,需要在该定义的方法头也加上声明。那么一个最简单的方式可能如下:public class Task implements Runnable {

@Override  public void run() throws Exception {  int number0 = Integer.parseInt("1");  throw new Exception("Just for test");  }  }

可是,如果我们去编译上面这段代码,会发现根本就编译不过去的。系统报的错误是:Task.java:3: error: run() in Task cannot implement run() in Runnable

public void run() throws Exception {

^

overridden method does not throw Exception

1 error

由此我们发现这种方式行不通。也就是说,在线程里直接抛异常是不行的。

可是,这又会引出一个问题,如果我们在线程代码里头确实是产生了异常,那该怎么办呢?

比如说,我们通过一个线程访问一些文件或者对网络进行IO操作,结果产生了异常。或者说访问某些资源的时候系统崩溃了。这样的场景是确实可能会发生的,我们就需要针对这些情况进行进一步的讨论。

异常处理的几种方式

在前面提到的几种在线程访问资源产生了异常的情况。我们可以看,比如说我们访问文件系统的时候,会抛出IOException, FileNotFoundException等异常。我在访问的代码里实际上是需要采用两种方式来处理的。一种是在使用改资源的方法头增加throws IOException, FileNotFoundException等异常的修饰。还有一是直接在这部分的代码块增加try/catch部分。由前面我们的讨论已经发现,在方法声明加throws Exception的方式是行不通的。

那么就只有使用try/catch这么种方式了。

另外,我们也知道,在异常的处理上,一般异常可以分为checked exception和unchecked exception。

作为unchecked exception,他们通常是指一些比较严重系统错误或者系统设计错误,比如Error, OutOfMemoryError或者系统直接就崩溃了。

对于这种异常发生的时候,我们一般是无能为力也没法恢复的。

那么这种情况发生,我们会怎么来处理呢?

checked exception

直接使用 try-catch 进行处理即可,后文也会对这个进行详解。

此处暂时不做展开。

unchecked exception

对于这种unchecked exception,相对来说就会不一样一点。

setUncaughtExceptionHandler(UncaughtExceptionHandler)

实际上,在Thread的定义里有一个实例方法:setUncaughtExceptionHandler(UncaughtExceptionHandler). 这个方法可以用来处理一些unchecked exception。那么,这种情况的场景是如何的呢?

setUncaughtExceptionHandler()方法相当于一个事件注册的入口。

在jdk里面,该方法的定义如下:public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {  checkAccess();

uncaughtExceptionHandler = eh;  }

而UncaughtExceptionHandler则是一个接口,它的声明如下:public interface UncaughtExceptionHandler {  /**

* Method invoked when the given thread terminates due to the

* given uncaught exception.

*

Any exception thrown by this method will be ignored by the

* Java Virtual Machine.

* @param t the thread

* @param e the exception

*/  void uncaughtException(Thread t, Throwable e);  }

在异常发生的时候,我们传入的UncaughtExceptionHandler参数的uncaughtException方法会被调用。

综合前面的讨论,我们这边要实现handle unchecked exception的方法的具体步骤可以总结如下:定义一个类实现UncaughtExceptionHandler接口。在实现的方法里包含对异常处理的逻辑和步骤。

定义线程执行结构和逻辑。这一步和普通线程定义一样。

在创建和执行改子线程的方法里在thread.start()语句前增加一个thread.setUncaughtExceptionHandler语句来实现处理逻辑的注册。

例子

下面,我们就按照这里定义的步骤来实现一个示例:

首先是实现UncaughtExceptionHandler接口部分:import java.lang.Thread.UncaughtExceptionHandler;

public class ExceptionHandler implements UncaughtExceptionHandler {  public void uncaughtException(Thread t, Throwable e) {

System.out.printf("An exception has been captured

");

System.out.printf("Thread: %s

", t.getId());

System.out.printf("Exception: %s: %s

",

e.getClass().getName(), e.getMessage());

System.out.printf("Stack Trace:

");

e.printStackTrace(System.out);

System.out.printf("Thread status: %s

", t.getState());  }  }

这里我们添加的异常处理逻辑很简单,只是把线程的信息和异常信息都打印出来。

然后,我们定义线程的内容,这里,我们故意让该线程产生一个unchecked exception:public class Task implements Runnable {

@Override  public void run() {  int number0 = Integer.parseInt("TTT");  }  }

从这代码里我们可以看到,Integer.parseInt()里面的参数是错误的,肯定会抛出一个异常来。

现在,我们再把创建线程和注册处理逻辑的部分补上来public class Main {  public static void main(String[] args) {

Task task = new Task();

Thread thread = new Thread(task);

thread.setUncaughtExceptionHandler(new ExceptionHandler());

thread.start();  }  }

整体运行结果日志如下:An exception has been captured

Thread: 8

Exception: java.lang.NumberFormatException: For input string: "TTT"

Stack Trace:

java.lang.NumberFormatException: For input string: "TTT"

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)

at java.lang.Integer.parseInt(Integer.java:492)

at java.lang.Integer.parseInt(Integer.java:527)

at Task.run(Task.java:5)

at java.lang.Thread.run(Thread.java:722)

Thread status: RUNNABLE

这部分的输出正好就是我们前面实现UncaughtExceptionHandler接口的定义。

因此,对于unchecked exception,我们也可以采用类似事件注册的机制做一定程度的处理。

中断的原理

Java 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。

这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。

Java 中断模型也是这么简单,每个线程对象里都有一个 boolean 类型的标识(不一定就要是 Thread 类的字段,实际上也的确不是,这几个方法最终都是通过 native 方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。

例如,当线程 t1 想中断线程 t2,只需要在线程 t1 中将线程 t2 对象的中断标识置为 true,然后线程 2 可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。

Thread 类中断相关的方法

java.lang.Thread 类提供了几个方法来操作这个中断状态,这些方法包括:方法说明public static boolean interrupted测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

public boolean isInterrupted()测试线程是否已经中断。线程的中断状态不受该方法的影响。

public void interrupt()中断线程。

其中,interrupt 方法是唯一能将中断状态设置为 true 的方法。静态方法 interrupted 会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。

上面的例子中,线程 t1 通过调用 interrupt 方法将线程 t2 的中断状态置为 true,t2 可以在合适的时候调用 interrupted 或 isInterrupted 来检测状态并做相应的处理。

此外,类库中的有些类的方法也可能会调用中断,如 FutureTask 中的 cancel 方法,如果传入的参数为 true,它将会在正在运行异步任务的线程上调用 interrupt 方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么 cancel 方法中的参数将不会起到什么效果;又如 ThreadPoolExecutor 中的 shutdownNow 方法会遍历线程池中的工作线程并调用线程的 interrupt 方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。

中断的处理

既然 Java 中断机制只是设置被中断线程的中断状态,那么被中断线程该做些什么?

处理时机

显然,作为一种协作机制,不会强求被中断线程一定要在某个点进行处理。

实际上,被中断线程只需在合适的时候处理即可,如果没有合适的时间点,甚至可以不处理,这时候在任务处理层面,就跟没有调用中断方法一样。“合适的时候”与线程正在处理的业务逻辑紧密相关。

例如,每次迭代的时候,进入一个可能阻塞且无法中断的方法之前等,但多半不会出现在某个临界区更新另一个对象状态的时候,因为这可能会导致对象处于不一致状态。

处理时机决定着程序的效率与中断响应的灵敏性。频繁的检查中断状态可能会使程序执行效率下降,相反,检查的较少可能使中断请求得不到及时响应。如果发出中断请求之后,被中断的线程继续执行一段时间不会给系统带来灾难,那么就可以将中断处理放到方便检查中断,同时又能从一定程度上保证响应灵敏度的地方。当程序的性能指标比较关键时,可能需要建立一个测试模型来分析最佳的中断检测点,以平衡性能和响应灵敏性。

处理方式

1、 中断状态的管理

一般说来,当可能阻塞的方法声明中有抛出 InterruptedException 则暗示该方法是可中断的,如 BlockingQueue#put、BlockingQueue#take、Object#wait、Thread#sleep 等,如果程序捕获到这些可中断的阻塞方法抛出的 InterruptedException 或检测到中断后,这些中断信息该如何处理?一般有以下两个通用原则:

如果遇到的是可中断的阻塞方法抛出 InterruptedException,可以继续向方法调用栈的上层抛出该异常,如果是检测到中断,则可清除中断状态并抛出 InterruptedException,使当前方法也成为一个可中断的方法。

若有时候不太方便在方法上抛出 InterruptedException,比如要实现的某个接口中的方法签名上没有 throws InterruptedException,这时就可以捕获可中断方法的 InterruptedException 并通过 Thread.currentThread.interrupt() 来重新设置中断状态。如果是检测并清除了中断状态,亦是如此。

一般的代码中,尤其是作为一个基础类库时,绝不应当吞掉中断,即捕获到 InterruptedException 后在 catch 里什么也不做,清除中断状态后又不重设中断状态也不抛出 InterruptedException 等。因为吞掉中断状态会导致方法调用栈的上层得不到这些信息。

当然,凡事总有例外的时候,当你完全清楚自己的方法会被谁调用,而调用者也不会因为中断被吞掉了而遇到麻烦,就可以这么做。

总得来说,就是要让方法调用栈的上层获知中断的发生。假设你写了一个类库,类库里有个方法 amethod,在 amethod 中检测并清除了中断状态,而没有抛出 InterruptedException,作为 amethod 的用户来说,他并不知道里面的细节,如果用户在调用 amethod 后也要使用中断来做些事情,那么在调用 amethod 之后他将永远也检测不到中断了,因为中断信息已经被 amethod 清除掉了。如果作为用户,遇到这样有问题的类库,又不能修改代码,那该怎么处理?只好在自己的类里设置一个自己的中断状态,在调用 interrupt 方法的时候,同时设置该状态,这实在是无路可走时才使用的方法。

2、 中断的响应

程序里发现中断后该怎么响应?

这就得视实际情况而定了。有些程序可能一检测到中断就立马将线程终止,有些可能是退出当前执行的任务,继续执行下一个任务……作为一种协作机制,这要与中断方协商好,当调用 interrupt 会发生些什么都是事先知道的,如做一些事务回滚操作,一些清理工作,一些补偿操作等。若不确定调用某个线程的 interrupt 后该线程会做出什么样的响应,那就不应当中断该线程。

4. Thread.interrupt VS Thread.stop

Thread.stop 方法已经不推荐使用了。而在某些方面 Thread.stop 与中断机制有着相似之处。如当线程在等待内置锁或 IO 时,stop 跟 interrupt 一样,不会中止这些操作;当 catch 住 stop 导致的异常时,程序也可以继续执行,虽然 stop 本意是要停止线程,这么做会让程序行为变得更加混乱。

那么它们的区别在哪里?

最重要的就是中断需要程序自己去检测然后做相应的处理,而 Thread.stop 会直接在代码执行过程中抛出 ThreadDeath 错误,这是一个 java.lang.Error 的子类。import java.util.Arrays;import java.util.Random;import java.util.concurrent.TimeUnit;public class TestStop {private static final int[] array = new int[80000];private static final Thread t = new Thread() {public void run() {try {

System.out.println(sort(array));} catch (Error err) {

err.printStackTrace();}

System.out.println("in thread t");}};static {

Random random = new Random();for(int i = 0; i

array[i] = random.nextInt(i + 1);}}private static int sort(int[] array) {for (int i = 0; i

array[j] = array[j + 1];

array[j + 1] = temp;}}}return array[0];}public static void main(String[] args) throws Exception {

t.start();

TimeUnit.SECONDS.sleep(1);

System.out.println("go to stop thread t");

t.stop();

System.out.println("finish main");}}

这个例子很简单,线程 t 里面做了一个非常耗时的排序操作,排序方法中,只有简单的加、减、赋值、比较等操作,一个可能的执行结果如下:go to stop thread t

java.lang.ThreadDeath

at java.lang.Thread.stop(Thread.java:758)

at com.ticmy.interrupt.TestStop.main(TestStop.java:44)

finish main

in thread t

这里 sort 方法是个非常耗时的操作,也就是说主线程休眠一秒钟后调用 stop 的时候,线程 t 还在执行 sort 方法。就是这样一个简单的方法,也会抛出错误!换一句话说,调用 stop 后,大部分 Java 字节码都有可能抛出错误,哪怕是简单的加法!

stop 为什么被禁用

如果线程当前正持有锁,stop 之后则会释放该锁。由于此错误可能出现在很多地方,那么这就让编程人员防不胜防,极易造成对象状态的不一致。

例如,对象 obj 中存放着一个范围值:最小值 low,最大值 high,且 low 不得大于 high,这种关系由锁 lock 保护,以避免并发时产生竞态条件而导致该关系失效。假设当前 low 值是 5,high 值是 10,当线程 t 获取 lock 后,将 low 值更新为了 15,此时被 stop 了,真是糟糕,如果没有捕获住 stop 导致的 Error,low 的值就为 15,high 还是 10,这导致它们之间的小于关系得不到保证,也就是对象状态被破坏了!如果在给 low 赋值的时候 catch 住 stop 导致的 Error 则可能使后面 high 变量的赋值继续,但是谁也不知道 Error 会在哪条语句抛出,如果对象状态之间的关系更复杂呢?这种方式几乎是无法维护的,太复杂了!如果是中断操作,它决计不会在执行 low 赋值的时候抛出错误,这样程序对于对象状态一致性就是可控的。

正是因为可能导致对象状态不一致,stop 才被禁用。

5. 中断的使用

中断的场景

通常,中断的使用场景有以下几个:点击某个桌面应用中的取消按钮时;

某个操作超过了一定的执行时间限制需要中止时;

多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;

&l

java 线程 异常中断_java多线程并发之旅-19-InterruptedException 中断异常处理及中断机制...相关推荐

  1. java 线程面试题_JAVA多线程面试题(一)

    1.进程和线程的区别 一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用.而线程是在进程中执行的一个任务.Java运行环境是一个包含了不同的类和程序的单一进 ...

  2. java线程代码实现_Java 多线程代码实现讲解

    作为一个完全面向对象的语言,Java提供了类 java.lang.Thread 来方便多线程编程,这个类提供了大量的方法来方便我们控制自己的各个线程.那么如何提供给 Java 我们要线程执行的代码呢? ...

  3. java线程池 锁_java多线程——锁

    这是多线程系列第四篇,其他请关注以下: 如果你看过前面几篇关于线程的文字,会对线程的实现原理了然于胸,有了理论的支持会对实践有更好的指导,那么本篇会偏重于线程的实践,对线程的几种应用做个简要的介绍. ...

  4. java线程知识梳理_Java多线程——多线程相关知识的逻辑关系梳理

    1 学习多线程知识的根本目标 多线程知识的根本目标是:设计稳健的并发程序. 当然,本文无法回答这个实践性很强的问题(这与具体的业务相关,涉及到具体的策略),本文主要阐述相关知识之间的关系,希望初学者不 ...

  5. java线程下载文件_Java多线程下载文件实例详解

    本文实例为大家分享了Java多线程下载文件的具体代码,供大家参考,具体内容如下 import java.io.File; import java.io.InputStream; import java ...

  6. java 线程锁概念_Java多线程——锁概念与锁优化

    为了性能与使用的场景,Java实现锁的方式有非常多.而关于锁主要的实现包含synchronized关键字.AQS框架下的锁,其中的实现都离不开以下的策略. 悲观锁与乐观锁 乐观锁.乐观的想法,认为并发 ...

  7. java线程异步传值_Java 多线程传值的四种方法

    Java 多线程传值的四种方法 作者: sunjs 更新时间:2020-09-11 15:20:16 原文链接 其实大家都知道多线程传值有三种方式: 1:通过构造方法传递数据 2:通过变量和方法传递数 ...

  8. java线程怎么用_Java多线程基本使用

    一.概念 1.进程 1.1进程:是一个正在进行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元. 1.2线程:就是进程中一个独立的控制单元,线程在控制着进程的执行,一 ...

  9. java线程集合点_Java多线程学习笔记(三) 甚欢篇

    使人有乍交之欢,不若使其无久处之厌 <小窗幽记>很多时候,我们需要的都不是再多一个线程,我们需要的线程是许多个,我们需要让他们配合.同时我们还有一个愿望就是复用线程,就是将线程当做一个工人 ...

最新文章

  1. 折腾了一晚上Windows网络却未果
  2. UFS和eMMC闪存差异在哪?
  3. webstorm+nodejs+JetBrains IDE Support+chrome打造前端开发神器
  4. iphone网络交互json实现
  5. C++描述杭电OJ 2016.数据的交换输出 ||
  6. 4 转推流格式_ppt 转 pdf怎么转?这个宝藏技巧不会太亏了!
  7. denyhosts、中文文档乱码、端口占用查询
  8. asp.net性能优化之使用Redis缓存(入门)
  9. ajax beforeSend中无效果
  10. 伪指令相当于c语言的什么,ARM汇编中关于“.word”伪指令的概念(转)
  11. jQuery静态方法(each、map、isFunction, isWindow, isArray, .trim(), .holdReady())
  12. 伯克利推出世界最快的KVS数据库Anna:秒杀Redis和Cassandra
  13. ASP.NET在线考试系统+文档说明
  14. 上海电力学院计算机软件技术大作业,计算机硬件技术大作业报告2.doc
  15. 快速将正式环境的数据同步到本地测试库
  16. android10获取WiFi名称 已经连接的WiFi名称 SSID
  17. 用html语言制作表白动画,抖音很火的卡通做我女朋友表白html源码
  18. uniapp ->video 黑屏 无时长(新手容易遇到的坑)
  19. FFmpeg源码分析:swr_convert()音频格式转换
  20. Vue3基础难点总结

热门文章

  1. VMware Workstation之MacOS系统安装
  2. nmn几岁可以吃,nmn多少岁可以吃,归纳总结!
  3. 在Windows Server 2008系统环境下无损调整分区
  4. java集合练习题十六个国家随机分为四组
  5. mysql不停机主从配置
  6. 易课寄在线购课系统开发笔记(二十五)--完成课程详情页面展示相关功能(应用Redis缓存)
  7. python 语言基础 - 你不得不知道的字符串常用函数之isdigit
  8. Kafka服务不可用(宕机)问题踩坑记
  9. Python读取dll库报错:[WinError 126]找不到指定的模块
  10. word中自动生成递增数字