java多线程系列 ---- 第三篇Thread API

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u013513053/article/details/99692589

线程sleep

sleep是一个静态方法,它有两个重载方法。第一个需要传入休眠的毫秒数,第二个需要传入休眠的毫秒数和纳秒数。主要使用第一个就够了,第二个也是调用了第一个的方法,最后休眠的时间还是按照毫秒数进行的计算。

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

sleep方法根据指定的毫秒数休眠,但是也是交由底层实现,它的精度以系统定时器和调度器的精准度为准。sleep调用休眠不会放弃monitor锁的所有权,关于monitor锁后面再做介绍。
sleep休眠会作用于前线程,每个线程间互不影响。

除了sleep外,我们还可以使用TimeUnit,这个工具类对于 时间处理做了很好地换算封装,同时也把sleep方法封装进去。如果我们想要使线程休眠2小时30分钟20秒100毫秒,使用TimeUnit非常容易省去了换算。

TimeUnit.HOURS.sleep(2);
TimeUnit.MINUTES.sleep(30);
TimeUnit.SECONDS.sleep(20);
TimeUnit.MILLISECONDS.sleep(100);

我在前面的例子中使用的都是TimeUnit,非常的方便。

线程 yield

yield方法也是一个静态方法,直接使用 Thread.yield() 调用即可。它是一个无参无返回值的方法,也没有异常处理。

public static native void yield();

yield方法的作用是礼让,意思就是如果有多条线程在运行,当前线程调用了yield表明告诉调度器我愿意放弃当前的CPU,让其他线程先来。当然这种事也看CPU,如果CPU并不紧张,CPU会表示我们正常来就行了,不用切换。
调用yield并不会进入block,而是重新排队,只是放弃了CPU资源而已,这个方法也不是很常用。

与sleep的关系

  • sleep会导致当前线程暂停指定时间,没有CPU的消耗
  • yield只会对CPU调度器一个提示,如果CPU没有忽略这个提示,会导致线程上下文切换
  • sleep会使线程进入block,在给定的时间内释放CPU资源
  • yield如果CPU没有忽略消息的话,当前线程进入Runnable状态
  • sleep百分百休眠,yield不一定会切换
  • sleep能够捕获到interrupt中断信号,yield不会

线程优先级

优先级的介绍

关于线程优先级有两个方法,一个set,一个get

public final void setPriority(int newPriority)

public final int getPriority()

请注意,优先级并不会让线程先执行,只是让线程调度得到更多的机会。然而这种得到更多调度机会也不一定能更快,这取决于CPU。
理想状态下,优先级高的线程会得到更多的调度机会,举个例子:正常四条线程 1234 会按照1234 1234 1234 这样的顺序被调度,但是如果设置了线程1的优先级高的话,那么可能是安装12134 12314这样的顺序,线程1得到了更多的调度机会。
如果CPU比较慢,优先级可能会获得更多的的CPU时间片。在CPU闲时几乎没什么作用。所以不要在程序设计中依赖优先级,可能会让你失望。
优先级使用的例子

 public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (true){
                System.out.println("t1");
            }
        });
        t1.setPriority(2);

        Thread t2 = new Thread(()->{
            while (true){
                System.out.println("t2");
            }
        });
        t2.setPriority(10);

        t1.start();
        t2.start();

    }

这里设置t2优先级高一些,在打印中t2 出现的频率也是高一些。当然每次运行的结果都会不一样,取决于CPU的调度。

优先级的源码分析

我们看一下setPriority方法源码

public final static int MIN_PRIORITY = 1;

public final static int NORM_PRIORITY = 5;

 public final static int MAX_PRIORITY = 10;

//省略中间代码···

 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);
        }
    }

通过源码我们可以看到,线程的优先级最小为1 最大为 10。如果超出范围则会报IllegalArgumentException异常。
如果是设置的值大于线程所在group的优先级,也是设置失败 ,使用当前group的最高优先级。这里优先级与ThreadGroup也是有很大关系。
默认的优先级为5,因为main线程的优先级就是5。main线程创建的线程优先级默认就是5.

获取线程id

获取线程id的方法

public long getId()

线程的ID在JVM中是唯一的,并且是从0开始逐次递增的。当在main中创建了一个线程,你会发现id并不是从0开始的,这是为啥?JVM在启动的时候,已经开辟了很多个线程,自增序列已经使用了,所以自己创建的线程肯定不是从0开始的

