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

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

常用方法

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

start和run

调用run

public static void main(String[] args) {
 Thread t1 = new Thread("t1") {
 @Override
 	public void run() {
 		log.debug(Thread.currentThread().getName());
 		FileReader.read(Constants.MP4_FULL_PATH);
 	}
 };
 t1.run();
 log.debug("do other things ...");
}

输出:

19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

调用start

将上述代码的 t1.run() 改为 t1.start

输出:

19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

此时再t1线程执行,而FileReader.read() 方法调用是异步的

总结

直接调用run是在主线程中执行了run,并没有启用新的线程,而使用start是启动新的线程,通过新的线程简介执行了run中的代码。

sleep和yield

sleep

**调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)**以一段代码为例

		Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t1.start();

        System.out.println("t1 state: "+ t1.getState());

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1 state: "+ t1.getState());

输出:

t1 state: RUNNABLE
t1 state: TIMED_WAITING

其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

		Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt...");
        t1.interrupt();

输出:

enter sleep...
wake up...
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at test2$1.run(test2.java:10)

睡眠结束后的线程未必会立刻得到执行

睡眠结束,可能还在处理其他的线程,需要等待cpu调度器调度

yield

本质上是让出cpu的使用权, 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程。具体的实现依赖于操作系统的任务调度器

线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

join方法详解

为什么需要join?

下面的代码执行,打印 r 是什么?

		log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");
        t1.start();
        // t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");

分析:

  • ​ 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • ​ 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

解决方案:

  • ​ 用 sleep 行不行?为什么?不能精准的把控执行结束的时间。
  • ​ 用 join,加在 t1.start() 之后即可**(可选)**

以调度的角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

在这里插入图片描述

等待多个结果

Thread t1 = new Thread(() -> {
 	sleep(1);
 	r1 = 10;
});
Thread t2 = new Thread(() -> {
 	sleep(2);
 	r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();

t1.join();
t2.join();

long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);

分析:

实际上,2s执行完成,为什么呢?因为 两个线程都是同时启动的,无论t1.join 和 t2.join那个在前那个在后,其最终都会对两个线程分别的阻塞等待,只因为两个线程是同时启动的。

所以结果时不变的,效果图如下:

在这里插入图片描述

有时效的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):在终止状态下,线程不再占用任何系统资源。它的执行上下文和局部变量等信息都会被释放,线程对象本身也可以被垃圾回收。

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

相关推荐博客

👉👉👉 剑指JUC原理-1.进程与线程-CSDN博客

👉👉👉 剑指JUC原理-2.线程-CSDN博客

👉👉👉 剑指JUC原理-4.共享资源和线程安全性-CSDN博客

👉👉👉 剑指JUC原理-5.synchronized底层原理-CSDN博客

👉👉👉 剑指JUC原理-6.wait notify-CSDN博客

👉👉👉 剑指JUC原理-7.线程状态与ReentrantLock-CSDN博客

👉👉👉 剑指JUC原理-8.Java内存模型-CSDN博客

👉👉👉 剑指JUC原理-9.Java无锁模型-CSDN博客

👉👉👉 剑指JUC原理-10.并发编程大师的原子累加器底层优化原理(与人类的优秀灵魂对话)-CSDN博客

👉👉👉 剑指JUC原理-11.不可变设计-CSDN博客

👉👉👉 剑指JUC原理-12.手写简易版线程池思路-CSDN博客

👉👉👉 剑指JUC原理-13.线程池-CSDN博客

👉👉👉 剑指JUC原理-14.ReentrantLock原理-CSDN博客

👉👉👉 剑指JUC原理-15.ThreadLocal-CSDN博客

👉👉👉 剑指JUC原理-16.读写锁-CSDN博客

👉👉👉 剑指JUC原理-17.CompletableFuture-CSDN博客

👉👉👉 剑指JUC原理-18.同步协作-CSDN博客

👉👉👉 剑指JUC原理-19.线程安全集合-CSDN博客

👉👉👉 剑指JUC原理-20.并发编程实践-CSDN博客

  • 11
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱吃芝士的土豆倪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值