跳到主要内容

02、Java多线程:Thread类核心API介绍

设置线程优先级

Java提供了一个线程调度器来监控程序启动后进去就绪状态的所有线程。线程调度器通过线程的优先级来决定调度哪些线程执行。一般来说,Java的线程调度器采用时间片轮转算法使多个线程轮转获得CPU的时间片。

线程可以划分优先级,优先级高的线程得到的CPU资源比较多,也就是CPU优先执行优先级高的线程对象中的任务。优先级具有随机性,不一定优先级高的有限制性。

优先级说明

  • 当线程的优先级没有指定时,所有线程都携带普通优先级。
  • 优先级可以用从1到10的范围指定。10表示最高优先级,1表示最低优先级,5是普通优先级。
  • 优先级最高的线程在执行时被给予优先。但是不能保证线程在启动时就进入运行状态。
  • 与在线程池中等待运行机会的线程相比,当前正在运行的线程可能总是拥有更高的优先级。
  • 由调度程序决定哪一个线程被执行。
  • t.setPriority()用来设定线程的优先级。
  • 在线程开始方法被调用之前,线程的优先级应该被设定。
  • 可以使用常量,如MIN_PRIORITY,MAX_PRIORITY,NORM_PRIORITY来设定优先级。

Thread类优先级源码

