一、进程和多线程的概念以及线程的优点

  打开Windo任务管理器可以看到很多正在运行着的exe程序,完全可以将运行在内存中的exe文件理解成进程,进程是受操作系统管理的基本运行单元。

  线程可以理解成在进程中独立运行的子任务。比如,QQ.exe运行时就有很多的子任务在同时运行。

  使用线程,可以最大限度地利用CPU的空闲时间来处理其他的任务,CPU在人物之间不停地切换,由于切换速度非常快,所以人们感觉这些任务似乎在同时运行。也就是说看,可以在同一时间内运行更多不同种类的任务,可以大幅增加CPU的利用率。

  二、使用多线程

  一个进程正在运行时至少会有一个线程在运行。

 package test;

 public class Test {

     public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
} }

  输出为:main。这里输出的main其实就是一个名称叫做main的线程在执行main()方法中的代码。

  1.继承Thread类

  实现多线程编程的方式主要有两种:一种是继承Thread类,另一种是实现Runnable接口。从Thread类的结构来看,Thread类实现了Runnable接口,它们之间具有多台关系。为了支持多继承,完全可以实现Runnable接口的形式,一边实现一边继承。

  1.1线程的调用的随机性

 public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
}
 package test;

 import com.mythread.www.MyThread;

 public class Run {

     public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();
System.out.println("运行结束");
} }
运行结束
MyThread

  从输出结果可以看出,代码的运行结果与代码执行顺序或调用顺序是无关的,线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。

  1.2 演示线程的随机性

 package mythread;

 public class MyThread extends Thread {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
int time = (int) (Math.random() * 1000);
Thread.sleep(time);
System.out.println("run=" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
 package test;

 import mythread.MyThread;

 public class Test {
public static void main(String[] args) {
try { MyThread thread = new MyThread();
thread.setName("myThread");
thread.start(); for (int i = 0; i < 10; i++) {
int time = (int) (Math.random() * 1000);
Thread.sleep(time);
System.out.println("main=" + Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
main=main
main=main
run=myThread
main=main
run=myThread
run=myThread
main=main
main=main
run=myThread
main=main
main=main
run=myThread
main=main
main=main
run=myThread
run=myThread
run=myThread
main=main
run=myThread
run=myThread

  从输出的20个结果可以看出,Thread.java类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用thread.run()方法就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。

  1.3 start()方法的顺序并不代表线程启动的顺序

 package extthread;

 public class MyThread extends Thread {

     private int i;

     public MyThread(int i) {
super();
this.i = i;
} @Override
public void run() {
System.out.println(i);
} }
 package test;

 import extthread.MyThread;

 public class Test {

     public static void main(String[] args) {
MyThread t11 = new MyThread(1);
MyThread t12 = new MyThread(2);
MyThread t13 = new MyThread(3);
MyThread t14 = new MyThread(4);
MyThread t15 = new MyThread(5);
MyThread t16 = new MyThread(6);
MyThread t17 = new MyThread(7);
MyThread t18 = new MyThread(8);
MyThread t19 = new MyThread(9);
MyThread t110 = new MyThread(10);
MyThread t111 = new MyThread(11);
MyThread t112 = new MyThread(12);
MyThread t113 = new MyThread(13); t11.start();
t12.start();
t13.start();
t14.start();
t15.start();
t16.start();
t17.start();
t18.start();
t19.start();
t110.start();
t111.start();
t112.start();
t113.start(); } }
2
3
1
4
5
7
6
8
10
9
11
12
13

  从输出结果可以看出,start()方法的顺序并不代表线程启动的顺序。

  2.实现Runnable接口

  如果想要创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口。

 package myrunnable;

 public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("myrunnable类中的run!");
}
}
 package test;

 import myrunnable.MyRunnable;

 public class Run {

     public static void main(String[] args) {
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
System.out.println("run中的main!");
} }
run中的main!
myrunnable类中的run!

  Thread.java类的8个构造函数中,有两个可以传递Runnable接口的对象,并且由于Thread类也实现了Runnable接口,所以也就意味着构造函数Thread(Runnable target)不光可以传入Runnable接口的对象, 还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。

  3.实例变量与线程安全

  自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。

  3.1 不共享数据

 package mythread;

 public class MyThread extends Thread {

     private int count = 5;

     public MyThread(String name) {
super();
this.setName(name);
} @Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由" + this.currentThread().getName() + " 计算, count=" + count);
}
}
}
 package test;

 import mythread.MyThread;

 public class Run {
public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}
由B 计算, count=4
由C 计算, count=4
由A 计算, count=4
由A 计算, count=3
由C 计算, count=3
由B 计算, count=3
由C 计算, count=2
由A 计算, count=2
由C 计算, count=1
由C 计算, count=0
由B 计算, count=2
由B 计算, count=1
由B 计算, count=0
由A 计算, count=1
由A 计算, count=0

  一共创建了3个线程,每个线程都有各自的count变量,自己减少自己的count变量的值。这样的情况就是变量不共享。

  3.2 共享数据

  共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多个线程可以同时处理同一个人的票数。

  3.2.1“非线程安全”的问题

 package mythread;

 public class MyThread extends Thread {

     private int count=5;

     @Override
public void run() {
super.run();
count--;
System.out.println("由"+this.currentThread().getName()+"计算,count="+count);
}
}
 package test;

 import mythread.MyThread;

 public class Run {
public static void main(String[] args) {
MyThread mythread=new MyThread(); Thread a=new Thread(mythread,"A");
Thread b=new Thread(mythread,"B");
Thread c=new Thread(mythread,"C");
Thread d=new Thread(mythread,"D");
Thread e=new Thread(mythread,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
由B计算,count=3
由A计算,count=3
由C计算,count=2
由D计算,count=1
由E计算,count=0

  从输出结果可以看出,线程B和Acount的值都是3,说明A和B同时对count进行处理,产生了“非线程安全”问题。

  3.2.2解决“非线程安全”的问题

  将MyThread类修改一下即可。

 package mythread;

 public class MyThread extends Thread {

     private int count=5;

     @Override
synchronized public void run() {
super.run();
count--;
System.out.println("由"+this.currentThread().getName()+"计算,count="+count);
}
}
由A计算,count=4
由B计算,count=3
由E计算,count=2
由D计算,count=1
由C计算,count=0

  通过在run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。这样也就实现了排队调用run方法的目的,也就达到了按顺序对count变量减1的效果。

  synchronized可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronized里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能拿到为止,而且是多个线程同时去争抢这把锁。

  3.2.3另一个解决“非线程安全”的示例

 package controller;

 //生成一个servlet组件
public class LoginServlet { private static String usernameRef;
private static String passwordRef; public static void doPost(String username, String password) {
try {
usernameRef = username;
if (username.equals("a")) {
Thread.sleep(5000);
}
passwordRef = password; System.out.println("username=" + usernameRef + " password="+ passwordRef);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
 package extthread;

 import controller.LoginServlet;

 public class ALogin extends Thread {
@Override
public void run() {
LoginServlet.doPost("a", "aa");
}
}
 package extthread;

 import controller.LoginServlet;

 public class BLogin extends Thread {
@Override
public void run() {
LoginServlet.doPost("b", "bb");
}
}
 package test;

 import extthread.ALogin;
import extthread.BLogin; public class Run { public static void main(String[] args) {
ALogin a = new ALogin();
a.start();
BLogin b = new BLogin();
b.start();
} }
username=b password=bb
username=b password=aa

  由于没有使用synchronized关键字,从而产生了“非线程安全”的问题。即在另一个线程没有等待sleep(5000)这个过程就执行了。修改代码使用synchronized关键字就可以解决这个问题。

 package controller;

 //生成一个servlet组件
public class LoginServlet { private static String usernameRef;
private static String passwordRef; synchronized public static void doPost(String username, String password) {
try {
usernameRef = username;
if (username.equals("a")) {
Thread.sleep(5000);
}
passwordRef = password; System.out.println("username=" + usernameRef + " password="+ passwordRef);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
username=a password=aa
username=b password=bb

  4.留意i--与System.out.println()的异常

  println()方法与i++联合使用时“有可能”出现的另外一种异常情况。

 package extthread;

 public class MyThread extends Thread {

     private int i = 5;

     @Override
public void run() {
System.out.println("i=" + (i--) + " threadName="+ Thread.currentThread().getName());
} }
 package test;

 import extthread.MyThread;

 public class Run {

     public static void main(String[] args) {

         MyThread run = new MyThread();

         Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
Thread t3 = new Thread(run);
Thread t4 = new Thread(run);
Thread t5 = new Thread(run); t1.start();
t2.start();
t3.start();
t4.start();
t5.start(); } }
i=5 threadName=Thread-4
i=5 threadName=Thread-1
i=4 threadName=Thread-3
i=3 threadName=Thread-5
i=2 threadName=Thread-2

  需要说明的是,虽然println()方法在内部是同步的(有synchronized关键字),但是i--的操作却是在进入println()之间发生的,所以有发生“非线程安全”问题的概率。所以还是应该使用synchronized关键字。

 package extthread;

 public class MyThread extends Thread {

     private int i = 5;

     @Override
synchronized public void run() {
System.out.println("i=" + (i--) + " threadName="+ Thread.currentThread().getName());
} }
i=5 threadName=Thread-1
i=4 threadName=Thread-2
i=3 threadName=Thread-3
i=2 threadName=Thread-5
i=1 threadName=Thread-4

  三、currentThread()方法

  currentThread()方法可返回代码段正在被哪个线程调用的信息。

  示例1:结果说明main方法被名为main的线程调用。

 package run;

 public class Run1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
}
main

  示例2:MyThread.java类的构造函数是被main线程调用的,而run方法时被名称为Thread-0的线程调用的,run方法是自动调用的方法。

 package mythread;

 public class MyThread extends Thread {

     public MyThread() {
System.out.println("构造方法的打印:" + Thread.currentThread().getName());
} @Override
public void run() {
System.out.println("run方法的打印:" + Thread.currentThread().getName());
} }
 package run;

 import mythread.MyThread;

 public class Run2 {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();
//mythread.run();
}
}
构造方法的打印:main
run方法的打印:Thread-0

  示例3:修改示例2中的Run2.java。从结果中我们可以看出MyThread.java类的构造函数和run方法都是被main线程调用的,这里也就是说明了同步和异步的区别。

package run;

import mythread.MyThread;

public class Run2 {
public static void main(String[] args) {
MyThread mythread = new MyThread();
//mythread.start();
mythread.run();
}
}
构造方法的打印:main
run方法的打印:main

  示例4:输出显示了this.getname()方法和currentThread().getname()的区别

 package mythread;

 public class CountOperate extends Thread {

     public CountOperate() {
System.out.println("CountOperate---begin");
System.out.println("Thread.currentThread().getName()="+ Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println(Thread.currentThread() == this);
System.out.println("CountOperate---end");
} @Override
public void run() {
System.out.println("run---begin");
System.out.println("Thread.currentThread().getName()="+ Thread.currentThread().getName());
System.out.println("this.getName()=" + this.getName());
System.out.println(Thread.currentThread() == this);
System.out.println("run---end");
} }
package test;

import mythread.CountOperate;

public class Run {

    public static void main(String[] args) {
CountOperate c = new CountOperate();
Thread t1 = new Thread(c);
t1.setName("test");
t1.start();
} }
CountOperate---begin
Thread.currentThread().getName()=main
this.getName()=Thread-0
false
CountOperate---end
run---begin
Thread.currentThread().getName()=test
this.getName()=Thread-0
false
run---end

  首先分析构造函数,根据输出可以看出,调用构造函数的是main线程,而此时还没有启动CountOperate子线程,所以打印出this.getName()=Thread-0,并且此时this代表的是CountOperate对象的实例,因此会输出false。

  然后分析run方法,从输出上来看,很让人疑惑的是run中的this.getName()=Thread-0这条输出。

  通过查看Thread类的源码,可以发现

  public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

  在Thread类的构造方法中,默认地会通过this.name=name给name赋值。也就是说,在Thread源码中实际上new Thread(c)会将c的应用对象绑定到一个private变量target上,在t1被执行的时候,即t1.run被调用的时候,它会调用target.run方法,也就是说它是直接调用c对象的run方法,也就是说,在run方法被执行的时候,this.getName()实际上返回的是target.getName()方法,而Thread.currentThread().getName()实际上是t1.getName()。

  示例5:修改示例4中的main方法

package test;

import mythread.CountOperate;

public class Run {

    public static void main(String[] args) {
CountOperate c = new CountOperate();
c.setName("test");
c.start();
} }
CountOperate---begin
Thread.currentThread().getName()=main
this.getName()=Thread-0
false
CountOperate---end
run---begin
Thread.currentThread().getName()=test
this.getName()=test
true
run---end

  从输出结果中也可以看出,此时CountOperate构造函数和之前的输出保持一致,因为执行构造方法时线程还没有执行,所以this.getName()=Thread-0。而run方法中CountOperate线程已经执行并且可以获取到线程的名称,所以输出this.getName()=test。这也就是自动调用和被动调用的区别。

  四、isAlive()方法

  isAlive()方法用于判断当前的线程是否处于活动状态。

  示例1:end ==true说明mythread线程还没有执行完毕

package mythread;

public class MyThread extends Thread {
@Override
public void run() {
System.out.println("run=" + this.isAlive());
}
}
package run;

import mythread.MyThread;

public class Run {
public static void main(String[] args) {
MyThread mythread = new MyThread();
System.out.println("begin ==" + mythread.isAlive());
mythread.start();
System.out.println("end ==" + mythread.isAlive());
}
}
begin ==false
end ==true
run=true

  示例2:修改示例1程序。end ==false说明mythread对象已经在1秒之内执行完毕。

package run;

import mythread.MyThread;

public class Run {
public static void main(String[] args) throws InterruptedException{
MyThread mythread = new MyThread();
System.out.println("begin ==" + mythread.isAlive());
mythread.start();
Thread.sleep(1000);
System.out.println("end ==" + mythread.isAlive());
}
}
begin ==false
run=true
end ==false

  示例3:将线程对象以构造参数的方式传递给Thread对象进行start()启动时,运行的结果会有差异。造成这种结果的原因和之前遇到的情况一样,也就是Thread.currentThread()和this的区别。

package mythread;

public class CountOperate extends Thread {

    public CountOperate() {
System.out.println("CountOperate---begin"); System.out.println("Thread.currentThread().getName()="+ Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive()="+ Thread.currentThread().isAlive());
System.out.println("this.getName()=" + this.getName());
System.out.println("this.isAlive()=" + this.isAlive());
System.out.println("CountOperate---end");
} @Override
public void run() {
System.out.println("run---begin");
System.out.println("Thread.currentThread().getName()="+ Thread.currentThread().getName());
System.out.println("Thread.currentThread().isAlive()="+ Thread.currentThread().isAlive());
System.out.println("this.getName()=" + this.getName());
System.out.println("this.isAlive()=" + this.isAlive());
System.out.println("run---end");
} }
package test;

import mythread.CountOperate;

public class Run {

    public static void main(String[] args) {
CountOperate c = new CountOperate();
Thread t1 = new Thread(c);
System.out.println("main begin t1 isAlive=" + t1.isAlive());
t1.setName("test");
t1.start();
System.out.println("main end t1 isAlive=" + t1.isAlive());
} }
CountOperate---begin
Thread.currentThread().getName()=main
Thread.currentThread().isAlive()=true
this.getName()=Thread-0
this.isAlive()=false
CountOperate---end
main begin t1 isAlive=false
main end t1 isAlive=true
run---begin
Thread.currentThread().getName()=test
Thread.currentThread().isAlive()=true
this.getName()=Thread-0
this.isAlive()=false
run---end

  五、sleep()方法

  sleep()方法的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是只this.currentThread()返回的线程。

  示例1:直接使用mythread.run()方法同步启动线程,这就使得通过main线程来执行MyThread1的线程,通过输出可以发现线程休眠了2秒。

package mythread;

public class MyThread1 extends Thread {
@Override
public void run() {
try {
System.out.println("run threadName="+ this.currentThread().getName() + " begin");
Thread.sleep(2000);
System.out.println("run threadName="+ this.currentThread().getName() + " end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package run;

import mythread.MyThread1;

public class Run1 {
public static void main(String[] args) {
MyThread1 mythread = new MyThread1();
System.out.println("begin =" + System.currentTimeMillis());
mythread.run();
System.out.println("end =" + System.currentTimeMillis());
}
}
begin =1525254700337
run threadName=main begin
run threadName=main end
end =1525254702337

  示例2:在main方法中使用mythread.start()方法后,main线程和MyThread2线程异步执行,所以首先打印信息为begin和end。而MyThread2线程是随后运行的,在最后两行打印run begin和run end。

package mythread;

public class MyThread2 extends Thread {
@Override
public void run() {
try {
System.out.println("run threadName="+ this.currentThread().getName() + " begin ="+ System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("run threadName="+ this.currentThread().getName() + " end ="+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package run;

import mythread.MyThread2;

public class Run2 {
public static void main(String[] args) {
MyThread2 mythread = new MyThread2();
System.out.println("begin =" + System.currentTimeMillis());
mythread.start();
System.out.println("end =" + System.currentTimeMillis());
}
}
begin =1525254898564
end =1525254898564
run threadName=Thread-0 begin =1525254898564
run threadName=Thread-0 end =1525254900564

  六、getId()方法

  getId()方法的所用是取得线程的唯一标识。从打印结果看,当前执行代码的线程名称为main,线程id为1。

public class Run1 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getId());
}
}
main
1

  七、停止线程

  在Java中有一下3中方法可以终止正在运行得线程:

  ①使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

  ②使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend即resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。

  ③使用interrupt方法中断线程。

  1.停止不了的线程

  示例:通过调用interrupt()方法来停止线程,但interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程打了一个停止的标记,并不是真的停止线程。从运行的结果也可以看出,调用interrupt方法并没有停止线程。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
System.out.println("i=" + (i + 1));
}
}
}
package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run {

    public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
} }
...
i=499991
i=499992
i=499993
i=499994
i=499995
i=499996
i=499997
i=499998
i=499999
i=500000

  2.判断线程是否是停止状态

  判断线程的状态是不是停止的,Thread.java类里提供了两种方法:①this.interrupted()测试当前线程是否已经中断,当前线程是指运行this.interrupted()方法的线程②this.isInterrupted()测试线程是否已经中断

  示例1:this.interrupted()使用。从打印情况来看,线程并未停止,这也就证明了interrupted()方法是测试当前线程是否已经中断,这个“当前线程”是main,它从未中断过,所以打印的结果是两个false。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
System.out.println("i=" + (i + 1));
}
}
}
package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
//Thread.currentThread().interrupt();
System.out.println("是否停止1?="+thread.interrupted());
System.out.println("是否停止2?="+thread.interrupted());
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
}
}
i=125615
i=125616
i=125617
是否停止1?=false
i=125618
i=125619
i=125620
i=125621
i=125622
是否停止2?=false
end!
i=125623
i=125624
i=125625

  示例2:修改示例1中的run方法。Thread.currentThread().interrupt();确实使main线程产生了中断效果。但是令人困惑的是,是否停止2?=false这是因为,如果连续两次调用interrupt()方法,则第一次调用已经清楚了其中断状态后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外,其他情况下都会返回false。

package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run2 {
public static void main(String[] args) {
Thread.currentThread().interrupt();
System.out.println("是否停止1?=" + Thread.interrupted());
System.out.println("是否停止2?=" + Thread.interrupted());
System.out.println("end!");
}
}
是否停止1?=true
是否停止2?=false
end!

  示例3:isInterrupted()方法并未清除状态标志,所以打印了两个true,和前面interrupted()方法的区别就是是否清除标志。

package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run3 {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.out.println("是否停止1?="+thread.isInterrupted());
System.out.println("是否停止2?="+thread.isInterrupted());
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
}
}
i=143424
i=143425
是否停止1?=true
i=143426
i=143427
i=143428
i=143429
i=143430
i=143431
i=143432
i=143433
i=143434
i=143435
i=143436
i=143437
i=143438
i=143439
i=143440
i=143441
i=143442
i=143443
i=143444
i=143445
i=143446
i=143447
i=143448
i=143449
i=143450
i=143451
i=143452
是否停止2?=true
i=143453

  3.能停止的线程--异常法

  可以在for语句来判断一下线程是否是停止状态,如果是停止状态,则后面的代码不再运行即可。

  示例1:虽然停止了线程,但是如果for语句下面还有语句,还是会继续运行的。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!");
break;
}
System.out.println("i=" + (i + 1));
}
}
}
package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run {

    public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
} }
i=252895
i=252896
i=252897
i=252898
end!
已经是停止状态了!

  示例2:验证如果for语句下面还有语句时,还是会继续运行的。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!");
break;
}
System.out.println("i=" + (i + 1));
}
System.out.println("如果此代码是for又继续运行,线程并未停止!");
}
}
package test;

