Java多线程基础详解(二)

线程的生命周期和状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW:初始状态,线程被创建出来,但是还没有调用start()方法。
  • RUNABLE:可运行状态,调用了start()方法,Java线程将操作系统中的就绪/可运行(READY)和运行中(RUNNING)两种状态统称为RUNABLE(可运行)状态。
  • BLOCKED:阻塞状态,线程阻塞于锁,需要等待锁释放。
  • WATING:等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIMED_WATING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:表示当前线程已经执行完毕。

在这里插入图片描述

  • 由上图可以看出:线程创建之后它将处于 NEW(初始) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(就绪/可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行中) 状态。

  • 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(可运行) 状态 。

  • 为什么 JVM 没有区分这两种状态呢?
    java 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。

  • 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

  • TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

  • 当线程进入 synchronized 方法/块或者调用 wait 后,(被 notify)想要重新进入 synchronized 方法/块时,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

  • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

线程控制

理解了线程生命周期的基础上,可以使用Java提供的线程控制命令对线程的生命周期进行干预。

线程控制方法详解

join()方法

Thread类提供了让一个线程等待另一个线程完成的方法——join()方法(或者说让主线程等待子线程完成)。

当在某个程序执行流中调用其他线程的join()方法时,调用线程将进入等待状态(WAITING),直到被join()方法加入的join线程执行完为止。

join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

讲解案例

上面的解释可能有些枯燥,我们来看一个简单的例子直观的感受一下join()方法的作用:

public class TestE {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.setName("子线程1");
        thread2.setName("子线程2");
        thread1.start();
        thread2.start();
        //thread1和thread2调用了join(),主线程进入等待状态
        thread1.join();
        thread2.join();

        //主线程需要等待调用了join()方法的所有子线程执行结束后才会执行
        System.out.println("主线程执行");
    }
}

运行结果:

子线程1执行
子线程2执行
主线程执行
//让thread1和thread2优先于主线程执行,主线程进入WAITING状态,直到两个子线程执行完
thread1.join();
thread2.join();

可以替换为如下代码:

while (thread1.isAlive() || thread2.isAlive()) {
    //只要两个线程中有任何一个线程还在活动,主线程就不会往下执行
}

这两种方式效果是一样的

如果不让子线程调用join()方法,主线程执行结束后子线程才能执行:

public class TestE {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行");
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.setName("子线程1");
        thread2.setName("子线程2");
        thread1.start();
        thread2.start();
        //不让子线程调用join()方法
        //thread1.join();
        //thread2.join();

        //主线程需要等待调用了join()方法的所有子线程执行结束后才会执行
        System.out.println("主线程执行");
    }
}

运行结果:

主线程执行
子线程1执行
子线程2执行
源码分析
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
    /**
     * 关键代码,让子线程调用join()方法,意味着参数millis=0,所以会进入这个if语句中(不明白的可以看下面的图)
     * wait(0):让线程一直等待,特别注意的是,这个wait()方法是Object的native wait方法,所以他实际生效的是当前线程,即会让主线程一直等待
     * 如下图所示,这个wait()方法是Object的方法,而不是被调用join()方法的子线程对象的
     * join()方法是用wait()方法实现,但为什么没有通过notify()系列方法唤醒呀,如果不唤醒,那不就一直等待下去了吗?
     * 原因是:
     * 被调用的线程执行完毕后,会自动调用该对象的notifyAll()方法来唤醒等待队列中的线程。
     * 这意味着在join()方法内部已经处理了唤醒等待的逻辑,不需要显式使用notify()方法来进行唤醒。
     */
                wait(0); 
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

调用某个线程的 join 方法,实际调用的是这个方法,然后才会调用内部的 join 方法,也就是上面的代码,同时传参 millis = 0。
在这里插入图片描述

在这里插入图片描述

sleep()方法

sleep()方法可以使线程进入WAITING状态,而且不会占用CPU资源(让出CPU给其他线程),但也不会释放锁,直到过了规定的时间后再执行后续代码,休眠期间如果被中断,会抛出异常,并会清空中断状态标记。

