Java中Thread类的基本用法
文章目录
并发编程是现在应用开发的必备技术,多核心的CPU为我们提供了硬件支撑,而如果想利用上这些硬件设备,我们需要在软件层面上充分应用多线程技术。Java标准库中提供的Thread类对操作系统中的线程进行了封装和更进一步抽象,通过学习使用Thread类API来进行并发编程。
一、Thread类构造方法
Thread类中提供了许多重载的构造方法,目前我们来看几个常用的。
构造方法 | 描述 |
---|---|
Thread() | 空参构造方法,创建一个线程对象。 |
Thread(Runnable target) | 创建一个线程对象,target为线程要执行的任务 |
Thread(Runnable target, String name) | 创建一个以name命名的线程对象,target为线程要执行的任务 |
Thread(ThreadGroup group,String name) | 创建一个线程对象并将其归纳到group线程分组中 |
二、创建线程的五种方式
若想在程序中使用多线程,首先我们需要创建一个Thread类的对象。Thread是对操作系统中线程的封装,我们通过JVM中的thread对象可以表示一个操作系统内核中的线程,但也不完全是,当我们构造好一个thread对象时,必须要调用.start()方法才能真正的创建并启动一个线程。
方式一:自定义类继承Thread类并重写run方法
public static void main(String[] args){
//我们这里使用内部类一样可以
class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello thread!");
}
}
//构造线程类对象,调用start方法
MyThread myThread = new MyThread();
myThread.start();
}
方式二: 自定义类实现Runnable接口并重写run方法
public static void main(String[] args) {
class MyThread implements Runnable{
@Override
public void run() {
System.out.println("Hello thread!");
}
}
//实现了Runnable接口的类,本质上是描述了线程要执行的任务,还需要将描述任务类的实例传给真正的线程类对象Thread
Thread thread = new Thread(new MyThread());
thread.start();
}
方式三: 匿名内部类继承Thread类
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("Hello thread!");
}
};
thread.start();
}
方式四:匿名内部类实现Runnable接口
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello thread!");
}
});
thread.start();
}
方式五: 使用lambda表达式
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Hello thread!");
}
});
thread.start();
}
实际上创建一个线程类对象的方法就是两种:自定义类继承Thread类并重写run方法 和 自定义类实现Runnable接口并重写run方法,只是写法形式不同,本质还是一样。
三、run方法和start方法
在上面五种方法中,我们都重写了run并调用了start,那么它们有什么区别和联系?
- run()
run方法主要是描述了线程要执行的任务,它本身并不具备创建线程的功能,当显示的调用run()时,会由调用者线程顺序执行run中的代码,且run方法属于类中的一个普通方法,可以被多次调用。
- start()
start方法会真正去创建一个系统线程,当调用start方法时,其内部会调用到本地native方法(JVM对系统调用的封装)在操作系统内核中创建一个PCB (Process Control Block,操作系统内核中是以PCB结构体的形式来表示一个线程),然后等待操作系统将其调度,当该PCB被调度到CPU上运行时,会执行到线程中重写的run方法,最后当run方法结束时,操作系统会自动销毁内核中的PCB。start()方法只能被调用一次,多次调用会抛IllegalThreadStateException异常。
四、Thread的几个常见属性
Thread类中包含线程的一些属性和状态,我们可以通过对应的公共方法来查看。
属性或状态 | 获取方法 |
---|---|
ID | getId() |
线程名称 | getName() |
线程状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否中断 | isInterrupted() |
1. ID
不能显示设置,由系统自动分配,类中只提供了getId()方法来查看。
2. 线程名称
在构造对象时可以设置,也可以通过setName()方法设置。
3. 线程状态
线程的整个生命周期中会经历不同的状态,状态是描述当前线程的调度情况,这里查看的是Java虚拟机中的线程状态(对操作系统线程的状态的进一步封装)。
Thread中一共描述了六种线程状态:
NEW:线程对象刚创建好,执行任务已交代,但是还未调用start方法,在操作系统内核态中还没有对应的PCB。
RUNNABLE:线程处于就绪队列中,可以随时被调度到CPU上执行,或线程正在CPU上执行。
BLOCKED:线程处于阻塞队列中,等待获取到同步锁。
WAITING:线程无期限地处于阻塞队列中,等待另一个线程执行唤醒操作。
TIMED_WAITING:线程处于阻塞队列中不超过指定时间,可以提前被唤醒或到达指定时间自动唤醒。
TERMINATED:线程完成了执行任务,内核态中的PCB已释放,但是thread对象还存在。
当操作系统内核中的PCB释放之后,Java虚拟机中的thread对象还会存在一段时间,当垃圾回收器空闲时会自动将其回收。
4. 优先级
在操作系统内核中每个线程都会有不同的优先级,内核中的优先级是操作系统来设置和调整的,我们从应用层很难干涉,但是ThreadAPI还是给我们提供了getPriority()和setPriority()方法来设置线程对象的优先级,似乎没什么用,即使我们设置了最大优先级,操作系统内核也不一定会按照这个优先级来进行线程调度。
5. 是否后台线程
线程分为两种:前台线程和后台线程(后台线程也称守护线程)。
-
前台线程:会阻止进程结束,前台线程的任务没跑完,进程是不会结束的(除非手动关掉进程)。
-
后台线程:不会阻止进程结束,后台线程的任务没跑完,进程也是能够结束的。
我们在代码里创建的线程默认都是前台线程,包括main主线程在内。而其他JVM启动时自带的线程都是后台线程,例如GC线程。
可以调用isDaemon()方法来查看是否为后台线程,通过setDaemon()方法来设置线程。
控制台输出信息只打印了几次就显示进程结束了,这是因为我们将子线程设置为了后台线程,虽然t内部的代码逻辑是一直打印输出,但是main线程在调用t.start()方法之后就运行完毕了,当程序中的所有前台线程都结束时Java虚拟机会结束进程,所以这也证实了即使后台线程的任务没跑完,进程也能够结束。
注意:setDaemon()方法必须要在调用start方法之前被调用,否则会抛异常。
6. 是否存活
调用isAlive()方法可以看到当前线程是否存活,本质上是检查操作系统内核中的PCB是否存在,如果线程对应的PCB还未创建(即还未调用start()方法)或线程的run()方法运行完了,调用isAlive()都会返回false。
7. 是否中断
在某些情况下我们需要中断某个线程的运行,这里的中断不是让线程立刻就结束运行,而是通知线程应该结束了,具体何时结束得看实际线程里的代码逻辑。我们可以通过设置变量标志位或者用Thread的isInterrupted()方法配合interrupt()方法来通知线程中断。
设置变量标志位
isInterrupted()和interrupt()
我们来看三个版本
- 线程忽略中断请求
结合代码并观察控制台结果进行分析,在主线程休眠3秒后调用了t.interrupt()方法,按理说子线程应该被中断了,但是程序抛出异常后又继续运行了,原因是如果子线程是在阻塞状态下(这里是sleep引发的)被调用interrupt(),会导致中断状态被清空,也就是恢复成false,处理异常后当再次回到while时就会继续执行下去。
-
线程立即响应中断
-
线程稍后处理中断
五、线程休眠
线程休眠的意思就是让线程不参与调度了(不去CPU上运行了)。
原本调用了start()方法的线程都处于操作系统内核的就绪队列中,随时准备被调度到CPU上运行。一旦某个线程被休眠了就会从就绪队列移出并加入阻塞队列,当休眠时间到了才会重新加入就绪队列。
调用sleep(millis)
休眠结束
六、等待线程
线程本身是一个随机调度的过程(线程由操作系统进行调度,我们在应用层干涉不了,而操作系统的调度策略比较复杂,所以我们这里就理解为随机调度),各线程之间是抢占式执行。通过等待线程可以控制线程的执行顺序。
实现的API
代码演示
我们还可以使用从Object中继承的wait()和notify()等方法来实现等待线程,也可以说是线程间相互协作。
七、多线程应用案例
我们通过完整的代码来感受一下应用多线程与单线程之间的区别。
/**
* 多线程应用案例:对比多线程与单线程在CPU操作密集下的执行效率
* 让单线程先对a变量累加COUNT次,然后再对b变量累加COUNT次;
* 创建两个线程,同时对a,b变量累加COUNT次。
*
*/
public class Demo {
private static final long COUNT = 100_0000_0000L;
public static void main(String[] args) {
//测试单线程执行用时
serial();
//测试多线程执行用时
concurrency();
}
public static void serial(){
//开始时间
long start = System.currentTimeMillis();
long a = 0L;
for (long i = 0; i < COUNT; i++) {
a++;
}
long b = 0L;
for (long i = 0; i < COUNT; i++) {
b++;
}
//计算用时
long end = System.currentTimeMillis();
System.out.println("单线程执行用时:" + (end - start) + "ms");
}
public static void concurrency(){
//开始时间
long start = System.currentTimeMillis();
//创建两个线程
Thread t1 = new Thread(() -> {
long a = 0L;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
Thread t2 = new Thread(() -> {
long b = 0L;
for (long i = 0; i < COUNT; i++) {
b++;
}
});
//启动线程
t1.start();
t2.start();
//让主线程等待,
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//计算用时
long end = System.currentTimeMillis();
System.out.println("并发执行用时:" + (end - start) + "ms");
}
}
通过控制台输出可以看到,多线程确实是真正提高了执行效率了,因为充分利用了CPU的多核心资源。
附录:通过jconsole监视和管理线程
在JDK安装路径的bin目录下可以看到很多用java编写的开发工具,jconsole.exe就是用来进行线程管理的工具。
打开jconsole可以看到当前电脑上运行的java进程。
文章为本人独立编写,难免会有错误之处。
如发现有误,恳请评论提出!