java线程之Thread类的基本用法

小鱼在上一篇博客详细的讲解了如何创建线程,java使用Thread类来创建多线程,但是对于好多没有相关经验的人来说,比较不容易理解的地方在于操作系统调度的执行过程. 我们通过下面代码举例:

    public static void main(String[] args) {
    
    
        Thread t = new Thread(() -> {
    
    
            System.out.println("hello t");
        });
        //调用start(),创建一个新的线程
        t.start();
 
        System.out.println("hello main");
    }

我们的java在启动时,会创建一个主线程,此时主线程的名字就是main,接下来我们执行main线程里面的一条条代码了,当我们执行到t.start()时,系统就会自动帮我们创建一个新的线程,并且会自动帮我们调用该线程中的run()方法,此时的run()方法就可以认为是个入口,一些重要的实现逻辑将都在里面实现.

我们通过上述代码举例:

当我们实例一个thread并且调用start()方法之后, main 线程和 t 线程就是(并发+并行)的过程,由于多线程有一个万恶之源–抢占机制,所以就会导致我们无法预测到程序是优先执行"hello t" 还是 “hello main”,并且对于谁先结束线程的任务,我们也是不确定的,可能是main线程优先结束,也可能是t线程优先结束.

在这里插入图片描述

创建线程是看start的顺序,但是具体的线程对应的任务什么时候执行,要看系统调度器。

1. Thread类的构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象并命名
Thread(Runnable target,String name) 使用Runnable对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target) 线程可以被用来分组管理,分好的组即为线程组

关于构造方法的讲解在这篇文章中详细的讲到了,包括如何创建一个线程,以及创建线程需要注意的事项都在这里一一列举了出来多线程的创建及其注意事项

2. Thread的几个常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

常见属性

(1) getid() : ID表示线程的身份.就类似于我们的身份证一样,是独一无二的.这里的方法获取到的是线程在JVM中的身份标识,线程的标识有好多个,在内核的PCb上有标识,在用户态线程数据库中也有标识(操作系统的线程库)

(2) getName() : 获取当前线程的名字,多用于调试时使用.

(3) getState() :获取当前线程的状态,状态表示线程当前所属的一个情况.

(4) getPriority() : 优先级高的线程理论上来说更容易被调度到,但实际上并没有什么区别.大概可以理解为,你建议我不要晚睡,但也仅仅是建议,我可以不听.

(5) isDaemon() :daemon称为"守护线程",也可以称为后台线程,如果是后台线程就会返回true,前台线程则是false,我们可以这么理解后台线程,当我们手机打开qq这个app时,此时qq就是前台运行,但是由于我们后续可能会切换到快手app,此时qq就来到后台运行了.

一个线程创建出现默认是前台线程,前台线程会阻止进程的退出,进程只有在所有的前台线程都执行完之后才可以退出,但是对于后台进程,并不会阻止进程的结束.

JVM会在一个进程的所有非后台进程结束后,才会结束运行。

(6)isAlive() : 是否存活,即run()方法是否结束。

public class Demo4 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("t 线程正在运行");
            }
        });
        System.out.println("t线程是否存活 "+t.isAlive());
        t.start();
        System.out.println("t线程是否存活 "+t.isAlive());

        try {
    
    
            Thread.sleep(1000);
            System.out.println("t线程是否存活 "+t.isAlive());
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

    }
}

如果 t 的run还没跑,isAlive就为false;
如果 t 的run正在跑,isAlive就为true;
如果 t 的run跑完了,isAlive就为false;

执行结果:

在这里插入图片描述

此时我们将上述涉及到的属性的状态打印出来:

在这里插入图片描述

使用setDaemon(布尔值)方法设置一个线程为后台,true表示后台线程。

class MyThread1 extends Thread{
    
    
    @Override
    public void run() {
    
    
        System.out.println("hello  t");
    }
}
public class Demo1 {
    
    
    public static void main(String[] args) {
    
    
        Thread t = new MyThread1();
        t.start();

        System.out.println("hello main");
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        //设置为后台线程
        t.setDaemon(true);
        System.out.println("是否是后台线程 "+ t.isDaemon());
    }
}

执行结果:

在这里插入图片描述
上述操作都是获取到的一瞬间的状态,不是持续的状态。