sleep()方法的特点
  1. sleep()方法可以使线程进入WAITING状态
    这个和wait()方法一致,都会使线程进入等待状态。

  2. 不会占用CPU资源
    不会浪费系统资源,可以放心使用。

  3. 不释放锁
    我记忆的口诀分享:sleep()方法是抱着锁睡觉。
    线程休眠期间是不会释放锁的,线程会一直保持在等待状态。

  4. 响应中断
    遇到中断请求时,支持当前线程中断,并抛出sleep interrupted异常。

sleep()方法的两种代码写法
  1. Thread.sleep(timeout)

    此方式参数只能是毫秒,如果参数是负值,则会抛出异常。虽然常见,但不推荐使用。

  2. TimeUnit.SECONDS.sleep(timeout)

    此种方式可以接收负数参数,当参数为负数,阅读源码会发现,它会跳过执行,所以不会抛出异常。此种方式的可读性高,可以指定小时、分钟、秒、毫秒、微秒等参数,所以更加优秀,推荐使用这种写法。
    例如:
    TimeUnit.MICROSECONDS.sleep(1000);
    TimeUnit.SECONDS.sleep(1000);
    TimeUnit.DAYS.sleep(1000);

源码分析:

public void sleep(long timeout) throws InterruptedException {
        //如果参数是负数,会跳过执行,所以不会抛出异常
        if (timeout > 0) {
            //自动进行时间格式转换
            long ms = toMillis(timeout);
            int ns = excessNanos(timeout, ms);
            //底层仍然是通过Thread.sleep()方法实现的,只是对Thread.sleep()方法进行了封装
            Thread.sleep(ms, ns);
        }
    }
sleep() 方法和 wait() 方法对比

共同点:

  • 两者都可以暂停线程的执行。
  • 两者都可以响应中断。

不同点:

  • sleep()方法没有释放锁,而wait()方法释放了锁。
  • sleep()方法通常用于暂停线程的执行,wait()方法通常用于线程间交互/通信。
  • sleep() 方法执行完成后,线程会自动苏醒;wait() 方法被调用后,线程不会自动苏醒,需要其他线程调用同一个对象上的 notify()或者 notifyAll() 方法。或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep()方法是Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。
  • wait()、notify()方法必须写在同步方法/同步代码块中,是为了防止死锁和永久等待,使线程更安全,而sleep()方法没有这个限制。

yield()方法

暂停当前正在执行的线程对象(即放弃当前拥有的cup资源),并执行其他线程。
yield()做的是让当前运行线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。
但是,实际中无法保证yield()一定能达到让步目的,因为让步的线程还有可能被线程调度程序再次选中,即有可能刚刚放弃但是马上又获得cpu时间片。

yield()方法详解
  1. yield()方法只是提出申请释放CPU资源,至于能否成功释放由JVM决定。由于这个特性,一般编程中用不到此方法,但在很多并发工具包中,yield()方法被使用,如AQS、ConcurrentHashMap、FutureTask等。

  2. 调用了yield()方法后,线程依然处于RUNNABLE状态,线程不会进入堵塞状态。

什么是堵塞状态?

线程状态是处于BLOCKED或WAITING或TIME_WAITING这三种统称为堵塞状态,堵塞状态下cpu是不会分配时间片的。

  1. 调用了yield()方法后,线程处于RUNNABLE状态时,线程就保留了随时被调度的权利。
yield()方法和sleep()方法有什么区别

yield()方法调用后线程处于RUNNABLE状态,而sleep()方法调用后线程处于TIME_WAITING状态,所以yield()方法调用后线程只是暂时的将调度权让给别人,但立刻可以回到竞争线程锁的状态;而sleep()方法调用后线程处于阻塞状态。

setDaemon()方法

JAVA线程分为即实线程与守护线程,守护线程是优先级低,存活与否不影响JVM退出的线程,实现守护线程的方法是在线程start()之前setDaemon(true)。

其他非守护线程关闭后无需手动关闭守护线程,守护线程会自动关闭,避免了麻烦,Java垃圾回收线程就是一个典型的守护线程。

守护线程的特点当非守护线程执行结束时,守护线程跟着销毁。当运行的唯一线程是守护线程时,Java虚拟机将退出。

案例详解
public class TestF {