import exthread.MyThread;
import exthread.MyThread; public class Run { public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
} }
i=251852
i=251853
i=251854
已经是停止状态了!
如果此代码是for又继续运行,线程并未停止!
end!

  示例3:解决语句继续运行的问题,通过异常语句来停止线程。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!");
throw new InterruptedException();
}
System.out.println("i=" + (i + 1));
}
System.out.println("我在for语句下面,你猜我会不会输出");
} catch (InterruptedException e) {
System.out.println("进run方法中的catch了");
e.printStackTrace();
}
}
}
i=238006
i=238007
end!
已经是停止状态了!
进run方法中的catch了
java.lang.InterruptedException
at exthread.MyThread.run(MyThread.java:11)

  4.在沉睡中停止

  示例1:在sleep()状态下停止线程(先sleep然后再interrupt()),会进入catch语句,并且清除停止状态值,使之变成false。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
System.out.println("run begin");
Thread.sleep(200000);
System.out.println("run end");
} catch (InterruptedException e) {
System.out.println("在沉睡中被停止!进入了catch!"+this.isInterrupted());
e.printStackTrace();
}
}
}
package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run {

    public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(200);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
System.out.println("end!");
} }
run begin
end!
在沉睡中被停止!进入了catch!false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at exthread.MyThread.run(MyThread.java:9)

  示例2:先执行interrupt停止线程,因此输出end!然后for循环执行完成,然后进入sleep,产生异常,进入catch。