start()和run()方法的区别:

当我们调用start()这个方法时,创建一个新的线程(有一个向系统创建一个线程的这么一个动作),此时这个线程会由系统自动调用run()方法(入口方法).并且这个run()方法是在这个新建的线程中运行的.
但是如果是程序员手动调用run()方法的话,并不会新建一个线程,并且该方法是在调用这个方法的线程中执行的.

获取当前线程的引用:

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用
 public static void main(String[] args) {
    
    
 		//获取当前线程的引用
        Thread t = Thread.currentThread();
        //通过当前线程的引用调用当前线程的名字
        System.out.println("当前线程名字 : "+t.getName());
    }

执行结果:
在这里插入图片描述

休眠当前线程:

方法 说明
public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throwsInterruptedException 可以更高精度的休眠
   public static void main(String[] args) {
    
    
        Thread t = new Thread(()->{
    
    
            System.out.println("hello t");
        });
        t.start();
        try {
    
    
            //睡眠1000ms,也可以称为阻塞1000ms
             Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("hello main");
    }

睡眠的状态就相当于这个线程被阻塞了,阻塞的时间由里面的参数决定,在这个线程阻塞的时间并不影响其他线程执行,可以理解为~我们在打游戏,打着打着未成年时间限制弹了出来,这时候就要求我们停止打游戏这个行为,只有等这个等待时间过去之后才可以继续打游戏.

:等待的过程中什么都不能做,相当于被定身了一样!!!

线程中断

中断线程,就是让线程尽快的把入口方法(run方法)执行结束。有2种方法。

(1) 直接手动创建标志位来区分线程是否要结束.

public class ThreadDemo5 {
    
    
    static boolean flog = true;
    public static void main(String[] args)  {
    
    
        Thread t = new Thread(()->{
    
    
           while (flog){
    
    
               try {
    
    
                   Thread.sleep(500);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
               }
               System.out.println("线程运行中~~");
           }
            System.out.println("t 线程结束");
        });
        t.start();
        try {
    
    
            //睡眠3000ms
            Thread.sleep(3000);
            //将标志位设置为false终止上面的while()
            System.out.println("控制新线程结束");
            flog = false;
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

运行结果:

在这里插入图片描述
有些线程可能会存在一些循环需要执行的情况,但是可能只是需要执行一定时间,并不是一直执行,所以就可以通过sleep()来控制这个线程结束的时间.

需要注意的是:flog需要是成员变量,局部变量是线程私有的,其余线程不能进行修改,但是成员变量是处于进程的数据区,是共享的资源,所以可以访问和修改~~

Thread内置了标志位,不需要手动创建标志位。

(2)Thread内置的标志位

public class ThreadDemo5 {
    
    
    static boolean flog = true;
    public static void main(String[] args)  {
    
    
        Thread t = new Thread(()->{
    
    
           while (!Thread.currentThread().isInterrupted()){
    
    
               try {
    
    
                   Thread.sleep(500);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
               }
               System.out.println("线程运行中~~");
           }
            System.out.println("t 线程结束");
        });
        t.start();
        try {
    
    
            //睡眠3000ms
            Thread.sleep(3000);
            //将标志位设置为false终止上面的while()
            System.out.println("控制新线程结束");
           t.interrupt();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
Thread.currentThread().这个是Thread类的静态方法,通过这个方法可以获取当前线程,类似this;

isInterrupted()方法是一个判定内置的标志位,默认是false,true表示线程要中断。现在运行,查看结果。

在这里插入图片描述

为什么在抛出异常后线程还是在不停的执行呢?

那是因为当代码执行到interrupt()时会做两件事:

(1) 如果t 线程没有处于阻塞状态时,这时候interrupt()就会修改内置标志位,将isInterrupted的状态设置为true;

(2) 如果 t 线程处于阻塞状态时,此时interrupt()会通过触发异常,将阻塞状态(sleep)提前唤醒,但是由于把sleep唤醒会导致标志位变成false,也就是将标志位清空,如果标志位本身就是false则不变.由于我们的循环条件是while(!Thread.currentThread().isInterrupted()),又因为我们的标志位又变成了false,所以我们可以继续执行这个循环.

实际中能产生阻塞的方法有很多,并不是只有sleep,这里只是用sleep举例子.

由于计算机的执行速度时很快的,所以阻塞状态大概占线程执行状态的99.99%,所以会有更大的几率触发(2).

当然,即使是大部分时间都在阻塞状态,但是该线程始终是在阻塞状态和正常运行的状态之间切换.

当然,我们可以通过break关键字,当抛出异常时直接break就可以不再执行循环,之后就可以结束线程,这就相当于我正在打游戏,我妈喊我吃饭,此时我不敢抗拒,只能去吃饭.

代码如下:

public class ThreadDemo5 {
    
    
    public static void main(String[] args)  {
    
    
        Thread t = new Thread(()->{
    
    
           while (!Thread.currentThread().isInterrupted()){
    
    
               try {
    
    
                   Thread.sleep(500);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
                    break;
               }
               System.out.println("线程运行中~~");
           }
            System.out.println("t 线程结束");
        });
        t.start();
        try {
    
    
            //睡眠3000ms
            Thread.sleep(3000);
            //将标志位设置为false终止上面的while()
            System.out.println("控制新线程结束");
           t.interrupt();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

此时的运行结果:

在这里插入图片描述
当然,由于我们捕捉到了异常,我们不仅可以直接让他break,还可以让他先等待一会再结束执行.

就像我们在打电话一样:

比如我正在打游戏,女朋友让我出去买菜,此时我不搭理她,这个时候就相当于,你说你的我干我的.

但是呢?过了一会女朋友又来跟我说让我买菜去,我因为怕她生气嘛,所以我跟她说,五分钟,五分钟就好…等五分钟一到,我就退出游戏买菜去了.

当我买完菜的时候,刚躺到床上打开游戏,此时厨房传来女朋友的尖叫声,那我必须立刻放下手机去看看发生了什么事情…

总结三种状态;(1)立即退出 (2) 等待一会再退出 (3) 不退出,你说你的我做我的.

由我们自己来决定什么时候退出.

如果是直接写死的话,让我收到命令就必须退出,假如我在跟老板汇报工作,此时女朋友让我去买菜,如果我把老板电话挂掉,那自己也就寄了.

只有自己才有资格决定自己要做什么事情,别人的话之能当作意见,只有自己是为自己好.

等待一个线程

线程是一个随机调度过程,等待线程,就是控制两个线程的结束顺序.

但是呢?我们可以通过过join()方法来控制线程的执行顺序.

大家看下面的代码以及执行结果:

在这里插入图片描述
由于线程是抢占式执行的,所以可能这次是先打印 main 但是下次可能就先打印t了,可是程序员不喜欢这种不确定性,所以我们可以通过join()方法来控制顺序.

大家看下面的代码和执行结果:

在这里插入图片描述
当我们在 main 线程中调用 t.join() ,此时只有当 t 线程执行完之后,才会去执行 main 线程中的内容,我们也可以理解为,在哪个线程中调用join()方法,那么就哪个线程阻塞,只有当调用这个方法的引用所对应的线程执行完之后,才可以继续执行后续代码~

在这里插入图片描述
当然,这个join()也是可以传参数的,假如 t 线程一直没有执行完,那么我这个线程就不工作了? 就像我约女神见面一样,我等了一天,两天,三天都没等到,难道我还要等一辈子? 所以此时我们就可以传一个参数,作为最大等待时间,超过这个时间我就不等了,世界上美女那么多,我换个人舔还不行?

join方法自身也会阻塞,Java线程但凡是阻塞的方法都可能抛出这个异常(throws InterruptedException)。

join有两种行为:

(1)如果被等待的线程还没有执行结束,那么直接阻塞等待;

例如:我接我孩子放学,他5.00放学,我4.50到了,此时我就需要等待他放学,菜才能回家.

(2)如果被等待的线程已经执行结束了,就直接返回。

例如:我接我孩子放学,他5.00放学,我5.10才到,那么我就不用等,可以直接接回家.

join方法不带参数的时候,就是死等,一直等待下去。方法带参数的时候,如果超过了等待的最大时间,就开始执行下一步的代码。一般写带参数的join方法。

猜你喜欢

转载自blog.csdn.net/xiaoyubuhuiqiche/article/details/129655077