    public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while(i++ < 1000){
                    System.out.println("子线程执行:"+i);
                }
            }
        });
        
        /**
         * 如果添加了这段代码:thread.setDaemon(true),证明将thread线程设置为守护线程
         * 
         * 由thread线程run()方法可知,thread线程会循环执行1000条输出语句,而主线程只会循环执行10条输出语句
         * 
         * 如果将thread线程设置为守护线程,当主线程的输出语句执行完毕时,程序就会终止,无论thread线程是否循环执行完1000条输出语句
         * 
         * 如果没有将thread线程设置为守护线程,即使主线程的输出语句已经执行完毕,程序仍然不会终止,直到thread线程循环执行完1000条输出语                               句,程序才会终止
         */
        thread.setDaemon(true);
        
        thread.start();

        for(int i =0; i < 10; i++){
            System.out.println("主线程执行:"+i);
        }

    }
}

interrupt()方法

interrupt()方法是Thread类的一个方法,用于中断线程的执行。调用该方法会设置线程的中断状态为true。具体而言,该方法会在以下情况下起作用:

  1. 当线程处于阻塞状态(如sleep、wait、join等)时,调用interrupt()方法会使线程立即抛出InterruptedException异常,从而可以提前终止线程的阻塞状态;
  2. 如果线程处于运行状态,则中断状态会被设置为true,可通过isInterrupted()方法检测线程的中断状态。可以根据中断状态来决定线程是否继续执行;
  3. 如果线程已经被中断,那么再次调用interrupt()方法并不会造成任何效果。

需要注意的是,interrupt()方法并不会真正中断线程的执行,它只是提供了一种机制,可以通知线程中断的需求,并可通过检测中断状态来决定线程的行为。真正的中断需要在业务逻辑中显式处理,并在适当的时候使用return或break等语句来终止线程的执行。

假设有一个线程执行了一个耗时操作,我们可以通过使用interrupt()方法来中断该线程。以下是一个例子:

