剑指JUC原理-3.线程常用方法及状态(下)

简介: 剑指JUC原理-3.线程常用方法及状态

剑指JUC原理-3.线程常用方法及状态(上):https://developer.aliyun.com/article/1413565


有时效的join


等待时间

 Thread t1 = new Thread(() -> {
  sleep(1);
  r1 = 10;
 });
 long start = System.currentTimeMillis();
 t1.start();
 // 线程执行结束会导致 join 结束
 t1.join(1500);
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);

输出:

20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010

其实就是如果 执行完时间在 join 等待时间内,那么join同步阻塞就是有效的,否则没等够时间

 Thread t1 = new Thread(() -> {
  sleep(2);
  r1 = 10;
 });
 long start = System.currentTimeMillis();
 t1.start();
 // 线程执行结束会导致 join 结束
 t1.join(1500);
 long end = System.currentTimeMillis();
 log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);

输出:

20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502


interrupt 方法详解


打断 sleep,wait,join 的线程 ,这几个方法都会让线程进入阻塞状态,而打断阻塞状态的线程,会清空打断状态,同时抛出一个 InterruptedException 异常。


打断标记


在 Java 中,每个线程都有一个称为 “打断标记”(interrupt flag)的状态位。这个状态位用于标识线程是否被请求中断。


当一个线程通过调用另一个线程的 interrupt() 方法来请求中断时,被请求中断的线程的打断标记会被设置为 “true”。这意味着被请求中断的线程可以检查自己的打断标记来判断是否被中断,并根据需要采取相应的操作。


打断标记不会直接中断线程的执行,而是提供了一种机制,让线程能够感知到中断请求并根据情况作出响应。具体的响应方式由线程自身决定,可以是终止线程、抛出异常或执行其他适当的操作。


在 Java 中,可以使用 Thread.interrupted() 方法来检查当前线程的打断标记,并清除标记(将其设置为 “false”)。还可以使用 Thread.isInterrupted() 方法来检查线程的打断标记,但不会清除标记。


需要注意的是,打断标记只是一个指示线程是否被请求中断的标志,它不会自动中断线程的执行。线程在执行过程中需要自行检查打断标记,并根据需要采取相应的操作来处理中断请求。


打断阻塞线程


 Thread t1 = new Thread(()->{
   sleep(1);
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
 log.debug(" 打断状态: {}", t1.isInterrupted());

输出:

java.lang.InterruptedException: sleep interrupted
 at java.lang.Thread.sleep(Native Method)
 at java.lang.Thread.sleep(Thread.java:340)
 at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
 at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8)
 at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59)
 at java.lang.Thread.run(Thread.java:745)
21:18:10.374 [main] c.TestInterrupt - 打断状态: false

此时可以看到,不仅抛出 InterruptedException 异常,同时打断状态设置为 false


打断正常线程


使用 interrupt() 方法来打断正在运行的线程不会立即停止线程的执行。它只是设置了线程的打断标记为 “true”,并且如果线程处于等待状态(如 sleep()、wait()、join() 等),它会立即抛出 InterruptedException 异常。


打断正常运行的线程, 不会清空打断状态

 Thread t2 = new Thread(()->{
  while(true) {
    Thread current = Thread.currentThread();
    boolean interrupted = current.isInterrupted();
     if(interrupted) {
      log.debug(" 打断状态: {}", interrupted);
      break;
    }
  }
 }, "t2");
 t2.start();
 sleep(0.5);
 t2.interrupt();

输出:

20:57:37.964 [t2] c.TestInterrupt - 打断状态: true 


终止模式之两阶段终止模式


在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。


使用线程对象的stop方法停止线程


stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁!!1 所以这个方法不妥


使用 System.exit(int) 方法停止线程


目的仅是停止一个线程,但这种做法会让整个程序都停止


两阶段终止模式


以一个监控系统为例

如果有被打断,那么就料理后事,结束循环,如果没有被打断,那么 首先睡眠 2s,如果无异常,那么就正常的执行监控记录,然后继续循环,如果有异常,因为 使用interrupt 如果线程是阻塞的,会抛出 InterruptedException异常同时 重置标志位,所以需要重新设置打断标记。

public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                    // 打断 sleep后会清除 打断标记,所以需要重新设置打断标记
                    current.interrupt();
                }
            }
        },"监控线程");
        thread.start();
    }
    public void stop() {
        thread.interrupt();
    }

输出:

11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop 
11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事


主线程与守护线程


默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。(默认情况下,主线程是守护线程还是非守护线程)

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
  log.debug("开始运行...");
  sleep(2);
  log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");

输出:

08:26:38.123 [main] c.TestDaemon - 开始运行... 
08:26:38.213 [daemon] c.TestDaemon - 开始运行... 
08:26:39.215 [main] c.TestDaemon - 运行结束... 

注意:


垃圾回收器线程就是一种守护线程


Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求


原理解读:


这句话的意思是,在Tomcat服务器中,Acceptor(接收器)和Poller(轮询器)线程都被设置为守护线程。当Tomcat接收到关闭命令时,它不会等待这些线程处理完当前正在进行的请求。


在Tomcat中,Acceptor线程负责接收新的连接请求,而Poller线程负责处理已建立的连接上的I/O操作。由于它们是守护线程,它们的运行不会阻止Tomcat服务器的关闭过程。