获取当前线程

获取前线程的方法,这个方法是一个静态方法。方法比较简单,但是使用也是非常的频繁。

public static native Thread currentThread();

设置线程上下文类加载器

类加载器涉及到两个方法

public ClassLoader getContextClassLoader() 
public void setContextClassLoader(ClassLoader cl)

getContextClassLoader() :获取线程上下文的类加载器,简单来说就是这个线程是由哪个类加载器加载的,如果没有修改,那么这个类加载器与父线程是同样的类加载器。

setContextClassLoader() :java类加载器后门,可以设置线程的类加载器,这个方法可以打破java类加载器的父委托机制。

关于线程上下文类加载器在之后再进行详细说明

线程interrupt

这是一个非常重要的API,也是经常使用的一个方法。相关的API有

public void interrupt()
public static boolean interrupted()
public boolean isInterrupted()

interrupt

当线程进入阻塞时,调用阻塞线程的interrupt方法可以打断阻塞。

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("be interrupt");
            }
        });
        thread.start();

        TimeUnit.SECONDS.sleep(2);

        thread.interrupt();

    }

执行结果
在这里插入图片描述
上面开启了一个线程,并且预计休眠1分钟。不过在两秒后,我们在主线程调用子线程的interrupt方法,子线程的阻塞被打断,执行输出 “be interrupt” 。
interrupt的实现方式是怎样的呢?在线程内部有一个interrupt flag标识,如果一个线程被阻塞,那么flag标识被设置。当线程正在阻塞,调用interrupt 方法将其中断,flag标识会被清除。这一点后面再做详细介绍。
如果一个线程已经是死亡状态的话,调用interrupt方法会被直接忽略。

isInterrupt

isInterrupt是Thread的一个成员方法,用作判断当前线程是否被中断。该方法仅仅是对interrupt标识的一个判断,不会影响标识发生改变。

 public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            while (true){

            }
        });
        thread.setDaemon(true);
        thread.start();
        System.out.printf("Thread is interrupt %s\n",thread.isInterrupted());
        thread.interrupt();
        System.out.printf("Thread is interrupt %s\n",thread.isInterrupted());
    }

结果:
在这里插入图片描述
我们开启了一个线程,并且在线程内有一个死循环。给线程设置为守护线程,线程运行结束会自动停止。默认的标识是false,当执行interrupt之后,标识变为true。如果在线程中写sleep方法的话,结果又会不一样。因为sleep是中断方法,会捕捉到中断信号。

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run() {
                while (true){
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (InterruptedException e) {
                        System.out.printf("I am interrupted %s\n",isInterrupted());
                    }
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        System.out.printf("Thread is interrupted %s\n",thread.isInterrupted());
        thread.interrupt();
        System.out.printf("Thread is interrupted %s\n",thread.isInterrupted());
    }

结果
在这里插入图片描述
在run方法中我们使用了sleep这个中断方法,它会捕捉中断信号,并且擦除interrupt标识。我们看到输出结果开始为false,中断后变为true,在sleep中 又重置为false。其实也不难理解,可中断方法捕捉到interrupt信号之后,为了不影响线程中其他方法执行,将线程interrupt标识复位是一种很合理的设计。

interrupted

interrupted是一个静态方法,也用作判断当前线程是否被中断。但是和成员方法isInterrupt还是有很大区别。调用该方法会直接擦除掉线程的interrupt标识,如果当前线程被打断了,那么第一次调用interrupted会返回true,并且重置标识为false。再次调用还会返回false。除非又一次调用interrupt。

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(){
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.interrupted());
                }
            }
        };
        //thread.setDaemon(true);
        thread.start();
        TimeUnit.MILLISECONDS.sleep(10);
        thread.interrupt();
    }

结果
在这里插入图片描述

interrupt注意事项

查看源码,可以看到isInterrupt和interrupted都是调用了同一个方法

private native boolean isInterrupted(boolean ClearInterrupted);

参数ClearInterrupted主要用来控制是否擦除线程interrupt的标识。

public boolean isInterrupted() {
	return isInterrupted(false);   
}

public static boolean interrupted() {
	return currentThread().isInterrupted(true);
}

再思考一个问题,如果先调用了interrupt,然后在调用sleep这种阻塞方法,会出现什么情况呢?答案是sleep马上会被打断。但是这里也是视情况而定。如果我们在调用interrupt后调用interrupted方法,那么sleep并不会被打断。如果调用的是isInterrupt或者不调用,都会导致sleep打断。这里就是由于interrupt 标识决定的。