package exthread;

public class MyThread extends Thread {
@Override
public void run() {
super.run();
try {
for(int i=0;i<100000;i++){
System.out.println("i="+(i+1));
}
System.out.println("run begin");
Thread.sleep(200000);
System.out.println("run end");
} catch (InterruptedException e) {
System.out.println("先执行interrupt停止线程,再遇到sleep!进入catch!");
e.printStackTrace();
}
}
}
package test;

import exthread.MyThread;

import exthread.MyThread;

public class Run {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
System.out.println("end!");
}
}
end!
i=1
i=2
i=3
i=4
i=5
i=6
i=7
...
i=99998
i=99999
i=100000
run begin
先执行interrupt停止线程,再遇到sleep!进入catch!
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at exthread.MyThread.run(MyThread.java:12)

  5.能停止的线程--暴力停止

  示例:使用stop()方法停止线程则是非常暴力的。从输出可以看出,main线程停止了8秒然后被stop掉了,然后子线程每隔一秒执行输出一个数字。

package testpackage;

public class MyThread extends Thread {
private int i = 0; @Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
package test.run;

import testpackage.MyThread;

public class Run {

    public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(8000);
thread.stop();
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8

  6.方法stop()与java.lang.ThreadDeath异常

  示例:调用stop()方法会抛出java.lang.ThreadDeath异常,但在通常情况下,次异常不需要显示地捕捉。不过需要注意的是,stop()方法已经被作废,因为如果强制让线程停止则有可能使一些请理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的问题。

package testpackage;

public class MyThread extends Thread {
@Override
public void run() {
try {
this.stop();
} catch (ThreadDeath e) {
System.out.println("进入run方法中的catch了");
e.printStackTrace();
}
}
}
package test.run;

import testpackage.MyThread;

public class Run {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
进入run方法中的catch了
java.lang.ThreadDeath
at java.lang.Thread.stop(Unknown Source)
at testpackage.MyThread.run(MyThread.java:7)