当接收到关闭命令时,Tomcat会立即停止接受新的连接请求,并开始关闭已经建立的连接。但是,由于Acceptor和Poller线程是守护线程,它们可能无法处理完当前正在进行的请求,因为守护线程会随着主线程的结束而立即停止。


因此,这句话的含义是,Tomcat在接收到关闭命令后,不会等待Acceptor和Poller线程处理完当前请求,而是立即停止它们的运行,以便尽快关闭服务器。


五种状态


这是从 操作系统 层面来描述的

【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联(相当于 new了线程还没有start)


【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行


【运行状态】指获取了 CPU 时间片运行中的状态,当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换


【阻塞状态】


  • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
  • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
  • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们


【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态


六种状态


这是从Java API层面上来描述


根据 Thread.State 枚举,分为六种状态

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,
        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,
        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,
        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,
        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,
        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

这也是为什么能分成六种状态的原因

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(在Java中,阻塞状态可以由多种原因引起,例如线程等待锁、等待输入/输出完成等。需要注意的是,由于Java的I/O模型通常使用阻塞式的BIO(Blocking I/O),所以在Java中无法区分不同类型的阻塞状态,仍然将其归类为RUNNABLE状态)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束


以一段代码来详解这六种状态吧。

    Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                System.out.println("running...");
            }
        };
        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while(true) { // runnable
                }
            }
        };
        t2.start();
        Thread t3 = new Thread("t3") {
            @Override
            public void run() {
                System.out.println("running...");
            }
        };
        t3.start();
        Thread t4 = new Thread("t4") {
            @Override
            public void run() {
                synchronized (test.class) {
                    try {
                        Thread.sleep(1000000); // timed_waiting
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();
        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    t2.join(); // waiting
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();
        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                // t4先上锁,t6就拿不到了
                synchronized (test.class) { // blocked
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1 state "+ t1.getState());
        System.out.println("t2 state "+ t2.getState());
        System.out.println("t3 state "+ t3.getState());
        System.out.println("t4 state "+ t4.getState());
        System.out.println("t5 state "+ t5.getState());
        System.out.println("t6 state "+ t6.getState());

输出:

t1 state NEW
t2 state RUNNABLE
t3 state TERMINATED
t4 state TIMED_WAITING
t5 state WAITING
t6 state BLOCKED

以这段代码为例,t1其实就是新建状态,t2相当于正在运行中,t3相当于运行结束,t456相当于阻塞的三种状态,4是 sleep阻塞,5是join阻塞,6是获取不到锁阻塞。


各状态资源占用情况


在Java线程的各个状态中,不同状态下线程所占用的资源情况如下:


  1. 新建状态(New):在新建状态下,线程并不占用任何系统资源,只是占用了一些内存空间来存储线程对象本身的信息。
  2. 可运行状态(Runnable):在可运行状态下,线程占用了一些系统资源,包括程序计数器、虚拟机栈和一些线程私有数据。这些资源主要用于保存线程的执行上下文和局部变量等信息。
  3. 运行状态(Running):在运行状态下,线程会占用CPU资源,以便执行线程的任务。此时,除了占用的CPU资源外,线程还会继续占用可运行状态下的资源。
  4. 阻塞状态(Blocked):在阻塞状态下,线程暂时不占用CPU资源,但仍然占用了一些系统资源。具体资源的占用情况取决于线程被阻塞的原因。例如,如果线程因为等待获取锁而被阻塞,那么它会占用一定数量的锁资源和等待队列。
  5. 等待状态(Waiting):在等待状态下,线程通常不占用CPU资源和锁资源,但仍然占用了一些系统资源。这些资源包括等待队列、条件变量和一些其他线程同步机制所需的资源。
  6. 终止状态(Terminated):在终止状态下,线程不再占用任何系统资源。它的执行上下文和局部变量等信息都会被释放,线程对象本身也可以被垃圾回收。


需要注意的是,不同状态下线程所占用的资源情况是动态变化的。线程的状态会根据线程的调度、等待条件的满足以及任务的完成而发生变化。因此,在编写多线程程序时,需要注意合理管理线程的状态和资源,以避免资源的浪费和性能的下降。

目录
相关文章
|
23天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
16 3
|
23天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
23天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
15 1
|
23天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1
|
23天前
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
23 1
|
23天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
33 1
|
23天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1
|
27天前
|
监控 Java
在实际应用中选择线程异常捕获方法的考量
【10月更文挑战第15天】选择最适合的线程异常捕获方法需要综合考虑多种因素。没有一种方法是绝对最优的,需要根据具体情况进行权衡和选择。在实际应用中,还需要不断地实践和总结经验,以提高异常处理的效果和程序的稳定性。
19 3
|
27天前
|
监控 Java
捕获线程执行异常的多种方法
【10月更文挑战第15天】捕获线程执行异常的方法多种多样,每种方法都有其特点和适用场景。在实际开发中,需要根据具体情况选择合适的方法或结合多种方法来实现全面有效的线程异常捕获。这有助于提高程序的健壮性和稳定性,减少因线程异常带来的潜在风险。
19 1
|
1月前
|
Java C++
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
【多线程】JUC的常见类,Callable接口,ReentranLock,Semaphore,CountDownLatch
33 0