线程join

线程的join也是一个很重要的方法,这个方法也是一个可中断方法,使用interrupt方法可以打断并捕捉到中断信号。
关于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

join

sleep方法是使当前线程进入休眠等待,而join则是使其他线程进入等待。比如现在有两条线程A和B,在线程B中调用线程A的join方法,线程B会进入等待,直到线程A生命周期结束或到达指定时间。

public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println("线程1##"+i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println("线程2##"+i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t3 = new Thread(()->{
            for (int i=0;i<10;i++){
                System.out.println("线程3##"+i);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t1.join();
        t2.start();
        t3.start();
        
    }

结果
在这里插入图片描述
这里代码虽然多些,但是也是比较简单。我创建了三个线程,每个线程都输出从0-9的数。然后启动线程。这里我调用了t1线程的join,可以看到先执行了线程1,然后再交替执行线程2线程3。

这里其实不是很严谨,join阻塞的线程其实不是线程3,而是主线程main。调用t1.join()后,主线程阻塞,因为主线程是当前的调用线程。主线程阻塞,线程2线程3并没有被启动,所以不会有输出。我们换一下join的位置,放到线程2启动之后,在看结果就会发现,线程1和线程2交替输出,线程3等到线程1执行完成后开始执行。是由于主线程阻塞结束执行了线程3的start方法。

关闭线程

jdk有个废弃的方法stop,这个方法已经不推荐使用。官方给出的原因是该方法关闭的线程时可能不会释放掉monitor锁,不建议使用。下面再说一下其他停止线程的方式

正常关闭

1、线程结束生命周期正常退出

线程在运行完成之后,会自动结束生命周期,这种就是放任式的结束。如果不能确保一定能退出,或者逻辑比较复杂,那么还是要有其他的退出方式。

2、捕获中断信号关闭线程

我们可以使用中断信号interrupt来控制线程是否结束。前面有说,当调用interrupt方法后,interrupt flag标识会发生变化。我们可以利用这个来控制流程。

Thread thread = new Thread(){
	@Override
    public void run() {
    	System.out.println("线程开始");
        	while (!isInterrupted()){
            	//Do something
            }
            System.out.println("线程结束");
     }
};
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.interrupt();

3、使用volatile开关控制

由于线程的interrupt标识可能会被擦除,或者一些其他的中断但是不想结束线程,所以我们还会使用volatile修饰的开关flag关闭线程。

public class Demo9 extends Thread {

    private volatile boolean closed = false;

    @Override
    public void run() {
        System.out.println("线程开始");
        while (!closed){
            //Do something
        }
        System.out.println("线程结束");
    }

    public void close(){
        this.closed = true;
    }

    public static void main(String[] args) throws InterruptedException {

        Demo9 thread = new Demo9();
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        thread.close();
    }
}

在这里定义了一个closed变量,并且使用volatile修饰。关于volatile后面会详细说明。这是一个革命性的关键字,它是java原子变量以及并发包的基础。

异常退出

我们可以通过捕捉checked异常,从而在运行过程中进入到catch处理中,不再执行后面的代码,然后退出线程

	/* 自己定义的一种异常,线程可以先去捕获自定义异常*/
    class ExitException extends Exception {
        public ExitException(String message) {
            super(message);
        }
    }

	public static void main(String[] args) {
        Thread thread = new Thread() {
            int i = 0;
            @Override
            public void run() {
                try {
                    while (true) {
                        if (i > 3) {
                            throw new ExitException();
                        }
                        System.out.println(i);
                        i++;
                    }

                } catch (ExitException e) {
                    //捕获到,若不执行就会推出
                    System.out.println("捕获异常信号推出线程");
                }
            }
        };
        thread.start();
    }

我们就是利用异常会跳转到catch的特性,控制线程内的逻辑,从而达到终止线程的目的。

进程假死

有时候,我们得需要借助jconsole这样的监测工具来判断线程的状态。有时候我们发现线程一直没有输出,也没有什么动静。这时候不一定就是线程死亡,也有可能是进入线程休眠或者死锁。特别是死锁,这个开发中会经常见到。我争取坚持写博客,能够把前面所有想单独说的地方都说一下

猜你喜欢

转载自blog.csdn.net/u013513053/article/details/99692589