public class MyThread extends Thread {
    public void run() {
        try {
            // 模拟耗时操作
            for (int i = 0; i < 10; i++) {
                Thread.sleep(1000);
                System.out.println("正在执行耗时操作...");
            }
        } catch (InterruptedException e) {
            System.out.println("线程被中断了");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // 启动线程

        // 主线程休眠3秒后中断子线程
        try {
            Thread.sleep(3000);
            thread.interrupt();  // 中断线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,MyThread线程执行了一个模拟的耗时操作,在每次循环中休眠1秒钟。在主线程中,我们休眠3秒钟后调用了线程的interrupt()方法,以中断MyThread线程的执行。

当主线程调用interrupt()方法后,MyThread线程会立即从sleep方法中抛出InterruptedException异常,然后会执行catch块中的代码,输出"线程被中断了"。这说明线程确实被中断了。在实际业务中,我们可以根据中断状态来决定线程的继续执行或提前退出的逻辑。

interrupted()方法

interrupted()方法是Thread类的一个静态方法,用于判断当前线程是否被中断,并会清除中断状态。具体而言,该方法会在以下情况下起作用:

  1. 调用interrupt()方法后,可以通过interrupted()方法判断线程的中断状态;
  2. 通过interrupted()方法判断线程是否处于中断状态后,会清除线程的中断状态,即将线程的中断状态重置为false。

该方法的返回值为boolean类型,如果当前线程被中断,则返回true;否则返回false。

以下是一个简单的例子,展示了interrupted()方法的用法:

public class MyThread extends Thread {
    public void run() {
        while (!Thread.interrupted()) {
            System.out.println("线程正在运行...");
        }
        System.out.println("线程被中断了");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // 启动线程

        // 主线程休眠3秒后中断子线程
        try {
            Thread.sleep(3000);
            thread.interrupt();  // 中断线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,MyThread线程的run()方法中使用了一个while循环来判断线程是否被中断。当线程被中断后,会退出while循环并输出"线程被中断了"。

在主线程中,我们休眠3秒后调用了线程的interrupt()方法,以中断MyThread线程的执行。在MyThread线程的run()方法中,我们通过Thread.interrupted()方法来判断线程是否被中断,当线程被中断后,Thread.interrupted()方法会返回true,并且会清除线程的中断状态。

因此,当我们运行上述代码时,3秒后MyThread线程会输出"线程被中断了",说明线程确实被中断了。

isInterrupted()

isInterrupted()方法是Thread类的一个实例方法,用于判断当前线程是否被中断,但不会清除中断状态。具体而言,该方法会在以下情况下起作用:

  1. 调用interrupt()方法后,可以通过isInterrupted()方法判断线程的中断状态;
  2. 通过isInterrupted()方法判断线程是否处于中断状态后,线程的中断状态不会发生改变,即不会将线程的中断状态重置为false。

该方法的返回值为boolean类型,如果当前线程被中断,则返回true;否则返回false。

以下是一个简单的例子,展示了isInterrupted()方法的用法:

public class MyThread extends Thread {
    public void run() {
        while (!isInterrupted()) {
            System.out.println("线程正在运行...");
        }
        System.out.println("线程被中断了");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // 启动线程

        // 主线程休眠3秒后中断子线程
        try {
            Thread.sleep(3000);
            thread.interrupt();  // 中断线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,MyThread线程的run()方法中使用了一个while循环来判断线程是否被中断。当线程被中断后,会退出while循环并输出"线程被中断了"。

在主线程中,我们休眠3秒后调用了线程的interrupt()方法,以中断MyThread线程的执行。在MyThread线程的run()方法中,我们通过isInterrupted()方法来判断线程是否被中断,当线程被中断后,isInterrupted()方法会返回true。

因此,当我们运行上述代码时,3秒后MyThread线程会输出"线程被中断了",说明线程确实被中断了。

stop()方法

强制线程停止执行。

stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。

而 interrupt() 方法就温柔多了,interrupt() 方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被 interrupt 的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。

当线程 A 处于 WAITING、TIMED_WAITING 状态时,如果其他线程调用线程 A 的 interrupt() 方法,会使线程 A 返回到 RUNNABLE 状态,同时线程 A 的代码会触发 InterruptedException 异常。上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait()、join()、sleep() 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他线程调用了该线程的 interrupt() 方法。

当线程 A 处于 RUNNABLE 状态时,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;而阻塞在 java.nio.channels.Selector 上时,如果其他线程调用线程 A 的 interrupt() 方法,线程 A 的 java.nio.channels.Selector 会立即返回。

上面这两种情况属于被中断的线程通过异常的方式获得了通知。还有一种是主动检测,如果线程处于 RUNNABLE 状态,并且没有阻塞在某个 I/O 操作上,例如中断计算圆周率的线程 A,这时就得依赖线程 A 主动检测中断状态了。如果其他线程调用线程 A 的 interrupt() 方法,那么线程 A 可以通过 isInterrupted() 方法,检测是不是自己被中断了。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
`newFixedThreadPool` 是 Java 中线程池的一种实现方式,它可以创建一个固定大小的线程池,并且只有在池中的所有线程都处于忙碌状态时,才将新的任务加入到队列中等待执行。 以下是 `newFixedThreadPool` 的详细解释: 1. 创建一个固定大小的线程池,该线程池中的线程数量是固定的,一旦创建便无法更改。这意味着,如果池中的所有线程都处于忙碌状态并且有更多的任务需要执行,那么这些任务将被放置在一个队列中,等待空闲线程的出现。 2. 线程池中的所有线程都是可重用的,这意味着在执行完任务之后,线程将返回线程池并等待下一个任务的到来。 3. 线程池中的所有线程都是后台线程,这意味着它们不阻止应用程序的关闭。 4. 线程池中的任务可以是任何实现了 `Runnable` 接口或 `Callable` 接口的对象。使用 `Callable` 接口可以允许任务返回一个值,并且可以抛出异常。 5. 线程池中的任务将按照加入队列的顺序进行执行。 6. `newFixedThreadPool` 的底层实现是一个无界的工作队列和一个固定数量的线程池。 使用 `newFixedThreadPool` 可以有效地控制线程的数量,从而避免创建过多的线程而导致系统的资源浪费和性能下降。但是,如果任务的数量过多,而线程池中的线程数量过少,那么仍然出现任务排队等待的情况。因此,在使用 `newFixedThreadPool` 时,需要根据实际情况来确定线程池的大小。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路上阡陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值