这一篇我们就来了解一下,具体的Thread类的用法和使用场景。相信大家经过对多线程的一定认识都明白线程是需要创建的,接下来我们一起来详细学习Thread类。
Thread的常见构造方法
方法 | 说明 |
Thread()
| 创建一个线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程对象 |
Thread(String name) | 创建线程对象并命名 |
Thread(Runnable target, String name)
|
使用
Runnable
对象创建线程对象,并命名
|
【了解】
Thread(ThreadGroup group,
Runnable target)
|
线程可以被用来分组管理,分好的组即为线程组,这
个目前我们了解即可
|
上面就是常用的Thread类的构造方法,并不是很难,需要大家进行相应的练习,去记住如何创建Thread类对象即可。
class myRunnable1 implements Runnable {
@Override
public void run() {
System.out.println("我是线程二");
}
}
class myRunnable2 implements Runnable {
@Override
public void run() {
System.out.println("我是线程四");
}
}
public class demo9 {
public static void main(String[] args) throws InterruptedException {
// 创建线程对象
Thread t1 = new Thread();
Thread t2 = new Thread(new myRunnable1());
Thread t3 = new Thread("我是线程三");
Thread t4 = new Thread(new myRunnable2(), "我是线程四");
// 开启线程
t1.start();
Thread.sleep(1000);
t2.start();
Thread.sleep(1000);
t3.start();
Thread.sleep(1000);
t4.start();
Thread.sleep(1000);
System.out.println("我是主线程");
}
}
Thread的常见属性
属性 | 获取方法 |
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否有后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
上面是我们开发当中常用的Thread类常用的属性,我们来挨个介绍一下他们使用的场景和使用原因。
ID
ID是一个十分好用的东西,我们每个人都有自己的身份认证,在计算机的世界里面也是如此,我们既然创建了一个线程,那在计算机的世界里也会有属于他的身份认证。它可以让我们在进行调试线程的时候起到巨大的帮助,当我们创建一个线程的时候,JVM就会帮我们自动分配一个唯一的正整数作为这个线程的标识符,我们可以通过这个标识符来对这个线程进行调试、日志记录和线程管理。
它具有很强的线程安全性,getId()方法是Thread类的非静态方法,他操作的是调用它具体的线程对象,在多线程的使用环境下也不用担心返回的线程ID是错乱的。
public class ThreadgetId {
public static void main(String[] args) {
// 创建两个线程
Thread thread1 = new Thread(() -> {
long id = Thread.currentThread().getId();
System.out.println("Thread 1 ID: " + id);
});
Thread thread2 = new Thread(() -> {
long id = Thread.currentThread().getId();
System.out.println("Thread 2 ID: " + id);
});
// 开启线程
thread1.start();
thread2.start();
}
}
名称
讲完了ID,接下来我们再来讲一讲名称的使用,其实跟ID的用意很类似,但开发人员能更直观地看到每个线程的差异。就好比我们每个人都有自己的名字,也会有自己的身份证,国家可以通过身份证的信息来找到我们具体的活动轨迹,在计算机世界里,我们使用getName()方法获取的线程名称是和线程的ID绑定在一起的,但和我们现实生活不同,我们每个人的名字可能会出现有重名的可能,所以需要我们使用身份证号码来给每个人进行身份认证,但如果你在一个程序中给两个及以上的线程都命名一样的名称,会在我们去进行调试和管理上面耗费大量的时间,在我们实际的开发过程中,保证线程名称的唯一性。
public class ThreadgetName {
public static void main(String[] args) throws InterruptedException {
// 使用lambda表达式创建多线程
Thread t1 = new Thread(() -> {
System.out.println("我是线程一");
},"线程一");
Thread t2 = new Thread(() -> {
System.out.println("我是线程二");
},"线程二");
// 获取名称
System.out.println("Thread 1 name: " + t1.getName());
System.out.println("Thread 2 name: " + t2.getName());
t1.start();
t2.start();
Thread.sleep(1000);
t1.join();
t2.join();
// 重新设置线程名称
t1.setName("我现在是线程二");
t2.setName("我现在是线程一");
System.out.println("Thread 1 name: " + t1.getName());
System.out.println("Thread 2 name: " + t2.getName());
}
}
状态
线程状态是描述了一个线程在生命周期中的不同状态。
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
// 展示线程状态枚举常量
System.out.println(state);
}
}
}
Java中的线程状态是通过 Thread.State
枚举类来表示的,主要包括以下几种状态:
NEW(新建状态):当线程对象刚刚被创建但尚未启动时,它处于新建状态。
RUNNABLE(可运行状态):线程可以运行,可能正在等待 CPU 时间片(在操作系统层面),也可能正在执行任务。
BLOCKED(阻塞状态):线程因为等待监视器锁(synchronized关键字锁定的对象)而被阻塞,其他线程持有锁,当前线程无法获取锁时会进入这种状态。
WAITING(等待状态):线程无限期地等待另一个线程来执行某一特定操作。例如,调用 Object.wait()
方法或者 Thread.join()
方法而没有指定超时时间。
TIMED_WAITING(计时等待状态):线程等待另一个线程来执行操作,但是等待有超时限制。例如,调用 Thread.sleep()
方法或者 Object.wait()
方法。
TERMINATED(终止状态):线程已经执行完毕,不再活动。
接下来我来详细讲一下这几种状态在实际线程运作时的表现情况:
public class ThreadStateTransfer {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// 给线程一个代码执行
for (int i = 0;i < 1000000;i++) {
}
}
},"李四");
System.out.println(t.getName() + ":" + t.getState());
// 开启线程
t.start();
// 如果线程存活,那就执行sout语句
while (t.isAlive()) {
System.out.println(t.getName() + ":" + t.getState());
}
// 打印线程执行完代码状态
System.out.println(t.getName() + ":" + t.getState());
}
}
上述代码运行就说明了线程状态在其生命周期执行的一般过程,首先线程在没有开启的时候是NEW状态,代表已经被创建出来,但没有执行代码,是未启动的状态。
等到我们开启线程后,线程开始执行run()方法中的代码块,在我们的线程状态中就是正在运行,也就是RUNNABLE。
当我们在运行代码块的时候,用isAlive()方法判断线程是否执行完所有的代码块,如果执行完后就跳出循环,再打印线程状态,我们会发现显示的是TERMINATED,代表线程已经执行完毕。但这不意味着线程的生命周期到这一步就彻底结束,在我们目前的代码看来,这个我们创建的线程确实是完成了他的任务,但在我们以后的企业开发当中,可能会有线程池之类的操作,会把线程重新利用起来执行其他任务的代码块,那这个时候就不是死亡,而是新生,这也是为什么制定线程的大佬没有将线程任务执行完毕的状态常量设置为DEAD。
public class ThreadStateTransfer2 {
public static void main(String[] args) {
final Object object = new Object();
// 创建一个执行Thread睡眠的线程
Thread t1 = new Thread(() -> {
// 将执行代码块上锁,查看线程状态
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1");
// 开启t1线程
t1.start();
// 测试能不能执行到t2线程的代码块
Thread t2 = new Thread(() -> {
synchronized (object) {
System.out.println("ko no dio da!");
}
},"t2");
t2.start();
// 创建一个主线程来执行代码块
while(true) {
System.out.println("看看我运行了没有~");
}
}
}
但我们不能直观的感受到TIMED_WAITING和BLOCKED的线程状态,因为你在主线程中使用getState()方法获取到的线程状态只有未运行、正在运行和运行完毕这三种,因为在主线程中,你创建的线程正在执行代码块的编译,而不是本身线程的状态,也可以理解成你的老板只会看你人有没有来上班,也就是没来、来上班了和下班,但只有你自己知道身体舒不舒服,要不要和老板请假之类的~所以说我们在主线程中获取我们自己创建的线程状态并不是线程本身的状态,而是在主线程里的状态~
那我既然说了这么多,友友们一定就在想,难道没有更好的办法去获取TIMED_WAITING和BLOCKED的线程状态了吗?那当然是有的!而且还是可视化界面,可以很直观的让我们开发人员感受到线程状态的变化~
我们在我们JDK的版本jar包中找到一个叫jconsole.exe的程序,这个就是观察线程使用的监控管理工具,它不仅是显示线程状态的,还是监视JVM和Java应用程序的工具,我们可以在里面来查看JVM的运行状态、内存使用情况和线程信息等等,是一款十分好用的可视化工具。
我们还可以在我们的命令行里输入jconsole直接打开,效果是一样的。
打开以后我们会发现上面会显示我们的本地进程,我们要找到我们想要监视和管理的Java进程。
我们已经在后台运行了我上面的示例代码,就会在本地进程中找到对应的进程名称,这时候我们就可以点击连接去查看这个进程内的所有线程了!我们直接选择不安全的连接去查看线程即可。
点击线程就可以看到我们在ThreadStateTransfer2 上所有的线程了,我们来看看有哪些重要的线程把!
我们可以看到在我们创建并运行的Java程序中有正在执行的main线程,它一直在打印字符,所以它的线程状态是RUNNABLE。
我们接着再来看我们创建的t1线程,我们会发现它自身是一个TIMED_WAITING的状态,代表着我们这个线程正在执行Thread.sleep(1000)这串代码,这串代码会让我们的线程处于一个等待的时间,但这个等待时间并不是其它线程没有执行完,而是说需要过1000毫秒才能去执行其他命令。
我们拿生活中的例子来比较,比如说老板给你派了一项任务,让你必须今天把这个方案写出来,那你是不是拼命往死里写,不然可能就被炒鱿鱼了呀,但突然写着写着,肚子开始痛起来了,你才意识到中午吃的便宜外卖不干净,你急忙去上厕所,结果这个时候老板看看你任务完成的怎么样了,结果就发现你的工位是空的,老板打电话问你什么情况,你这时候就是一个TIMED_WAITING的状态,你只好告诉老板现在拉肚子,我需要上完厕所后才能继续完成任务,也就是有时间限制的等待~
我们再来看t2线程的状态,我们可以发现它处在一个BLOCKED的状态,为什么会造成这样的情况呢?这里其实和锁竞争有一定的关系~因为我们在代码中添加了一个叫synchronized的关键字(这个之后会详细介绍),他把我们的线程锁了起来,我们这里用一个定义的Object对象来表示一把锁,t1和t2都用的是同一把锁,当t1线程还没有结束,一直在死循环的时候,我们的t2线程只能乖乖的等t1执行完毕后才能执行自己的代码块,因为他们用的是同一把锁,现在只有t1有钥匙,而t2没有,所以就会出现阻塞的状态,也就是BLOCKED。所以我们可以看到工具上面显示拥有者是t1线程就是这个原因~
还是用生活中的例子来讲,接着上文,你们公司的小李这时候也是肚子疼,也是想来上厕所,但你们老板很抠门,整个公司十几个部门,就一个坑位!这时候小李就拍门说让你快点,他已经小荷才露尖尖角了,这个时候你就是t1线程,而可怜的小李就是t2线程,他必须等你上完了才能去释放,这个过程就是小李被你阻塞了,也就是BLOCKED~
public class ThreadStateTransfer3 {
public static void main(String[] args) {
final Object object = new Object();
// 创建一个执行Thread睡眠的线程
Thread t1 = new Thread(() -> {
// 将执行代码块上锁,查看线程状态
synchronized (object) {
while (true) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1");
// 开启t1线程
t1.start();
// 测试能不能执行到t2线程的代码块
Thread t2 = new Thread(() -> {
synchronized (object) {
System.out.println("ko no dio da!");
}
},"t2");
t2.start();
while(true) {
System.out.println("看看我运行了没有~");
}
}
}
我们再创建一个class,让我们把Thread.sleep(1000),变成object.wait(),再来查看我们的线程状态会不会有所改变,打开我们的jconsole窗口看看是个啥情况~
我们发现因为线程t1调用 object.wait()
来使线程进入等待状态(WAITING 状态),这意味着线程t1陷入了一个没有时间限制的等待状态,我相信各位友友们应该也理解了TIMED_WAITING和WAITING的区别,如果再拿我们举例子的话,那就只能一直在厕所便秘直到活活憋死,多少有点不人性了哈~
优先级
说到优先级,我相信各位友友们都能明白是啥意思,优先级高的那肯定就是优先享有权限,就还是可怜的小李,如果恰好这天没有你和他抢厕所,他正准备进去,一抬头就发现老板也想上厕所,这个时候小李自然是不敢抢占老板上厕所的,这也正是因为老板的优先级比小李高,如果是你和小李抢厕所,那真就是比谁更有参加男子一百米决赛的实力了~因为你和小李都是这个公司最底层的小社畜,你们的优先级是一样的~
public class ThreadPriority {
public static void main(String[] args) {
// 创建三个线程
Thread t1 = new Thread(() -> {
System.out.println("我是爷爷!");
},"爷爷");
Thread t2 = new Thread(() -> {
System.out.println("我是爸爸!");
},"爸爸");
Thread t3 = new Thread(() -> {
System.out.println("我是儿子!");
},"儿子");
// 查看当前线程优先级
System.out.println(t1.getPriority());
System.out.println(t2.getPriority());
System.out.println(t3.getPriority());
// 开启线程
t1.start();
t2.start();
t3.start();
}
}
我们可以看到在没有手动设置优先级的情况下,我和爸爸还有爷爷都是同辈的,那肯定不行,不然你爸爸还是你爸爸吗,你爷爷还是你爷爷吗?我们需要进行一些小小的修改。
class old implements Runnable {
@Override
public void run() {
while(true) {
System.out.println("还是"+Thread.currentThread().getName()+"技高一筹!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadPriority {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
// 创建三个线程
Thread t1 = new Thread(new old(),"爷爷");
Thread t2 = new Thread(new old(),"爸爸");
Thread t3 = new Thread(new old(),"儿子");
// 设置每个线程的优先级
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t3.setPriority(Thread.MIN_PRIORITY);
// 查看当前线程优先级
System.out.println(t1.getName() + "优先级是" + t1.getPriority());
t1.start();
System.out.println(t2.getName() + "优先级是" + t2.getPriority());
t2.start();
System.out.println(t3.getName() + "优先级是" + t3.getPriority());
t3.start();
}
}
这时我们就可以发现我们的优先级发生了改变,死循环打印数据会发现爷爷在第一个的概率是最高的。
那是不是意味着优先级就是无敌的呢?当然不是,如果小李实在忍不住了,他也一样不会让着老板,肯定自己先舒舒服服去释放了~所以说线程优先级的确是一个指示,而不是强制性规定。具体的线程调度行为依赖于操作系统和JVM的具体实现。高优先级的线程通常会更频繁地被调度执行,但并不保证它们一定会在低优先级线程之前完成。所以我们在平常使用的时候一定要慎重去使用优先级来控制线程的行为,他可能会导致不可预料的结果。
所以那种只有一个厕所的公司老板真的有存在的必要吗?
后台线程
说完了优先级,我们再来说说后台线程。
public class ThreadDaemon {
public static void main(String[] args) {
Thread normalThread = new Thread(new NormalTask());
Thread daemonThread = new Thread(new DaemonTask());
// 将daemonThread设置为后台线程
daemonThread.setDaemon(true);
// 启动线程
normalThread.start();
daemonThread.start();
System.out.println("主线程准备退出");
}
static class NormalTask implements Runnable {
public void run() {
try {
Thread.sleep(5000); // 模拟任务执行5秒钟
System.out.println("普通任务已完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class DaemonTask implements Runnable {
public void run() {
while (true) {
try {
Thread.sleep(1000); // 模拟后台任务每秒执行一次
System.out.println("后台任务正在执行中");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
在这个示例中,创建了两个线程 normalThread
和 daemonThread
,分别执行普通任务和后台任务。daemonThread
被设置为后台线程(通过 setDaemon(true)
方法),而 normalThread
是普通线程。在主线程中,先启动了这两个线程,然后主线程输出一条信息表示即将退出。
由于 daemonThread
是后台线程,当所有的非后台线程(这里指 normalThread
)完成后,程序会自动退出,并且 daemonThread
的执行也会随之停止。在示例中,daemonThread
每秒输出一条信息,直到程序退出。
这种后台线程通常用于执行一些不需要在应用程序退出时手动关闭的任务,比如监控、日志记录等。
中断
终于来到最后一个内容,不容易不容易,那我们最后就好好讲一讲什么是中断。
public class interrupteddemo1{
private static final Object lock = new Object();
private static volatile int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
try {
while (true) {
lock.wait(); // 等待被唤醒
if (isQuit == 1) {
System.out.println("t1 退出!");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
System.out.println("请输入isQuit:");
Scanner scanner = new Scanner(System.in);
isQuit = scanner.nextInt();
synchronized (lock) {
lock.notify(); // 唤醒等待的线程
}
}
});
t1.start();
t2.start();
}
}
我们中断一个线程最常见的有两种方式:
方法 | 说明 |
public void interrupt()
|
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,
否则设置标志位
|
public static boolean
interrupted()
|
判断当前线程的中断标志位是否设置,调用后清除标志位
|
public boolean
isInterrupted()
|
判断对象关联的线程的标志位是否设置,调用后不清除标志位
|
public class interrupteddemo2 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程正在运行");
Thread.sleep(1000); // 可能被中断的阻塞操作
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断标志位
System.out.println("结束线程");
}
});
t.start();
// 主线程等待一段时间后中断 t2 线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt(); // 请求中断 t2 线程
}
}
在这个示例中,t
线程在 Thread.sleep()
中阻塞,主线程调用 t.interrupt()
后,t
线程会抛出 InterruptedException
,然后在异常处理中重新设置中断标志位,这样线程就能够响应中断并退出。
说实话效果是一样滴,但不过下面这种比较适合懒人,咳咳,开个玩笑,我们不仅要掌握Thread方法,也要明白它背后的原理是什么!
结尾
到了结尾讲一讲Thread当中的join()方法,其实我觉得start()方法和sleep()方法没必要讲,相信各位友友们跟着我的示例代码进行练习也可以很快明白其含义~
public class joindemo {
// 此处定义一个int类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0;i < 50000;i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2. join();
// 预期结果应该是10w
System.out.println("count:" + count);
// 上述这样的情况就是典型的线程安全问题!!!
}
}
我们会发现,在上面的示例代码中每次计算出来的结果都不一样这是为什么呢?
因为我们的count变量是一个静态变量,会被多个线程同时进行访问和修改,这就可能导致当t1线程还在将数据录入进去的时候,t2线程已经录入完毕,并修改了count的储存在内存中的值,那t1老弟肯修改的值肯定就不算数了呀,因为它没有把数据保存到内存当中,那这个数据就被浪费掉了,结果就是缺失了一次t1线程老弟修改count++的机会。有没有感觉就跟你上学时期有喜欢的人,当你还在通宵写情书,刚刚把情书放在他/她的抽屉里,就看到他/她接受了另一个人的表白,那你觉得你的这封信还有存在的必要吗?好像说多了都是眼泪,不过计算机的世界同样也是绝情绝义~
这就是线程存在非原子的操作导致执行过程不是原子性,不能将count的值正确的保存到内存当中。
那就有友友问了,博主博主,原子性是什么东西?我在这边浅浅的讲一下,之后在关于线程安全问题上面我会进一步阐释原子性的重要性,原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行,中间不会被打断。
为了保证我们的程序是确保原子性的,我们就需要采取一些强硬的手段来完成!
我们就可以使用join()方法来确保原子性!
public class joindemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t1.join();
t2.start();
t2.join();
// 预期结果应该是10w
System.out.println("count:" + count);
}
}
这样出来的数据就是具有原子性的,那为什么会是这个样子的捏,其实道理很简单,聪明的友友估计已经看出些许端倪了,我们把join()方法的位置调整了一下,join()方法的意义就在于等待一个线程结束,在我们没有修改的示例当中,我们的t1和t2是一起开启的,但它们的执行是不可预料的,就可能会发生上面横刀夺爱的情况,这当然是谁都不想看到的情况,那我们就等一个线程全部执行完后再去执行另一个线程的代码块,这样就避免的非原子性的可能发生。
当然还有很多方法,比如使用同步机制(synchronized)或者使用原子类等方法也可以完成上述的操作,当然这些具体的知识等到之后我也会详细的进行讲解~
剩下的join()方法的其他用法就等着各位自行探索了,我把方法使用说明放在下面!
方法 | 说明 |
public void join()
|
等待线程结束
|
public void join(long millis)
| 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos)
|
同理,但可以更高精度
|