  7.释放锁的不良后果

  示例:使用stop()释放锁将会给数据造成不一致的结果。如果出现这样的情况,程序处理的数据就有可能遭到破坏,最终导致程序执行的流程错误。这里的意思就是还没来得及给password赋值就把线程stop掉了。由于stop()方法已经作废了,所以不建议使用。

package testpackage;

public class SynchronizedObject {

    private String username = "a";
private String password = "aa"; public String getUsername() {
return username;
} public void setUsername(String username) {
this.username = username;
} public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password;
} synchronized public void printString(String username, String password) {
try {
this.username = username;
Thread.sleep(100000);
this.password = password;
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
package testpackage;

public class MyThread extends Thread {

    private SynchronizedObject object;

    public MyThread(SynchronizedObject object) {
super();
this.object = object;
} @Override
public void run() {
object.printString("b", "bb");
}
}
package test.run;

import testpackage.MyThread;
import testpackage.SynchronizedObject; public class Run {
public static void main(String[] args) {
try {
SynchronizedObject object = new SynchronizedObject();
MyThread thread = new MyThread(object);
thread.start();
Thread.sleep(500);
thread.stop();
System.out.println(object.getUsername() + " "+ object.getPassword());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
b aa

  8.使用return停止线程

  示例:将方法interrupt()与return结合使用也能实现停止线程的效果。线程运行了2秒才停止。不过还是建议用“抛异常”的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止的事件得以传播。

package extthread;

public class MyThread extends Thread {

    @Override
public void run() {
while (true) {
if (this.isInterrupted()) {
System.out.println("ֹͣ停止了!");
return;
}
System.out.println("timer=" + System.currentTimeMillis());
}
} }
package test.run;

import extthread.MyThread;

public class Run {

    public static void main(String[] args) throws InterruptedException {
MyThread t=new MyThread();
t.start();
Thread.sleep(2000);
t.interrupt();
} }
timer=1525267712005
timer=1525267712006
timer=1525267712006
timer=1525267712006
timer=1525267712006
...
timer=1525267714005
timer=1525267714005
timer=1525267714005
timer=1525267714005
timer=1525267714005
ֹͣ停止了!

  八、暂停线程

  暂停线程意味着次线程还可以恢复运行。在Java中,使用suspend()方法暂停线程,使用resume()方法恢复线程的执行。

  1.suspend与resume方法的使用

  示例:通过输出我们可以看出,四行输出的时间间隔都是5秒,并且暂停和恢复控制线程中的run方法中的getI()方法的暂停和执行。

package mythread;

public class MyThread extends Thread {

    private long i = 0;

    public long getI() {
return i;
} public void setI(long i) {
this.i = i;
} @Override
public void run() {
while (true) {
i++;
}
} }
package test.run;

import mythread.MyThread;

public class Run {

    public static void main(String[] args) {

        try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(5000);
// A
thread.suspend();
System.out.println("A= " + System.currentTimeMillis() + " i="+ thread.getI());
Thread.sleep(5000);
System.out.println("A= " + System.currentTimeMillis() + " i="+ thread.getI());
// B
thread.resume();
Thread.sleep(5000);
// C
thread.suspend();
System.out.println("B= " + System.currentTimeMillis() + " i="+ thread.getI());
Thread.sleep(5000);
System.out.println("B= " + System.currentTimeMillis() + " i="+ thread.getI());
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
A= 1525268875079 i=2770802031
A= 1525268880079 i=2770802031
B= 1525268885079 i=5533965062
B= 1525268890079 i=5533965062

  2.suspend与resume方法的缺点--独占

  示例1:在使用suspend与resume方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。因为线程a被锁定,所以线程的方法也被锁定,其他线程就无法访问这个方法。即独占并锁死了printString方法。

package testpackage;

public class SynchronizedObject {

    synchronized public void printString() {
System.out.println("begin");
if (Thread.currentThread().getName().equals("a")) {
System.out.println("a线程永远suspend了");
Thread.currentThread().suspend();
}
System.out.println("end");
} }
package test.run;

import testpackage.SynchronizedObject;

public class Run {

    public static void main(String[] args) {
try {
final SynchronizedObject object = new SynchronizedObject(); Thread thread1 = new Thread() {
@Override
public void run() {
object.printString();
}
}; thread1.setName("a");
thread1.start(); Thread.sleep(1000); Thread thread2 = new Thread() {
@Override
public void run() {
System.out.println("thread2启动了,但进入不了printString方法");
System.out.println("因为printString方法被a线程锁定并且永远被suspend了");
object.printString();
System.out.println("你猜我会不会执行");
}
};
thread2.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
begin
a线程永远suspend了
thread2启动了,但进入不了printString方法
因为printString方法被a线程锁定并且永远被suspend了

  示例2:注意这里的main end!可以正常打印出来。将和之后的示例3作对比。

package mythread;

public class MyThread extends Thread {
private long i = 0; @Override
public void run() {
while (true) {
i++;
//System.out.println(i);
}
}
}
package test.run;

import mythread.MyThread;

public class Run {

    public static void main(String[] args) {

        try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(1000);
thread.suspend();
System.out.println("main end!");
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
main end!

  示例3:修改示例2中的MyThread。控制台将不再打印main end!,这是因为当程序运行到println()方法内部停止时,同步锁未被释放。下面看一下println()方法的源码

    public void println(long x) {
synchronized (this) {
print(x);
newLine();
}
}

  这导致当前PringStream的println()方法一直呈“暂停”状态,并且“锁未释放”,而main()方法当中的代码 System.out.println("main end!"); 迟迟不能执行打印!也就是线程中使用了println()方法然后被暂停,由于println()方法需要同步,即两个println()方法不能分别执行,所以就造成了第一个println()方法再暂停的情况下第二个不能执行的情况。

package mythread;

public class MyThread extends Thread {
private long i = 0; @Override
public void run() {
while (true) {
i++;
System.out.println(i);
}
}
}
...121036
121037
121038
121039
121040
121041

  3.suspend与resume方法的缺点--不同步

  示例:和前面的stop()方法使得数据不同步的情况一样,使用suspend与resume方法也会造成数据不同步的情况。线程a被暂停了导致password字段的值不能被更新,所以线程2在执行的时候保留默认值。

package myobject;

public class MyObject {

    private String username = "1";
private String password = "11"; public void setValue(String u, String p) {
this.username = u;
if (Thread.currentThread().getName().equals("a")) {
System.out.println("ֹͣ停止a线程");
Thread.currentThread().suspend();
}
this.password = p;
} public void printUsernamePassword() {
System.out.println(username + " " + password);
}
}
package test;

import myobject.MyObject;

public class Run {

    public static void main(String[] args) throws InterruptedException {

        final MyObject myobject = new MyObject();

        Thread thread1 = new Thread() {
public void run() {
myobject.setValue("a", "aa");
};
};
thread1.setName("a");
thread1.start(); Thread.sleep(500); Thread thread2 = new Thread() {
public void run() {
myobject.printUsernamePassword();
};
};
thread2.start(); } }
ֹͣ停止a线程
a 11

  九、yield()方法

  yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

  示例1:不添加yield()方法的情况。即线程独占CPU时间片来执行线程。用时21毫秒!

package test;

import extthread.MyThread;

import extthread.MyThread;

public class Run {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
} }
package extthread;

public class MyThread extends Thread {

    @Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
//Thread.yield();
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时" + (endTime - beginTime) + "毫秒!");
} }
用时21毫秒!

  示例2:添加yield()方法的情况。将CPU时间片让给其他资源导致速度变慢很多。用时13017毫秒!

package extthread;

public class MyThread extends Thread {

    @Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
//Thread.yield();
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时" + (endTime - beginTime) + "毫秒!");
} }
用时13017毫秒!

  十、线程的优先级

  在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级高的线程对象中的任务。设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。

  Java中的线程优先级分为1~10这10个等级,使用MIN_PRIORITY/NORM_PRIORITY/MAX_PRIORITY三个常量来预置优先级的值。设置线程的优先级使用setPriority()方法,源码如下

    public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}

  1.线程优先级的继承特性

  在Java中,线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。

  示例1:优先级被继承,线程1启动线程2,两个线程的优先级是一样的,同时和mian线程也是一样的优先级。

package extthread;

public class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("MyThread1 run priority=" + this.getPriority());
MyThread2 thread2 = new MyThread2();
thread2.start();
}
}
package extthread;

public class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("MyThread2 run priority=" + this.getPriority());
}
}
package test;

import extthread.MyThread1;

public class Run {
public static void main(String[] args) {
System.out.println("main thread begin priority="+ Thread.currentThread().getPriority());
//Thread.currentThread().setPriority(6);
System.out.println("main thread end priority="+ Thread.currentThread().getPriority());
MyThread1 thread1 = new MyThread1();
thread1.start();
}
}
main thread begin priority=5
main thread end priority=5
MyThread1 run priority=5
MyThread2 run priority=5

  示例2:修改main方法。修改后main线程的优先级比原始的5高,通过main线程启动1线程,然后通过1线程启动2线程,优先级都是一样的。

package test;

import extthread.MyThread1;

public class Run {
public static void main(String[] args) {
System.out.println("main thread begin priority="+ Thread.currentThread().getPriority());
Thread.currentThread().setPriority(6);
System.out.println("main thread end priority="+ Thread.currentThread().getPriority());
MyThread1 thread1 = new MyThread1();
thread1.start();
}
}
main thread begin priority=5
main thread end priority=6
MyThread1 run priority=6
MyThread2 run priority=6

  2.优先级具有规则性

  示例:高优先级的线程总是大部分先执行完,但并不代表高优先级的线程全部先执行完。另外,并不是MyThread先被main线程调用就会先执行完,当优先级等级差距很大时,谁先执行完和代码的调用顺序无关。即线程的优先级与代码执行顺序无关。

package extthread;

import java.util.Random;

public class MyThread1 extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
long addResult = 0;
for (int j = 0; j < 10; j++) {
for (int i = 0; i < 50000; i++) {
Random random = new Random();
random.nextInt();
addResult = addResult + i;
}
}
long endTime = System.currentTimeMillis();
System.out.println("******线程1 use time=" + (endTime - beginTime));
}
}
package extthread;

import java.util.Random;

public class MyThread2 extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
long addResult = 0;
for (int j = 0; j < 10; j++) {
for (int i = 0; i < 50000; i++) {
Random random = new Random();
random.nextInt();
addResult = addResult + i;
}
}
long endTime = System.currentTimeMillis();
System.out.println("%%%%%%线程2 use time=" + (endTime - beginTime));
}
}
package test;

import extthread.MyThread1;
import extthread.MyThread2; public class Run {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
MyThread1 thread1 = new MyThread1();
thread1.setPriority(1);
thread1.start(); MyThread2 thread2 = new MyThread2();
thread2.setPriority(10);
thread2.start();
}
}
}
%%%%%%线程2 use time=239
%%%%%%线程2 use time=256
%%%%%%线程2 use time=311
%%%%%%线程2 use time=312
%%%%%%线程2 use time=319
******线程1 use time=173
******线程1 use time=211
******线程1 use time=212
******线程1 use time=180
******线程1 use time=477

