导语
想要学习一个新的技术就必须要无限的接近它,深入的了解它,了解一个东西的步骤就是由浅入深的去深入的了解它。下面这个专题博主会带着大家共同学习Java多线程的核心编程技术,从入门到深入,也欢迎大家能够加入面试群聊,来分享自己的面试学习心得。
使用多线程
在一个进程运行的过程中至少存在一个线程,这些线程在后台默默的执行,支撑着整个进程的运行。例如在Java 中调用public static void main方法就可以创建一个主线程,这线程是由JVM创建。
public class Test{
public static void main(String[] args){
System.out.println(Thread.currentThread().getName());
}
}
会看到在控制台输出了一个main,其实这个main就是我们获取到的当前执行线程的线程名。而这个线程名与main方法并没有直接关系。
通过继承Thread类创建线程
Java提供了很多的对于多线程技术的支持包,通过这些包提供的方法可以快速的创建线程。但是在实际开发中我们常用的两种方式:一种是继承Thread类,一种是实现Runnable接口。当然还有其他两种方式一种是继承Callable接口,一种是使用线程池进行创建。在一般的开发中都很少用到后两种方式,所以这里,先来简单的介绍一下前两种方式,后续的分享中也会讲到后两种实现方式,有兴趣的的读者可以尝试学习一下后两种方式。
public class Thread implements Runnable
首先呢,可以看到Thread类也是继承了Runnable接口,而他们之间就是为了维持一个多态的关系,这也是Java语言的三大特性之一。通过继承Thread类的方式来创建新的线程的时候,在很多程度上是限制了多继承的。我们都知道,Java语言是单继承的,也就是说一个类只能由一个父类。但是可以继承多个接口。所以从实质上来讲,无论是Thread的方式还是Runnable的方式,本质上是没有太大的区别的。
下面看一个小例子,我们来创建一个MyTestThread 的类,继承Thread类实现其中的run()方法。
public class MyTestThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyTestThread");
}
}
测试运行类
public class RunMain {
public static void main(String[] args) {
MyTestThread myTestThread = new MyTestThread();
myTestThread.start();
System.out.println("运行结束");
}
}
从上面的运行结果可以看到,线程通过start()启动之后,会自动的调用了run()方法中的内容。也就是说run()方法中的内容就是我们在开发中实际需要操作的业务逻辑对象。从控制台输出的结果来看,在MyTestThread类中run()方法中的内容实际要比主线程中的内容要输出的晚。这也就说明了一个问题,其实在调用了start()方法之后,执行的过程是一个非常耗时的操作。从线程启动到线程执行到run()方法之后,其实远比主线程直接执行完要耗时。所以就出现了“运行结束”,在MyThreadThread打印之前的结果。
虽然机会渺茫但是笔者通过几次的运行还是得到了如下的结果,从下面结果可以看到,在线程执行的过程中,并没有先后顺序,而是完全随机的。说明线程的执行时随机的。虽然输入这种结果的机会很小,但是多执行几次之后还是会出现。
如果非要要这样一个结果的话可以在主线程中加入耗时操作。让主线程睡眠 2秒钟。
public class RunMain {
public static void main(String[] args) throws InterruptedException {
MyTestThread myTestThread = new MyTestThread();
myTestThread.start();
TimeUnit.SECONDS.sleep(2);
System.out.println("运行结束");
}
}
这里需要注意的一点就是再使用多线程的时候,代码的运行结果与代码的执行顺序或者是调用顺序是无关的。线程是作为一个子任务来执行的,它对于CPU的使用是与主线程同一等级的。因为CPU调用每个线程是随机的,这也就解释了上面的结果。
来看一下下面这种情况,如果我们重复调用了start()方法就会出现如下的异常。
常见的线程分析命令
在平时的开发中,我们可以对运行中的线程进行状态分析,常见的三种线程状态分析的方法如下
- jps+jstack.exe
- jmc.exe
- jvisualvm.exe
创建了五个匿名线程对象,默认应该会是Thread-1~5的线程名。下面就通过上述的三种方式来分析一下线程
public class RunMain2 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(){
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
jps + jstack的方式
运行jps命令之后,会看到RunMain2线程ID为6312 ,然后通过jstack -l 6312 查看对应信息
C:\Users\Administrator>jps
13408
5248 Jps
10116 Launcher
6596 RuoYiApplication
11720 Launcher
12776 KotlinCompileDaemon
6312 RunMain2
11692 VideoCallApplication
18812
C:\Users\Administrator>jstask -l 6312
'jstask' 不是内部或外部命令,也不是可运行的程序
或批处理文件。
C:\Users\Administrator>jstack -l 6312
2022-03-25 17:26:26
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.291-b10 mixed mode):
"DestroyJavaVM" #19 prio=5 os_prio=0 tid=0x000002024835e800 nid=0x3784 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Thread-4" #18 prio=5 os_prio=0 tid=0x000002024835d000 nid=0x3f58 waiting on condition [0x000000ac7ffff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.netty.charp01.demo.RunMain2$1.run(RunMain2.java:19)
Locked ownable synchronizers:
- None
"Thread-3" #17 prio=5 os_prio=0 tid=0x0000020248359800 nid=0x25f8 waiting on condition [0x000000ac7fefe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.netty.charp01.demo.RunMain2$1.run(RunMain2.java:19)
Locked ownable synchronizers:
- None
"Thread-2" #16 prio=5 os_prio=0 tid=0x000002024835c000 nid=0x2454 waiting on condition [0x000000ac7fdff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.netty.charp01.demo.RunMain2$1.run(RunMain2.java:19)
Locked ownable synchronizers:
- None
"Thread-1" #15 prio=5 os_prio=0 tid=0x0000020248357000 nid=0x403c waiting on condition [0x000000ac7fcfe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.netty.charp01.demo.RunMain2$1.run(RunMain2.java:19)
Locked ownable synchronizers:
- None
"Thread-0" #14 prio=5 os_prio=0 tid=0x0000020248356800 nid=0x1ebc waiting on condition [0x000000ac7fbff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.netty.charp01.demo.RunMain2$1.run(RunMain2.java:19)
Locked ownable synchronizers:
- None
"Service Thread" #13 daemon prio=9 os_prio=0 tid=0x000002024828f000 nid=0x305c runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C1 CompilerThread3" #12 daemon prio=9 os_prio=2 tid=0x00000202481df800 nid=0x3390 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread2" #11 daemon prio=9 os_prio=2 tid=0x00000202481de000 nid=0x44b4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread1" #10 daemon prio=9 os_prio=2 tid=0x00000202481da800 nid=0x3094 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"C2 CompilerThread0" #9 daemon prio=9 os_prio=2 tid=0x00000202481da000 nid=0x1e34 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"JDWP Command Reader" #8 daemon prio=10 os_prio=0 tid=0x000002024815c000 nid=0x2ff0 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"JDWP Event Helper Thread" #7 daemon prio=10 os_prio=0 tid=0x0000020248155000 nid=0x3870 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"JDWP Transport Listener: dt_socket" #6 daemon prio=10 os_prio=0 tid=0x0000020245d6c000 nid=0x3700 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000020245d43800 nid=0x25fc waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x0000020245d43000 nid=0x1588 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000020245d12800 nid=0x2e8c in Object.wait() [0x000000ac7effe000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b008ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x000000076b008ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
Locked ownable synchronizers:
- None
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000020245d0b000 nid=0x3cfc in Object.wait() [0x000000ac7eeff000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076b006c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076b006c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
Locked ownable synchronizers:
- None
"VM Thread" os_prio=2 tid=0x0000020245ce2800 nid=0x3aec runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000002022b094800 nid=0xa3c runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000002022b096000 nid=0x2bec runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000002022b097000 nid=0x414 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000002022b098800 nid=0x3f04 runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000002022b09a800 nid=0x3d68 runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x000002022b09b800 nid=0x1db4 runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x000002022b09e800 nid=0x3880 runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x000002022b09f000 nid=0x21a8 runnable
"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x000002022b0a1000 nid=0x2f4c runnable
"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x000002022b0a2000 nid=0x33dc runnable
"VM Periodic Task Thread" os_prio=2 tid=0x00000202482ec000 nid=0x2230 waiting on condition
JNI global references: 1520
C:\Users\Administrator>
jmc.exe
笔者使用的时候需要从 http://jdk.java.net/jmc/8/ 连接上进行下载才可以使用
jvisualvm.exe
测试线程的随机性
在前面我们提到线程的调用是随机的,在上面代码中,对这一点并没有太多的展现,在这里我们编写一个测试数据来测试线程的随机性。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println("run="+Thread.currentThread().getName());
}
}
}
启动测试线程
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.setName("myThread");
thread.start();
for (int i = 0; i < 10000; i++) {
System.out.println("Main="+Thread.currentThread().getName());
}
}
}
测试结果
可以看到,在Thread类中的start方法启动线程之后,线程进入就绪状态,准备去调用run方法中的逻辑代码。在这整个的过程就需要CPU安排时间片来执行对应的逻辑代码。而这个时间片的获取是与其他线程的执行以及CPU的性能等有关系的。所以这就导致线程的执行是随机的,导致最终输出到控制台的内容是随机的。
Start方法的执行顺序并不代表就是run方法的执行顺序
一般情况下,我们都知道程序是从上到下按照顺序执行的,但在多线程中并不是这样。先来看个小例子。
public class MyThread extends Thread {
private int i;
public MyThread(int i){
super();
this.i = i;
}
@Override
public void run() {
System.out.println(i);
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread1 = new MyThread(1);
MyThread myThread2 = new MyThread(2);
MyThread myThread3 = new MyThread(3);
MyThread myThread4 = new MyThread(4);
MyThread myThread5 = new MyThread(5);
MyThread myThread6 = new MyThread(6);
MyThread myThread7 = new MyThread(7);
MyThread myThread8 = new MyThread(8);
MyThread myThread9 = new MyThread(9);
MyThread myThread10 = new MyThread(10);
MyThread myThread11 = new MyThread(11);
MyThread myThread12 = new MyThread(12);
myThread1.start();
myThread2.start();
myThread3.start();
myThread4.start();
myThread5.start();
myThread6.start();
myThread7.start();
myThread8.start();
myThread9.start();
myThread10.start();
myThread11.start();
myThread12.start();
}
}
从运行结果来看,我们调用start方法是按照顺序进行调用的,但实际执行run方法的时候却是另一种情况。这也从侧面反映出了,start方法的顺序并不会影响run方法的顺序