你真的了解在java层面线程对象的属性的含义吗?本文章就来聊一聊我们为什么要使用多线程编程?以及描述线程对象的属性
一.多线程的优势
首先在进入主题之前,我们先来搞清除一点,我们什么时候需要使用多线程,为什么需要使用多线程?
我们用几个简单的例子来说明,首先现在有一个场景是:我们需要完成两次自增10亿次,我们分别使用单线程以及多线程来完成这个任务。
package demo1;
//场景是:我们需要完成两次自增十亿次,我们分别使用单线程和多线程来完成,对比他们分别的运行时间
public class demo_01multiThread {
//可以使用_来分隔看得更加清楚
public static long count=10_0000_0000l;
public static void main(String[] args) throws InterruptedException {
single_thread();
multiThread();
}
//单线程完成
public static void single_thread(){
//记录开始的时间
long start=System.currentTimeMillis();
int a=0;
for (int i = 0; i <count ; i++) {
a++;
}
int b=0;
for (int i = 0; i < count; i++) {
b++;
}
//记录完成后的时间
long end=System.currentTimeMillis();
System.out.println("单线程所需要的时间(ms): "+(end-start));
}
//多线程来完成
public static void multiThread() throws InterruptedException {
//使用多线程来完成
Thread thread1 =new Thread(()->{
int a=0;
for (int i = 0; i <count ; i++) {
a++;
}
});
Thread thread2 =new Thread(()->{
int b=0;
for (int i = 0; i < count; i++) {
b++;
}
});
//记录开始的时间
long start=System.currentTimeMillis();
thread1.start();
thread2.start();
//用.join()这个方法让他们等待这两个线程完成任务后再执行后续代码
thread1.join();
thread2.join();
long end=System.currentTimeMillis();
System.out.println("多线程所需要的时间(ms): "+(end-start));
}
}
说明:使用 System.currentTimeMillis() 来获取当前的时间戳,使用A.join(),A在B线程中调用了join方法,B会在A线程执行结束后,才会继续往后执行
为什么main线程要等两个线程结束才统计时间?
我们通过一个时间轴图片可以很清晰看到
三条直线分别代表三个线程的时间线,此时很明显的观察到,当main线程结束的时候,两个子线程并没有完成任务,所以需要使用.join()方法等待线程任务完成后统计时间
很明显我们会看到使用多线程的时候的时间效率相比单线程的时间效率会大大提高,接近一半左右,因为我们将任务平均分给了两个线程,也许有人会有这样子的疑问,但是为什么不是很平均的一半时间呢?表现出来的是一半多一点的时间呢?因为(1).创建线程也是需要时间的,(2)参与cpu调度多线程也需要时间,当然(3)线程在cpu上运行是‘抢占式的’,尽管由于1,2两点的原因你认为应该会导致时间上应该是一半多一点,但是由于(3)的原因,假设main线程抢占的慢一点,另外的子线程抢占的快一点,就会导致时间上表现出来的是一半少一点的时间
这个现象很明显让我们看到了多线程的好处,但是是什么情况都适合使用多线程吗?
再让我们看一看以下的这种情况,代码与上面的一样我们只需要将count的值改为1_0000,我们来看看运行结果
可以很明显的发现多数情况下单线程的速度反而更快,这就说明了一个问题,并不是所有情况下多线程都会更快,举个例子,例如一个公司发布了一个任务,一个人5分钟就可以完成,而老板一定要求把这个任务分成两份给两个人完成,此时老板又要去找另一个员工一起来完成,花了十分钟找到了另一个员工,然后完成了剩下的一部分任务,那是不是明明一个人可以很快完成,此时这种情况要两个人,找人的时间都已经够一个人完成工作,那这时候两个人反而降低了效率,类比到多线程,当一个任务很小的时候,单线程的效率也许会更快,因为创建线程也是需要时间的,线程参与cpu调度也是需要时间的
并不是任何时候多线程都会提高效率,当任务量很小的时候,单线程可能会比多线程的效率比更加高
二.线程Thread对象的属性
接下来我们来介绍一下java中描述thread图像的一些常用属性,以及获取方法
先整体看一下
ID:这个是JVM自动为thread对象分配的,是JVM层面的,并不是操作系统中TCB中的ID
名称:在创建线程的时候调用构造方法时候可以为线程取名字(后续会介绍thread构造方法)
状态:也是说的是java层面的thread对象的状态,与系统中的TCB不一样
优先级:这个由于不太准确,其实使用的并不多
是否后台线程:指的是在系统中创建的TCB是前台进程还是后台进程,在线程启动前设置
是否存活:说的是系统中的TCB是否存货,还是已经被销毁,与java层面的thread对象没有关系
是否被中断:这个是一个标志位,线程运行的时候用这个标志位来判断是否要中断
我们需要明确一点
我们在java层面创建thread对象->JVM调用系统api->在系统中创建了TCB
thread对象与系统中的TCB一一对应,但是他们所处的层面不同,所以他们的生命周期也不算相同的
接下来来详细某些属性,先看代码
package demo1;
public class demo_03Thread {
public static void main(String[] args) throws InterruptedException {
//创建一个线程
Thread thread =new Thread(()->{
//线程任务
for (int i = 0; i <5 ; i++) {
System.out.println("thread线程正在运行.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//在启动线程之前查看一下线程对象的属性
System.out.println(
"线程启动之前查看属性"+"\n"+
"thread的ID:"+ thread.getId()+"\n"
+"thread的Name:"+thread.getName()+"\n"
+"thread的State:"+thread.getState()+"\n"
+"thread的Priority:"+thread.getPriority()+"\n"
+"thread是否是后台线程:"+thread.isDaemon()+"\n"
+"thread对应的系统线程的是否存活:"+thread.isAlive()+"\n"
+"thread线程是否被中断:"+thread.isInterrupted()+"\n"
);
//启动线程
thread.start();
//等待一下thread线程,保证子线程启动
Thread.sleep(1000);
//启动线程后查看属性
System.out.println(
"线程运行中查看属性"+"\n"+
"thread的ID:"+ thread.getId()+"\n"
+"thread的Name:"+thread.getName()+"\n"
+"thread的State:"+thread.getState()+"\n"
+"thread的Priority:"+thread.getPriority()+"\n"
+"thread是否是后台线程:"+thread.isDaemon()+"\n"
+"thread对应的系统线程的是否存活:"+thread.isAlive()+"\n"
+"thread线程是否被中断:"+thread.isInterrupted()+"\n"
);
//等待子线程结束后被销毁
thread.join();
Thread.sleep(1000);
//线程结束后查看线程属性
System.out.println(
"线程被销毁之后查看属性"+"\n"+
"thread的ID:"+ thread.getId()+"\n"
+"thread的Name:"+thread.getName()+"\n"
+"thread的State:"+thread.getState()+"\n"
+"thread的Priority:"+thread.getPriority()+"\n"
+"thread是否是后台线程:"+thread.isDaemon()+"\n"
+"thread对应的系统线程的是否存活:"+thread.isAlive()+"\n"
+"thread线程是否被中断:"+thread.isInterrupted()+"\n"
);
}
}
运行结果拆分成了三个图来看:
来聊一下一些相关的属性,本文不讲状态属性会在后文说明
一.名称
线程的名称,可以在创建线程时候,调用构造方法给他取名,当没有给他取名的时候,
系统默认取名Thread-N(N从0开始往上递增)
以下是Thread的构造方法
我们使用构造方法取名后可以在我们上一篇文章中所说的jconsole中查看https://blog.csdn.net/2301_80293365/article/details/148035798?spm=1001.2014.3001.5501
package demo1;
public class demo_04threadName {
public static void main(String[] args) {
//创建一个自己给名字的线程
Thread thread1 =new Thread(()->{
while (true){
//Thread.currentThread()用于获取当前运行的线程对象的引用.
System.out.println("线程名字: "+Thread.currentThread().getName()+" 正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"我是你创建的线程");
//创建默认名字的线程
Thread thread2 =new Thread(()->{
while (true){
//Thread.currentThread()用于获取当前运行的线程对象的引用.
System.out.println("线程名字: "+Thread.currentThread().getName()+" 正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
thread1.start();
thread2.start();
}
}
既然讲到获取线程名字这里,我们也顺带介绍一下怎么获取,类的全民,方法名,结合这几个名字以后我们就可以通过这些来创建日志,这些也是日志中的一些相关信息
package demo1;
public class demo_05moreName {
public static void main(String[] args) {
//创建一个自己给名字的线程
Thread thread1 =new Thread(()->{
while (true){
//Thread.currentThread()用于获取当前运行的线程对象的引用.
System.out.println("线程名字: "+Thread.currentThread().getName()+" 正在运行");
System.out.println("类的全名: "+demo_05moreName.class.getName()+
" 方法名: "+Thread.currentThread().getStackTrace()[1].getMethodName()+//这里获取当前线程的堆栈信息,下标0是run方法,下标1是main方法
" 线程的名字: "+Thread.currentThread().getName() );
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"我是你创建的线程");
//启动线程
thread1.start();
}
}
通过这些方法可以明确某个线程所产生的日志
二.后台线程
一个线程不是前台线程就是后台线程,下面是代码演示
package demo1;
public class demo_06isDaemon {
//演示后台线程与前台进程的区别
public static void main(String[] args) {
//创建thread1线程,没有设置前后台线程默认为前台线程
Thread thread1 =new Thread(()->{
//线程任务并不是死循环执行5次即可完成
for (int i = 0; i <5 ; i++) {
System.out.println("线程thread1正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//创建thread2为后台线程,需要在线程启动之前设置
Thread thread2 =new Thread(()->{
//线程任务是死循环打印
while (true){
System.out.println("线程thread2正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//设置为后台线程
thread2.setDaemon(true);
//启动线程
thread1.start();
thread2.start();
}
}
我们在定义后台线程的时候在写任务的时候明明使用的是死循环,为什么这里的运行结果却停下来了?
1.在一个进程中,后台进程会在所有的前台线程执行结束后,立即停止,不论后台线程此时是否在执行任务
2.前台线程不受其他线程影响,不会因为其他线程结束而停止3.创建线程的时候默认是前台线程,只有在线程启动之前,手动设置为后台线程才可以
场景理解:当一个公司正在营业(前台线程),员工正在工作(后台线程),此时,公司关门断电了(前台线程执行结束),员工自然也要离开(后台线程因为前台线程的结束而立即停止)。
三.线程是否存活
由在上述,在刚刚进入描述属性的时候,本文章的代码块已经体现了,这里将他重现
package demo1;
public class demo_03Thread {
public static void main(String[] args) throws InterruptedException {
//创建一个线程
Thread thread =new Thread(()->{
//线程任务
for (int i = 0; i <5 ; i++) {
System.out.println("thread线程正在运行.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//在启动线程之前查看一下线程对象的属性
System.out.println(
"线程启动之前查看属性"+"\n"+
"thread的ID:"+ thread.getId()+"\n"
+"thread的Name:"+thread.getName()+"\n"
+"thread的State:"+thread.getState()+"\n"
+"thread的Priority:"+thread.getPriority()+"\n"
+"thread是否是后台线程:"+thread.isDaemon()+"\n"
+"thread对应的系统线程的是否存活:"+thread.isAlive()+"\n"
+"thread线程是否被中断:"+thread.isInterrupted()+"\n"
);
//启动线程
thread.start();
//等待一下thread线程,保证子线程启动
Thread.sleep(1000);
//启动线程后查看属性
System.out.println(
"线程运行中查看属性"+"\n"+
"thread的ID:"+ thread.getId()+"\n"
+"thread的Name:"+thread.getName()+"\n"
+"thread的State:"+thread.getState()+"\n"
+"thread的Priority:"+thread.getPriority()+"\n"
+"thread是否是后台线程:"+thread.isDaemon()+"\n"
+"thread对应的系统线程的是否存活:"+thread.isAlive()+"\n"
+"thread线程是否被中断:"+thread.isInterrupted()+"\n"
);
//等待子线程结束后被销毁
thread.join();
Thread.sleep(1000);
//线程结束后查看线程属性
System.out.println(
"线程被销毁之后查看属性"+"\n"+
"thread的ID:"+ thread.getId()+"\n"
+"thread的Name:"+thread.getName()+"\n"
+"thread的State:"+thread.getState()+"\n"
+"thread的Priority:"+thread.getPriority()+"\n"
+"thread是否是后台线程:"+thread.isDaemon()+"\n"
+"thread对应的系统线程的是否存活:"+thread.isAlive()+"\n"
+"thread线程是否被中断:"+thread.isInterrupted()+"\n"
);
}
}
注意:这里说的是系统中的TCB是否存活,与Thread对象无关
这里也许有人会出现这种代码
有人会对这个运行结果产生这种疑问为什么启动之前就已经有一个hello thread,
当我们调用thread.start(),时候thread线程确实启动了,但是main线程此时也在执行,由于线程参与cpu调度是抢占式的,此时thread线程在比main方法执行sout语句之前更快的参与了cpu调度的执行,导致了“好像看起来在启动之前就有一个hello thread”,其实线程在.start(),后已经启动,只不过打印那个“启动之后查看线程是否存活”这句话慢了而已,所以这个问题本身就问的有问题,理解其中为什么即可
四.线程中断
线程中断,首先我们要明白这个属性只是一个标志位
1)方法一:我们使用自定义的中断标志,以下代码有问题
package demo1;
public class demo_07isInterrupt {
public static void main(String[] args) {
boolean isQuit=false;
Thread thread =new Thread(()->{
while (!isQuit) {
System.out.println("线程正在运行。。。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
thread.start();
//修改标志位,让他停止
isQuit=true;
}
}
这是一个设计“变量捕获”的问题
在 Java 中,变量捕获(Variable Capture) 发生在Lambda 表达式或匿名内部类使用其外部作用域中的局部变量时。
即向内部传入外部的局部变量,变量捕获(捕获的是值)直接说final,或者全局变量,为了保证生命周期不同(如外部方法结束,变量被销毁),线程安全(变量线程已经结束,变量随着消失),所以,我们在修改标志位的时候,编译器不通过,此时我们可以使用全局静态变量,就可以解决这个问题
package demo1;
public class demo_07isInterrupt {
static boolean isQuit=false;
public static void main(String[] args) throws InterruptedException {
Thread thread =new Thread(()->{
while (!isQuit) {
System.out.println("线程正在运行。。。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//启动线程
thread.start();
//让thread线程运行一会
Thread.sleep(3000);
//修改标志位,让他停止
isQuit=true;
}
}
2)方法二:更多的时候我们是使用thread内部的标志位,因为他有其中断机制
查看源码中的标志位
我们通过调用线程对象引用,interrupt(),来修改标志位
interrupt()中断他分为两种情况
1.线程运行状态下中断
2.线程阻塞状态下中断,如线程在执行
sleep()
、wait()
、join()
等方法
1)线程运行状态下中断
package demo1;
public class demo_08Interrupt {
public static void main(String[] args) throws InterruptedException {
Thread thread =new Thread(()->{
//只是一个标志位,修改标志位后是否中断,需要自己编译代码的时候符合逻辑
while (!Thread.currentThread().isInterrupted()){
//让线程时刻处于运行状态
System.out.println("线程正在运行");
}
});
//启动线程
thread.start();
//让线程运行一会
Thread.sleep(5);
//调用方法来中断线程
thread.interrupt();
}
}
在调用interrupt()方法后,他将标志位修改为true
注意:中断机制中只是一个标志位,我们使用interrupt方法将他修改为true后,是否中断,需要我们自己编译代码符合中断的逻辑,才会实现中断
2)在阻塞状态下中断
阻塞状态如线程正在执行sleep()
、wait()
、join()
等方法
package demo1;
public class demo_09InterruptBlock {
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();
//保证线程已经启动
Thread.sleep(1000);
//修改中断标志
thread.interrupt();
}
}
发现其并没有中断(代码逻辑上我们想要是中断即是线程结束,但是注意中断并不等于终止)
这是因为,在调用interrupt方法时候,他会检查当前线程的状态,(1)如果是运行状态,则直接修改标志位,
(2)如果是阻塞状态,
1)阻塞状态会被立即终止,并且抛出一个异常,为了快速响应
2)清除修改标志位,即标志还是false
3)执行catch语句中的代码,即要完成中断要在catch代码块中实现逻辑中断
以上就解释了为什么他并没有中断,因为他清除了对标志位的修改,我们重写修改代码实现中断
package demo1;
public class demo_09InterruptBlock {
public static void main(String[] args) throws InterruptedException {
Thread thread =new Thread(()->{
//此时线程大部分时间是在休眠状态
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread。。。。。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("休眠中被中断");
e.printStackTrace();
//代码实现逻辑中断,如果想要让他中断即停止我们可以使用break或者return
break;
}
}
System.out.println("线程已退出");
});
//启动线程
thread.start();
//保证线程已经启动
Thread.sleep(3000);
//修改中断标志
thread.interrupt();
}
}
此时就已经完成了线程中断
此时细心的人会发现一个问题为什么打印报错信息明明在线程已退出前面,打印时候确出现在了后面
即是输出信息和输出错误信息是两个独立的输出通道,我们无法控制,控制台可能会交错显示
总结:中断机制,只是一个标志位,interrupt的时候,他也只是修改标志位,具体是否中断,又或者让他中断即是线程结束,这些都是我们通过自己写的代码逻辑完成的,注意中断并不意味着线程终止,只是我们在演示的时候,让他逻辑上中断就是终止
调用interrupt方法时候
1.会检测此时线程的状态
1)如果是运行状态,即会直接修改标志位
2)如果是阻塞状态,
即会立刻终止阻塞状态,并且抛出一个异常,为了快速响应
清除对标志位的修改,即标志还是false
执行catch语句中的代码,即要完成中断要在catch代码块中实现逻辑中断
以上中断只是对标志位不同的线程状态下不同的处理方法,是否中断,都是根据自己写的代码逻辑来完成中断,或是不中断
五.一些线程对象方法
等待一个线程执行完毕.join()方法,join()等到结束,join(long time)最多等time毫秒,join(long time,int nanos)最多等待time+nanose秒
当我们需要调用线程对象的方法的时候,我们可以通过Thread.currentThread()的静态方法来获取,当前正在执行的线程对象的引用