02.Thread类的使用,线程类内部API详解

1.Thread提供的构造方法

Thread类为我们提供了比较丰富的构造函数。
在这里插入图片描述

2.线程的命名

上述构造方法中有几个构造方法并没有要求传线程名

在这里插入图片描述

观察源码可知
在这里插入图片描述

在没有显示设置线程名的话,线程会以 “Thread-” 的前缀和一个自增数字进行组合,形如:Thread-1,Thread-2…

除了通过构造方法显示的对线程初始化话名称外,还能通过setName(String)对线程进行名称修改,注意只能在线程启动前进行名称的修改,一单启动了,线程名将不可修改:
在这里插入图片描述

3.线程的父子关系

细看Thread的构造方法,可以看到最终都会调用一个静态方法init,在方法内我们可以看到每一个新创建的线程都会有一个父线程

在这里插入图片描述

currentThread()为获取当前线程的方法,在这里获取的将是执行新线程构造方法的那个旧有线程。观察方法中的代码,不难发现:

1.一个线程是有另一个线程创建的。2.被创建的线程的父线程就是创建它的那个线程。

main函数所在的main线程是由JVM创建的,我们在main方法中创建的线程,其父线程都是main线程。

父子线程中的ThreadGroup关系:

在线程的构造方法中,Thread(ThreadGroup group, Runnable target) 可以显式的指定线程的ThreadGroup(线程组),若没有指定呢?线程会加入到父线程所在的线程组中。

父子线程中的守护性继承关系:

守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。

父进程是守护线程子进程也是,反之亦然。当然子线程可以通过setDaemon(boolean)来更改自己的守护线程性质。

4.线程属性相关方法总结

以下是关系到线程属性的几个方法:

1)getId

用来得到线程ID

2)getName和setName

用来得到或者设置线程名称。

3)getPriority和setPriority

用来获取和设置线程优先级。

4)setDaemon和isDaemon

用来设置线程是否成为守护线程和判断线程是否是守护线程。

5)currentThread

Thread类有一个比较常用的静态方法currentThread()用来获取当前线程。

4.线程API的详细介绍

1)start方法

​ start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。一个线程实例在没有调用start方法前,它就不能算做一个线程,只是一个普通的Thread类实例。

2)run方法

​ run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

3)sleep方法

sleep是一个静态方法,有2个重载方法

  1.public static native void sleep(long millis) throws InterruptedException; //参数为毫秒
  
  2.public static void sleep(long millis, int nanos) throws InterruptedException   //第一参数为毫秒,第二个参数为纳秒`

sleep会让当前线程进入指定毫秒数的休眠,交出CPU,让CPU去执行其他的任务。

sleep进入休眠有一个非常重要的特性,那就是不会放弃monitor锁的所有权 ,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

使用TimeUnit代替Thread.sleep:

JDK1.5之后,JDK引入了一个枚举 TimeUnit,其对sleep方法进行了一次封装,它可以让sleep限定的睡眠时间,变的非常直观

//让线程睡眠1小时18分26秒又7毫秒
TimeUnit.HOURS.sleep(1);
TimeUnit.MINUTES.sleep(18);
TimeUnit.SECONDS.sleep(26);
TimeUnit.MILLISECONDS.sleep(7);

4)yield方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。yield方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前CPU资源,但是CPU并非一定执行,当CPU资源不紧张时,会忽略这种提醒。所以yield不能控制具体的交出CPU的时间,只能让拥有相同优先级的线程有获取CPU执行时间的机会。

注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

sleep与yield方法的区别:

​ 1.sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗

​ 2.yield知识对CPU调度的一个提示,若CPU没忽略该提示会导致线程上下文的切换

​ 3.sleep会使线程短暂block,会在给定的时间内释放CPU资源

​ 4.yield会使RUNNINGz状态的Thread进入RUNNABLE状态

​ 5.sleep肯定能完成指定时间的休眠,而yield的提示并不能一定保证

​ 6.一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield不会

5)interrupt方法

interrupt,顾名思义,即中断的意思。我们经常使用到的 与线程中断相关的方法有下面3个

1.public void interrupt() {}

2.public static boolean interrupted() 

3.public boolean isInterrupted() 

1.interrupt():

如下方法的调用会使得当前线程进入阻塞状态,而调用当前线程的interrupt方法。可以打断阻塞。

在这里插入图片描述

