⭐️前面的话⭐️
本篇文章介绍java多线程中Thread类的使用以及有关线程对象创建和常用方法。
博客主页:未见花闻的博客主页
欢迎关注点赞收藏⭐️留言
本文由未见花闻原创,CSDN首发!
首发时间:2022年3月29日
✉️坚持和努力一定能换来诗与远方!
参考书籍:《Java核心技术系列》,《Java编程思想》
参考在线编程网站:牛客网力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
也就是说Thread
类的一个实例就对应着一个线程。
序号 | 方法名 | 解释 |
---|---|---|
1 | public Thread() | 无参数构造方法 |
2 | public Thread(Runnable target) | 传入实现Runnable接口的对象(任务对象)构造线程 |
3 | public Thread(Runnable target, String name) | 根据目标任务并指定线程名创建线程 |
4 | public Thread(ThreadGroup group, Runnable target) | 根据线程组和任务创建线程(了解) |
5 | public Thread(ThreadGroup group, Runnable target, String name) | 比构造方法4多一个指定线程名 |
6 | public Thread(String name) | 指定线程名创建线程 |
7 | public Thread(ThreadGroup group, String name) | 根据线程组并指定线程名创建线程 |
8 | public Thread(ThreadGroup group, Runnable target, String name,long stackSize) | 构造函数与构造方法5相同,只是它允许指定线程堆栈大小 |
注:线程可以被用来分组管理,分好的组即为线程组,Runnable
类表示任务类,也就是线程需执行的任务。
想要使用java线程至少得知道Thread
类中这几个方法:
方法名 | 解释 |
---|---|
public void run() | 该方法用来封装线程运行时执行的内容 |
public synchronized void start() | 线程创建并执行run方法 |
public static native void sleep(long millis) throws InterruptedException | 使线程休眠millis毫秒 |
创建Thread
对象,必须重写run
方法,因为你创建一个线程肯定要用运行一些代码嘛。
首先,我们可以创建一个MyThread
类继承Thread
类,并重写run
方法。
class MyThread extends Thread{
//重写run方法
@Override
public void run() {
System.out.println("你好!线程!");
}
}
public class TestDemo {
public static void main(String[] args) {
//创建MyThread线程对象,但是线程没有创建
Thread thread = new MyThread();
//线程创建并运行
thread.start();
}
}
使用new
创建线程对象,线程并没有被创建,仅仅只是单纯地创建了一个线程对象,运行start
方法时才会创建线程并执行run
方法。
运行结果:
除了使用子类继承Thread
类并重写run
方法,使用子类实现Runnable
接口(该接口中也有一个run
方法,表示任务的内容),该对象可以理解为“任务”,也就是说Thread
对象可以接受Runnable
引用,并执行Runnable
引用的run
方法。
因为Runable
是一个接口,所以需要实现run
方法,线程Thread
对象创建好后,此时线程并没有创建运行,需要调用start
方法来创建启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("使用Runnable描述任务!");
}
}
public class TestDemo3 {
public static void main(String[] args) {
//将Runnable任务传给Thread对象来创建运行线程
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
运行结果:
根据“低内聚,高耦合”的编程风格,使用Runnable
的方式创建更优。
当然也可以使用匿名内部类,来传入匿名对象来重写run
方法。
public class TestDemo4 {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名内部类创建线程匿名对象");
}
};
thread.start();
}
}
使用Lambda表达式,本质还是使用匿名内部类创建的Thread
。
public class TestDemo6 {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("使用Lambda表达式表示匿名内部类来创建匿名任务"));
thread.start();
}
}
运行结果:
在一个进程中至少会有一个线程,如果不使用多线程编程,一个进程中默认会有执行main
方法的main
线程(该线程是自动创建的),当你创建一个新的线程t
,该线程会与main
线程并发执行。
public class TestDemo7 {
public static void main(String[] args) {
//thread 线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("thread线程执行中!");
//为了使效果更加明显 可以使用sleep方法设定线程睡眠时间
try {
Thread.sleep(1000);//每执行一次循环就睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
//main 线程
for (int i = 0; i < 10; i++) {
System.out.println("main线程执行中!");
//为了使效果更加明显 可以使用sleep方法设定线程睡眠时间
try {
Thread.sleep(1000);//每执行一次循环就睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
从上面的运行结果可以看出一个问题,因为thread
线程与main
线程都是每打印一句语句线程休眠1
秒,两个线程唤醒的先后顺序是随机的,这也是java多线程中的一个“万恶之源”,这个问题给我们带来了很多麻烦,细节等后续的博客细说。
加入我们现在有一个任务,就是分别将a
和b
两个变量都自增20亿次,我们来看看使用两个线程和单独使用一个线程分别所需的时间是多少。
public class Test {
private static final long COUNT = 20_0000_0000L;
//两个线程
public static void many() throws InterruptedException {
//获取开始执行时间戳
long start = System.currentTimeMillis();
Thread thread1 = new Thread(() -> {
long a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
long b = 0;
for (long i = 0; i < COUNT; i++) {
b++;
}
});
thread2.start();
//等待两个线程结束 再获取结束时的时间戳
thread1.join();
thread2.join();
long end = System.currentTimeMillis();
//执行时间,单位为毫秒
System.out.println("多线程执行时间:" + (end - start) + "ms");
}
//单线程
public static void single() {
//记录开始执行的时间戳
long start = System.currentTimeMillis();
long a = 0;
for (long i = 0; i < COUNT; i++) {
a++;
}
long b = 0;
for (long i = 0; i < COUNT; i++) {
b++;
}
//获取执行结束时的时间戳
long end = System.currentTimeMillis();
System.out.println("单线程执行时间:" + (end - start) + "ms");
}
public static void main(String[] args) throws InterruptedException {
//多线程
many();
//单线程
single();
}
}
我们来看看完成这个任务所需的时间:
根据结果我们发现两个线程并发执行的时间大约是500ms
左右,单线程执行的时间大约是1000ms
左右,当然如果任务量不够大,可能多线程相比于单线程并不会有优势,毕竟创建线程本身还是有开销的。
属性 | 获取该属性的方法 |
---|---|
线程的唯一标识ID |
public long getId() |
线程的名称name |
public final String getName() |
线程的状态state |
public State getState() |
线程的优先级priority |
public final int getPriority() |
线程是否后台线程 | public final boolean isDaemon() |
线程是否存活 | public final native boolean isAlive() |
线程是否中断 | public boolean isInterrupted() |
每一个线程都拥有一个id
作为标识,其中处于同一进程的所有线程id
相同,每个进程间都有唯一的id
标识。
线程也是拥有名字的,如果我们创建Thread
对象时,没有指定线程对象的名称,则会默认命名为Thread-i
,其中i
为整数。
通过了解进程,我们知道进程拥有3
种状态,分别为阻塞,执行和就绪。而java中的线程也有类似与这种状态的定义,后面我们细说,优先级也一样就不用多说了。
java线程分为后台线程与前台线程,其中后台线程不会影响到进程的退出,而前台线程会影响进程的退出,比如有线程t1
与线程t2
,当这两个线程为前台线程时,main
方法执行完毕时,t1
与t2
不会立即退出,要等到线程执行完毕,整个进程才会退出,反之,当这两个线程为后台线程时,main
方法执行完毕时,t1
与t2
线程被强制结束,整个进程也就结束了。
关于java线程的属性,我们可以通过java官方的jconsole
调试工具查看java线程的一些属性。
这个工具一般在jdk的bin
目录,
双击打开有如下界面:
选择需要查看的线程并查看:
方法名 | 解释 |
---|---|
public void run() | 该方法用来封装线程运行时执行的内容 |
public synchronized void start() | 线程创建并执行run方法 |
public static native void sleep(long millis) throws InterruptedException | 使线程休眠millis毫秒 |
public final void join() throws InterruptedException | 等待线程结束(在哪个线程中调用哪个对象的join方法,哪个线程就等待哪个对象) |
public final synchronized void join(long millis) throws InterruptedException | 等待线程结束,最多等待millis毫秒 |
public final synchronized void join(long millis, int nanos) throws InterruptedException | 指定最多等待时间等待线程,精确到纳秒 |
public void interrupt() | 中断线程对象所关联的对象,如果线程在休眠(阻塞状态)会抛出异常通知,否则设置中断标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后会清除线程的中断标志位 |
public boolean isInterrupted() | 判断当前线程的中断标志位是否设置,调用后不会影响线程的标志位 |
public final synchronized void setName(String name) | 修改线程对象名称 |
public static native Thread currentThread() | 获取当前线程对象 |
如果我们想中断一个正在执行的线程,该如何做呢?最简单但不严谨的方法就是我们在run
方法中定义一个中断标志位(需要中断时标志位为true
,默认情况为false
),每次执行具体任务时需要先判断中断标志位是否为true
,如果是就结束线程,否则继续执行。
public class TestDemo8 {
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while(!isQuit) {
//每隔1秒打印一句
System.out.println("一个不起眼的线程!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//main线程阻塞5秒 按理会打印5句话
Thread.sleep(5000);
isQuit = true;
}
}
但是该方法是不够严谨的,有些场景可能达不到预期的效果,最优的做法就是调整线程对象或者线程类中的自带标志位。
方式1:使用Thread对象中的标志位
首先使用currentThread
方法获取线程对象,然后再调用该对象中的isterrupted
方法获取该对象的中断标志位代替我们自己所写的isQuit
标志位,然后等该线程运行一段时间后使用interrupt
方法改变标志位,中断线程,写出如下代码,看看能不能达到预期效果:
public class TestDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() ->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("又是一个不起眼的线程!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//main休眠5秒
Thread.sleep(5000);
//使用interrupt方法修改线程标志位,使其中断
thread.interrupt();
}
}
我们来看一看:
失败了,抛出一个InterruptedException
异常后,线程没有中断,而是继续运行,原因是interrupt
方法遇到因为调用 wait/join/sleep
等方法而阻塞的线程时会使sleep
等方法抛出异常,并且中断标志位不会修改为true
,这时我们的catch
语句里面值输出了异常信息并没有去中断异常,所以我们需要在catch
语句中加上线程结束的收尾工作代码和退出任务循环的break
语句就可以了。
public class TestDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() ->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("又是一个不起眼的线程!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//收尾工作
System.out.println("收尾工作!");
break;
}
}
});
thread.start();
//main休眠5秒
Thread.sleep(5000);
//使用interrupt方法修改线程标志位,使其中断
thread.interrupt();
}
}
方式2:使用Thread类中的标志位
除了isInterrupted
,还有一个静态方法interrupted
能够访问类中的标志位,一般一个程序中只有一个,我们也可以使用该静态方法来作为中断标志位,然后到时机后使用interrupt
方法来中断线程执行。
、
public class TestDemo10 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (!Thread.interrupted()) {
System.out.println("又又是一个不起眼的线程!");
try {
//设置打印频率为1s
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//收尾工作
System.out.println("收尾工作!");
break;
}
}
});
thread.start();
//main休眠5秒
Thread.sleep(5000);
//使用interrupt方法修改线程标志位,使其中断
thread.interrupt();
}
}
运行结果:
综上所述,一般以方式1的方式无脑中断线程就可以。
像上面的计算自增20亿次的例子就需要线程等待join
方法,main
线程需要等两个线程运行完毕后才能计算计算结束时的时间戳。
针对这一点java还准备了带参数的join
方法,可以指定最长的等待时间。
还有一个细节那join
方法是谁等谁呢?
我们来假设几个线程,线程A表示调用join
方法的线程,线程B表示join
方法来自B线程对象,那么在A线程使用B.join
方法,那就是A线程等待B线程结束。
我们知道执行一个线程的任务就是线程对象中所重写的run
方法,那么可以直接调用run
方法来代替start
方法吗?
当然不行!因为你调用run
方法就是单纯地调用了Thread对象中的一个普通方法而已,并没有创建一个新线程来执行run
方法,而是通过main
线程来执行的run
方法,而使用start
方法,会创建一个新线程并执行run
方法。
操作系统中进程的状态有三种分别为阻塞,就绪和执行,而java线程中的状态基本上相同,但做了细分,有一点区别,我们来认识一下。
NEW
: 安排了工作, 还未开始行动,就是线程对象存在,但没有执行start
方法,java内部的状态,与进程中的状态无关。
RUNNABLE
: 就绪状态。
BLOCKED
: 线程正在等待锁释放而引起的阻塞状态(synchronized加锁)。
WAITING
: 线程正在等待等待唤醒而引起的阻塞状态(waitf方法使线程等待唤醒)。
TIMED_WAITING
: 在一段时间内处于阻塞状态,通常是使用sleep
或者join(带参数)
方法引起。
TERMINATED
:Thread对象还存在,但是关联的线程已经工作完成了,java内部的状态,与进程中的状态无关。
我先使用一个流程图来介绍状态之间的关系:
对于里面没有介绍的方法我们留在下回在分解。
这期的内容分享了有关线程创建执行以及有关Thread类中的基本方法,下期继续介绍多线程更深入的知识,比如线程安全问题,如何加锁等更深一点的内容。