今天来聊一聊经典的Java技术,并发编程。并发是程序的灵魂,一个优秀的Java程序一定会支持高并发,并且,并发编程也是面试环节中经常会问到的一个问题,那么今天我们以一道经典的Java面试题回顾一下Java的并发编程。废话不多说,直入正题…
请你简单讲一下什么是线程?在Java中创建线程有几种方式?Java中的线程有哪些特点?
答:线程是比进程更细致的独立单位,线程是CPU调度与分派的最小单位。在Java中创建线程的方式一共有两种:一种是继承Thread类完成对Thread类的扩展,进而实现线程的创建,另一种是实现Runnable接口。从代码层面来解读就是如下:
public class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " 运?了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然后我们可以在测试方法或者Main方法中进行测试: 请读者不要看不起这不起眼的代码,其实内部蕴含着很深的奥妙。比如这里调用的Start()方法,start()方法从英文翻译的角度字面意思来看,就是启动线程,也就是将调用该方法的线程的当前状态从创建-就绪状态转向运行时状态。
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
实现Runnable接口方式:
public class MyRunnable implements Runnable {
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " 运?了");
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然后我们可以在测试方法或者Main方法中进行测试:
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
在Java程序中,不论是并发还是单线程程序,都会存在一个名为主线程Thread对象。在JVM(Java虚拟机)执行Java程序时,Java虚拟机会创建一个Thread来执行Main方法。也就是说,Java中的Main方法在执行的时候,也是以线程的形式存在于Java虚拟机中。 以上描述,值得注意的点是:
- Java中任何程序不论并发与否,只要启动Jav程序,那么就必定存在主线程。
- Java中的多线程,线程与线程之间共享Java应用程序的所有资源。[1]
- Java中线程都具备优先级[2]。
- Java中主动创建线程时,可以创建以下两类线程:(1)守护线程(2)非守护线程。
- 线程具备六种状态:
(1)NEW:新建状态。 (2)RUNNABLE:可运行状态,我不建议称作就绪状态,原因下文会解释。 (3)BLOCKED:阻塞状态。 (4)WAITING:等待状态。 (5)TIMED_WAITING:定时等待状态。 (6)TERMINATED:终止状态。
[ 1 ] 这里的资源则包括内存,与存在于内存中的文件、对象、变量等资源,线程之间快速?简单地共享应用程序的信息。需要注意的是:必须使?同步避免线程与线程之间发生数据竞争。 [ 2 ] Java中线程的优先级数值存在一定的范围,优先级数值介于Thread.MIN_PRIORITY(1)与Thread.MAX_PRIORITY(10)之间,默认优先级是Thread.NORM_PRIORITY(5),具体的优先级起不起作用需要关注操作系统对线程优先级的支持,线程的优先级无法一定保证优先级,只能提高优先调度的概率。
以上主要讲解了Java中线程的基本概念,以及线程的创建方式方法,以及线程的五点值得关注的主要性质。对于线程的状态,我以JDK11最新版11.0.13版本作为源码样本,进行详致讲解。如下图所示:这里我们先关注两个点,Thread的内部静态方法getState()方法的用途与返回值。 我们不难发现,Thread内部的State是一个枚举类。线程的状态获取底层是通过jdk.internal.misc.VM.toThreadState()方法进行实现的,该方法需要传入一个Int类型的数据,这个方法的底层实现逻辑如下图: 一方面,这充分说明了,在Thread类中,线程状态是固定的范围,一共六种状态,上面已做列举。每一种状态使用不同的数值进行表示。将传入的数值与JVMTI_THREAD_STATE常量进行按位与,从而计算按位与的结果。值得一提的是,JVMTI_THREAD_STATE 字段也就是线程状态字段是根据虚拟机在hotspot中实现状态转换时设置的。它的值是根据 JVM-TI 规范中的 GetThreadState () 函数设置的。 到现在为止,线程的状态获得的底层原理已经有了一个初步的大概,但对于线程的六种状态之间的切换是怎么操作的我们还没有一个明确的答案,接下来,我们一起来看一下线程切换的流程。下图:线程流程图,选图来自CSDN博主【m0_37779570】。 从这张图中,我们可以清晰的看出线程对象在调用start()方法后进入了RUNNABLE状态,进入RUNNABLE状态并不意味着就能立马执行,而是等待CPU线程调度器进行选择调度,当线程被调度后,才会进入运行中状态,但是如果此时调用了yield()方法,就会让线程重新回到就绪状态。
在线程处于RUNNABLE期间,此时发起阻塞IO操作会导致线程进入BLOCKED状态,当阻塞IO操作结束,又会重新回到RUNNABLE状态,当操作申请锁时,会进入阻塞状态,在获得锁后,又会进入RUNNABLE状态。
当处于RUNNABLE状态的线程调用了wait() 方法后,线程会进入WAITING状态,当被其他对象调用了notify() 或者notifyAll() 方法进行线程唤醒后,又会重新进入RUNNABLE状态,等待CPU调度。
当处于RUNNABLE状态的线程调用了Sleep() 方法后,线程会进入TIMED_WAITING状态,执行结束后又会及进入RUNNABLE状态。
特别注意!在给定的单位时间内,线程只能处于一个状态。
这里所涉及的所有线程状态,均来自于JVM-java虚拟机规范中的线程状态,不能类比操作系统的线程状态。
特别注意这里的RUNNABLE,我为什么不建议将RUNNABLE状态称之为就绪状态?因为RUNNABLE细分的话还包括就绪状态与执行中状态,这个RUNNABLE要从字面意思去理解更好理解一些,RUNNABLE代表着线程处于可执行状态,但是可执行并不代表着线程就处于执行状态,线程是否真正处于执行状态是取决于CPU的时间片与其他资源分配的。简而言之,就是说,线程处于就绪状态后,需要等待CPU分配系统资源才能进入RUNNING运行中状态;在处于RUNNABLE状态后,如果未获得CPU分配的系统资源,那么就会处于READY就绪状态。
|