上述方法会让当前线程进入阻塞状态,若另一个线程调用被阻塞线程的interrupt方法,则会打断这种阻塞,因此这种方法有时会被称为可中断方法,注意打断一个线程并不等于该线程的生命周期结束,,仅仅是打断了线程当前的阻塞状态。

一旦一个线程在阻塞状态下被打断,都会抛出一个称为InterruptedException的异常,这个异常就像一个信号一样通知当前线程被打断了。

Thread t = new Thread(()->{
    
    
    try{
    
    
        System.out.println("我开始睡了");
        TimeUnit.MINUTES.sleep(1);
        System.out.println("我睡了一分钟");
    }catch (InterruptedException e){
    
    
        System.out.println("我被打断了:"+e.getMessage());
    }
});
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt();

//运行结果
我开始睡了
我被打断了:sleep interrupted

上面代码创建了一个线程,并准备休眠1分钟,然而,在两秒后被主线程调用该线程的interrupt方法,然后休眠就被打断了。

ininterrupt方法到底做了什么?在线程里存在着一个名为interrupt flag的标识,如一个线程被interrupt,那么这个flag将被设置,但如果当前线程正在执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除。注意一个线程如果已经死亡状态,那么尝试对其interrupt会被直接忽略。

2.isInterrupted

isInterrupted是Thread的一个成员方法,它主要判断当前线程是否被中断,方法仅仅是对interrupt标识的一个判断,并不会影响标识的变化。

3.interrupted

interrupted是一个静态方法,虽然其也是用于判断当前线程是否被中断,但是他和成员方法isInterrupted还是有很大的区别,该调用方法会直接擦除掉线程的interrupt标识,注意的是,如果当前线程被打断了,那么第一次调用interrupted方法会返回false,并且立即擦除了interrupt标识,第二次包括以后的调用都会返回false,除非在此线程又一次被打断。

public static void main(String args[]) throws Exception{
    
    
    Thread t = new Thread(()->{
    
    
        System.out.println(Thread.interrupted());
        //中断
        Thread.currentThread().interrupt();
        System.out.println(Thread.interrupted());
        System.out.println(Thread.interrupted());
    });
    t.start();
}

//运行结果
false
true
false

6)join方法

join方法有三个重载版本:

1.public final void join() throws InterruptedException

2.public final synchronized void join(long millis)throws InterruptedException //参数为毫秒 

3.public final synchronized void join(long millis, int nanos)throws InterruptedException  //第一参数为毫秒,第二个参数为纳秒 

join某个线程A,会使当前线程B进入等等,直到线程A结束生命周期,或者达到了给定的时间,在此期间B线程处于BLOCKED的,而不是A线程。

public class Try {
    
    
    public static void main(String args[]) throws Exception{
    
    
        //1.定义两个线程并启动
        List<Thread> threadList = IntStream.range(1,3).mapToObj(Try::create).collect(Collectors.toList());
        threadList.forEach(Thread::start);
        //2.在main线程中调用这两线程的join方法
        for (Thread t: threadList) {
    
    
            t.join();
        }
        //3.main线程循环输出
        for (int i = 0; i < 3; i++) {
    
    
            System.out.println(Thread.currentThread().getName()+">>>"+i);
            try{
    
    
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
    
    
            }
        }
    }
}

在上面代码中,创建了两个线程后,在main线程里循环调用了join方法,所以main线程会阻塞,直到两线程运行结束。输出结果会如下,两个线程交替输出结束后main线程才开始输出:

在这里插入图片描述

如果主线程需要等到子线程的运行结束再进行某些操作,就可以运用join实现这一场景。

7)线程上下文类加载器相关方法

1.public ClassLoader getContextClassLoader()

getContextClassLoader()获取线程上下文的类加载器,简而言之通过这个方法能知道线程是由哪个类加载器加载的,如果是在没有修改线程上下文类加载器的情况下,则保持父线程同样的类加载器。

2.public void setContextClassLoader(ClassLoader cl)

设置该线程的类加载器,这个方法可以打破JAVA类加载器的父委托机制,有时候这个方法也被称为JAVA类加载器的后门。

8)stop方法(已废弃) 和 destroy(已废弃)方法

stop方法已经是一个废弃的方法,它是一个不安全的方法。因为调用stop方法会直接终止run方法的调用,并且会抛出一个ThreadDeath错误,如果线程持有某个对象锁的话,会完全释放锁,导致对象状态不一致。所以stop方法基本是不会被用到的。

猜你喜欢

转载自blog.csdn.net/weixin_43828467/article/details/110482618