  3.优先级具有随机性

  优先级较高的线程不一定每一次都先执行完。

  示例:修改2中示例中的main方法,修改优先级为5和6,从结果可以看出,不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程不一定每一次都先执行完run()方法中的任务,也就是说,线程优先级与打印顺序无关,不要讲这两者的关系相关联,它们的关系具有不确定性和随机性。

package extthread;

import java.util.Random;

public class MyThread1 extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
Random random = new Random();
random.nextInt();
}
long endTime = System.currentTimeMillis();
System.out.println("**********1 use time=" + (endTime - beginTime));
}
}
%%%%%%%%%%2 use time=2
**********1 use time=6
**********1 use time=5
%%%%%%%%%%2 use time=0
**********1 use time=0
**********1 use time=1
%%%%%%%%%%2 use time=1
%%%%%%%%%%2 use time=0
**********1 use time=0
%%%%%%%%%%2 use time=1

  4.看谁运行得快

  示例:从输出结果可以看出,优先级高的线程运行得快,b的优先级为8,a的优先级为2,b运行得快得多,这里要注意的是,int类型的数一直加超过范围后会变成负数!!!

package extthread;

public class ThreadA extends Thread {

    private int count = 0;

    public int getCount() {
return count;
} @Override
public void run() {
while (true) {
count++;
}
} }
package extthread;