// 优先级
private int  priority;
// 获取优先级
public final int getPriority() {

 
    return priority;
}
// 设置优先级,java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
public final void setPriority(int newPriority) {

 
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {

 
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {

 
        if (newPriority > g.getMaxPriority()) {

 
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}
/**
 * 线程可以具有的最低优先级
 */
public final static int MIN_PRIORITY = 1;

/**
 * 分配给线程的默认优先级。
 */
public final static int NORM_PRIORITY = 5;

/**
 * 线程可以具有的最大优先级。
 */
public final static int MAX_PRIORITY = 10;

设置优先级案例

public class Test001 {
   
     
    public static void main(String[] args) {
   
     
        MyThread001 thread001 = new MyThread001();
        // 设置优先级最高
        thread001.setPriority(Thread.MAX_PRIORITY);
        thread001.start();
        // 设置优先级最低
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        // 打印优先级
        System.out.println("thread001优先级:"+thread001.getPriority());
        System.out.println("Main 优先级:"+Thread.currentThread().getPriority());
        System.out.println(Thread.currentThread().getName() + "线程正在运行任务");
    }
}

设置守护线程

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。

用户线程:我们平常创建的普通线程。
守护线程:守护线程是一类比较特殊的线程,一般用于处理一些后台的工作,随着用户线程的销毁,守护线程也会随着销毁,比如JDK的垃圾回收线程。

使用场景

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

Java垃圾回收线程就是一个典型的守护线程,因为我们的垃圾回收是一个一直需要运行的机制,但是当没有用户线程的时候,也就不需要垃圾回收线程了,守护线程刚好满足这样的需求。

场景:某个用户线程在执行时,需要一个定时无线循环线程,去检测心跳,一旦用户线程结束,这个检测线程也需要关闭。如果不设置守护线程,那个这个检测线程将无法停止,此时可以这只设置这个线程为守护线程,随着业务线程的完成而自动退出。

使用案例

public class DaemonThreadTest001 {
   
     
    public static void main(String[] args) {
   
     
        Thread t = new Thread(new Runnable() {
   
     
            @Override
            public void run() {
   
     
                while (true) {
   
     
                    System.out.println(Thread.currentThread().getName() + "正在检测心跳");
                    try {
   
     
                        Thread.sleep( 3_000);
                    } catch (InterruptedException e) {
   
     
                        e.printStackTrace();
                    }
                }
            }
        },"检测心跳任务线程");
        // 设置为守护线程
        t.setDaemon(true);
        // 查看线程是否为守护线程
        System.out.println(t.getName()+"是否守护线程"+t.isDaemon());
        System.out.println(Thread.currentThread().getName()+"是否守护线程"+Thread.currentThread().isDaemon());
        t.start();
        try {
   
     
            Thread.sleep( 20_000);
        } catch (InterruptedException e) {
   
     
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "线程任务运行结束");
    }
}

注意事项

1、 设置守护线程的方法很简单,调用setDaemon方法即可,true代表守护线程,false代表正常线程;
2、 线程是否为守护线程和它的父线程有很大的关系,如果父线程是正常线程,则子线程也是正常线程,反之亦然,如果你想要修改它的特性则可以借助setDaemon方法isDaemon方法可以判断该线程是不是守护线程;
3、 setDaemon(true)必须在t.start()之前设置,否则会抛出IllegalThreadStateException异常;

获取线程ID

public long getId()获取线程的唯一ID,线程的ID在整个JVM进程中都会是唯一的。并且是从0开始逐次递增。在一个JVM进程启动的时候,实际上是开辟了很多个线程,自增序列已经有了一定的消耗,因此我们自己创建的线程可能并不是第0号线程。

  // 获取线程长整型的 id,id 唯一
  long id = t.getId();

获取当前线程

public static Thread currentThread()用于返回当前执行线程的引用。

  // 获取当前正在执行的线程
  Thread thread = Thread.currentThread();

设置线程休眠

sleep方法方法会使当前线程进人指定毫秒数的休眠,暂停执行,虽然给定了一个休眠的时间,但是最终要以系统的定时器和调度器的精度为准,休眠有一个非常重要的特性,那就是其不会放弃监视器锁的所有权。

sleep是一个静态方法,其有两个重载方法,其中一个需要传入毫秒数,另外一个既需要毫秒数也需要纳秒数。

在JDK1.5以后,JDK引入了一个枚举TimeUnit,其对sleep方法提供了很好的封装,使用它可以省去时间单位的换算步骤。

public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos) throws InterruptedException

案例

// 休眠3秒
Thread.sleep(3_000);
TimeUnit.SECONDS.sleep(3);

线程yield

yield方法属于一种启发式的方法,其会提醒调度器我愿意放弃当前的CPU资源,如果CPU的资源不紧张,则会忽略这种提醒。调用yield方法会使当前线程从RUNNING状态切换到RUNNABLE状态,一般这个方法不太常用。主要是为了测试和调试。

public static native void yield();

yield和sleep的区别

1、 sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗;
2、 yield只是对CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换;
3、 sleep会使线程短暂block,会在给定的时间内释放CPU资源;
4、 yield会使RUNNING状态的Thread进入RUNNABLE状态(如果CPU调度器没有忽略这个提示的话);
5、 sleep几乎百分之百地完成了给定时间的休眠,而yield的提示并不能一定担保;
6、 一个线程sleep另一个线程调用interrupt会捕获到中断信号,而yield则不会;

设置线程上下文类加载器

@CallerSensitive
public ClassLoader getContextClassLoader() {

 
    if (contextClassLoader == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {

 
        ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                               Reflection.getCallerClass());
    }
    return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {

 
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {

 
        sm.checkPermission(new RuntimePermission("setContextClassLoader"));
    }
    contextClassLoader = cl;
}

public ClassLoader getContextClassLoader()获取线程上下文的类加载器,简单来说就是这个线程是由哪个类加器加载的,如果是在没有修改线程上下文类加载器的情况下,则保持与父线程同样的类加载器。

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

线程中断

取消一个任务的执行,最好的,同时也是最合理的方法,就是通过中断。

// 打断线程
public void interrupt() {

 
    if (this != Thread.currentThread())
        checkAccess();
    synchronized (blockerLock) {

 
        Interruptible b = blocker;
        if (b != null) {

 
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}
// 判断当前线程是否被打断
public static boolean interrupted() {

 
    return currentThread().isInterrupted(true);
}
// 测试某些线程是否已被中断
public boolean isInterrupted() {

 
    return isInterrupted(false);
}
private native boolean isInterrupted(boolean ClearInterrupted);

interrupt()

调用sleep或wait方法时,会使得当前线程进入阻塞状态,而调用当前线程的interrupt方法,就可以打断阻塞。打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。一旦线程在阻塞的情况下被打断,都会抛出一个称为InterruptedException的异常。

中断标识

在一个线程内部存在着名为interrupt flag的标识,如果一个线程被interrupt,那么它的flag将被设置,但是如果当前线程正在执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除。

InterruptedException

wait方和sleep方法前线程被中断了会抛出InterruptedException,并且清除中断状态。但是并不会终止当前线程的执行,当前线程可以选择忽略这个异常。

无论是设置interrupt status 还是抛出InterruptedException,它们都是给当前线程的建议,当前线程可以选择采纳或者不采纳,它们并不会影响当前线程的执行。

至于在收到这些中断的建议后,当前线程要怎么处理,也完全取决于当前线程。

测试案例

public class InterruptThreadTest001 {
   
     
    public static void main(String[] args) {
   
     
        Thread t = new Thread(new Runnable() {
   
     
            @Override
            public void run() {
   
     
                while (true) {
   
     
                    System.out.println(Thread.currentThread().getName() + "正在检测心跳");
                    System.out.println("中断状态" + Thread.interrupted());
                    try {
   
     
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
   
     
                        // 被中断时 退出循环
                        e.printStackTrace();
                        break;
                    }
                }
            }
        }, "检测心跳任务线程");
        // 获取当前线程
        t.start();
        try {
   
     
            Thread.sleep(20_000);
        } catch (InterruptedException e) {
   
     
            e.printStackTrace();
        }
        // 中断
        t.interrupt();
        System.out.println(Thread.currentThread().getName() + "线程任务运行结束");
    }
}

线程join

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

public final void join() throws InterruptedException
public final synchronized void join(long millis, int nanos)throws InterruptedException
public final synchronized void join(long millis)throws InterruptedException

测试案例

线程t2先启动,3秒后才启动t1,但是由于t2使用了t1.jion(),则会等待t1执行完毕后,t2才会执行后续代码。

public class JoinThreadTest001 {
   
     
    public static void main(String[] args) {
   
     
        Thread t1 = new Thread(() -> {
   
     
            System.out.println(Thread.currentThread().getName() + "正在检测心跳");
        }, "线程001");

        Thread t2 = new Thread(() -> {
   
     
            try {
   
     
                t1.join();
            } catch (InterruptedException e) {
   
     
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在检测心跳");
        }, "线程002");
        t2.start();
        try {
   
     
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
   
     
            e.printStackTrace();
        }
        t1.start();
    }
}

其他API

start()

启动一个新线程,在新的线程运行 run 方法中的代码。

start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException。

run()

新线程启动后会调用的方法。

如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。

getName()

获取线程名。

setName(String)

修改线程名

getState()

获取线程状态

isAlive()

线程是否存活(还没有运行完毕)

过时方法

这些方法已过时,容易破坏同步代码块,造成线程死锁,不推荐使用。

stop()

停止线程运行

suspend()

挂起(暂停)线程运行

resume()

恢复线程运行