public class ThreadB extends Thread {

    private int count = 0;

    public int getCount() {
return count;
} @Override
public void run() {
while (true) {
count++;
}
} }
package test;

import extthread.ThreadA;
import extthread.ThreadB; public class Run { public static void main(String[] args) { try {
ThreadA a = new ThreadA();
a.setPriority(Thread.NORM_PRIORITY - 3);
a.start(); ThreadB b = new ThreadB();
b.setPriority(Thread.NORM_PRIORITY + 3);
b.start(); Thread.sleep(20000);
a.stop();
b.stop(); System.out.println("a=" + a.getCount());
System.out.println("b=" + b.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
} }
a=1197537357
b=-1459844607

  十一、守护线程

  在Java线程中有两种线程,一种是用户线程,另一种是守护线程。守护线程就是当进程中不存在非守护线程了,则守护线程自动销毁。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),它就是一个很称职的守护者。

  示例:main线程休眠5秒后,最后一个非守护线程结束,守护线程也一同结束。

package testpackage;

public class MyThread extends Thread {
private int i = 0; @Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + (i));
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package test.run;

import testpackage.MyThread;

public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我离开thread对象也不再打印了,也就是停止了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
i=1
i=2
i=3
i=4
i=5
我离开thread对象也不再打印了,也就是停止了!

最新文章

  1. .NET平台常用的框架整理
  2. hibernate的@EmbeddedId嵌入式主键详解
  3. Bootstrap 模态框在用户点击背景空白处时会自动关闭
  4. pay-as-you-go
  5. SqlServer2008R2用Windows身份登录18456错误解决
  6. Android——BaseAdapter相关
  7. 我的WebX框架学习总结与心得分享
  8. Hive基础介绍
  9. 4G时代来临,运营商为谁搭台献唱?
  10. 利用反射的特性将DataReader对象转化为List集合
  11. LP64是什么意思
  12. TPL异步并行编程之取消任务
  13. Win内存分配函数(GlobalAlloc/HeapAlloc/LocalAlloc/VirtualAlloc)
  14. 老李分享:Mac快捷键
  15. iOS开发工程师必备技能(持续更新)
  16. 【转载】ZooKeeper学习第二期--ZooKeeper安装配置
  17. HiveQL详解
  18. [剑指Offer]42-连续子数组的最大和(DP)
  19. Mybatis 逆向工程学习随笔
  20. php发送 与接收流文件

热门文章

  1. Kerberoasting攻击
  2. selenium-03-02操作元素-等待
  3. Eclipse的egit插件冲突合并方法
  4. abp(net core)+easyui+efcore实现仓储管理系统——EasyUI之货物管理二 (二十)
  5. 死磕 java同步系列之终结篇
  6. Ocelot自定义管道中间件
  7. 运算符 字符串 for循环
  8. Mysql高手系列 - 第20篇:异常捕获及处理详解(实战经验)
  9. ActiveMQ学习总结------入门篇01
  10. Rust入坑指南:核心概念