1、Java 线程的基本知识

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


本章内容

  • 创建和运行线程
  • 查看线程
  • 线程 API
  • 线程状态

提示:以下是本篇文章正文内容,下面案例可供参考

一、创建和运行线程

方法一,直接使用 Thread

// 创建线程对象
Thread t = new Thread() {
 public void run() {
 // 要执行的任务
 }
};
// 启动线程
t.start();

// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
 @Override
 // run 方法内实现了要执行的任务
 public void run() {
 log.debug("hello");
 }
};
t1.start();

示例:

@Slf4j(topic = "c.Test1")
public class Test1 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                log.debug("running");
            }
        };
        t.setName("t1");
        t.start();
        log.debug("running");
    }
}
13:10:35.380 [t1] DEBUG c.Test1 - running
13:10:35.376 [main] DEBUG c.Test1 - running

方法二,使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
 public void run(){
 // 要执行的任务
 }
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

// 创建任务对象
Runnable task2 = new Runnable() {
 @Override
 public void run() {
 log.debug("hello");
 }
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

示例:

@Slf4j(topic = "c.Test2")
public class Test2 {

    public static void main(String[] args) {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                log.debug("running");
            }
        };
        
        Thread thread = new Thread(runnable,"t2");
        thread.start();
        log.debug("running");
    }

}
13:12:14.164 [main] DEBUG c.Test2 - running
13:12:14.177 [t2] DEBUG c.Test2 - running

使用lambda简化:因为Runnable是一个函数式接口

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

因此通常这样写,简化代码:

Runnable runnable1=()->{
          log.debug("running");
 };

原理之 Thread 与 Runnable 的关系

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • Thread实现了Runnable接口。
  • 在初始化Thread的时候,如果使用的是有参构造器
  • 会把Runnable的实现作为target传入方法,最终会赋值给Thread的成员变量target
  • Thread的对象调用run方法时,其实是target调用run方法,也就是之前传进来的Runnable的实现

小结

方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了

  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

方法三,FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
 log.debug("hello");
 return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);

示例:

@Slf4j(topic = "c.Test3")
public class Test3 {

    public static void main(String[] args) throws Exception{
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Integer call() throws Exception {

                log.debug("running");
                Thread.sleep(1000);
                return 100;

            }
        });

        Thread thread = new Thread(task,"t1");
        thread.start();

        log.debug("{}",task.get());//主线程就会阻塞在这,等待t1线程的任务task返回结果

    }
}

观察多个线程同时运行

测试代码

@Slf4j(topic = "c.Test4")
public class Test4 {

    public static void main(String[] args) {
         new Thread(()->{
            while (true){
              log.debug("running");
            }
        },"t1").start();

         new Thread(()->{
             while (true){
                 log.debug("running");
             }
         },"t2").start();

    }
}

现象:交替执行,谁先谁后,不由我们控制

13:23:37.483 [t2] DEBUG c.Test4 - running
13:23:37.483 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.489 [t1] DEBUG c.Test4 - running
13:23:37.489 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.490 [t2] DEBUG c.Test4 - running
13:23:37.490 [t1] DEBUG c.Test4 - running
13:23:37.510 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
13:23:37.511 [t2] DEBUG c.Test4 - running
...

二、查看进程线程的方法

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
jconsole 远程监控配置

如果是本地查看,直接在命令行窗口如果jconsole
在这里插入图片描述
或者找到你JDK的安装路径,在bin中找到这个命令
在这里插入图片描述

最中就是这样

在这里插入图片描述
这里面可以查看很多东西,比如,是否线程死锁、内存占用等等,是一种JDK自带的监控工具

你也可以远程监控:
在远程服务器上这样运行你的java类

java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java类

在这里插入图片描述

三、原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
public class TestFrames {
    public static void main(String[] args) {
        new Thread(()->{
            method1(20);
        },"t1").start();

        method1(10);
    }
    private  static  void method1(int x){
        int y= x+1;
        Object o = method2();
        System.out.println(o);

    }

    private  static Object method2(){
        Object n = new Object();
        return n;
    }
}

调整debug模式:Thread
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
一个栈帧就对应线程中的一个方法,栈帧按照线程中方法的执行顺序入栈,先进后出,每当一个方法执行完毕后,其对应的栈帧也就会弹出。

多线程的工作工作原理:JVM会对每个线程都分配一个私有的栈。

#四、 线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

五、线程中的方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

start()和run()

run()

run方法并不能启动线程,它定义的是线程对应的业务逻辑

看下面代码:
测试1:

package cn.itcast.text;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test5")
public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1");
        System.out.println(thread.getState());
        thread.run();
        System.out.println(thread.getState());
    }
}

NEW
15:01:44.912 [main] DEBUG c.Test5 - main
NEW

直接调用run,其实是主线程(当前线程)调用的,就是对象调用普通方法,调用前后,t1线程一直处于新建状态。

start()

测试2:把run改成start
start():启动线程,线程进入就绪态,等待CPU分配时间片,并执行run方法()

@Slf4j(topic = "c.Test5")
public class Test5 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1");
        System.out.println(thread.getState());
        thread.start();
        System.out.println(thread.getState());

    }
}
NEW
RUNNABLE
15:03:58.519 [t1] DEBUG c.Test5 - t1

测试2可以发现,start调用前后,t1的线程的状态由新建(NEW)变为运行(RUNNABLE)

其实调用start()后,线程是进入就绪状态,等待CPU分配时间片,真正分配时间片后才执行

sleep()和yield()

sleep()

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行 (需要等待CPU分配时间片)
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
  5. Thread.sleep在哪个线程里就是让哪个睡眠
  TimeUnit.SECONDS.sleep(1);//可读性更好

测试1:

package cn.itcast.text;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test6")
public class Test6 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1");
        thread.start();
        System.out.println(thread.getState());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(thread.getState());
    }
}

RUNNABLE
15:17:23.423 [t1] DEBUG c.Test6 - t1
TIMED_WAITING

这里对RUNNABLE进行个解释:因为主线程不会阻塞在【thread.start();】方法这,它会继续执行,而t1未必执行到了【 Thread.sleep(10000);】,t1和主线程是异步的。如果让主线程等待一下【 Thread.sleep(500);】,就能看到TIMED_WAITING

测试2:打断sleep

@Slf4j(topic = "c.Test6")
public class Test6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    log.debug("wake up ");
                    e.printStackTrace();
                }
            }
        }, "t1");

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

    }
}

15:23:15.413 [t1] DEBUG c.Test6 - enter sleep...
15:23:16.422 [main] DEBUG c.Test6 - interrupt  t1
15:23:16.422 [t1] DEBUG c.Test6 - wake up 
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.text.Test6$1.run(Test6.java:13)
	at java.lang.Thread.run(Thread.java:748)

yield()

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

就绪状态还有机会等待CPU分配时间片

  1. 具体的实现依赖于操作系统的任务调度器

首先不使用yield

public class Test7 {

    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("     ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.start();
        t2.start();
    }
}
---->1 48614
---->1 48615
---->1 48616
---->1 48617
---->1 48618
---->1 48619
---->1 48620
---->1 48621
---->1 48622
---->1 48623
     ---->2 37304
     ---->2 37305
     ---->2 37306
     ---->2 37307
     ---->2 37308
     ---->2 37309
     ---->2 37310
     ---->2 37311
     ---->2 37312
     ---->2 37313
     ---->2 37314
     ---->2 37315
     ---->2 37316
     ---->2 37317
     ---->2 37318
     ---->2 37319
     ---->2 37320
---->1 48624
---->1 48625
---->1 48626
---->1 48627
---->1 48628

使用yield

---->1 16386
---->1 16387
---->1 16388
---->1 16389
---->1 16390
---->1 16391
---->1 16392
---->1 16393
---->1 16394
---->1 16395
---->1 16396
---->1 16397
---->1 16398
---->1 16399
---->1 16400
---->1 16401
     ---->2 40
---->1 16402
---->1 16403
---->1 16404
---->1 16405

还是能明显看出,线程2的执行次数比线程1少的,这是因为每次线程2在执行前都会让出CPU的执行权,进入就绪

Sleep与yield的区别:

1.sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会
2.yield()方法只会给相同优先级或更高优先级的线程以运行的机会
3.线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态
4.sleep()方法声明会抛出InterruptedException,而yield()方法没有声明任何异常
5.sleep()方法比yield()方法具有更好的移植性(跟操作系统CPU调度相关)

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

我们给上述代码设置一下优先级

public class Test7 {

    public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("     ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }
}

     ---->2 4064
     ---->2 4065
     ---->2 4066
     ---->2 4067
     ---->2 4068
     ---->2 4069
     ---->2 4070
     ---->2 4071
     ---->2 4072
     ---->2 4073
     ---->2 4074
---->1 6654
---->1 6655
---->1 6656
---->1 6657
---->1 6658

Sleep应用——防止CPU占用100%

在没有利用CPU来计算时,不要让while空转浪费CPU,这时可以使用yield或sleep来让出CPU的使用权给其他程序

 while (true){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

测试不要sleep

public class Test7 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                
                }
            }
        }).start();
    }
}

用JDK自带的jvisualvm监控看下
在这里插入图片描述
在这里插入图片描述

jvisualvm比jconsole ,功能更全,也是JDK自带的,多用于JVM性能监控,是JVM调优的好把手,详细的使用可以学习我JVM调优的博客,后面会介绍更多的调优工具,以及调优案例:https://blog.csdn.net/m0_45364328/article/details/124641883

测试使用sleep:

public class Test7 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

在这里插入图片描述
很明显线程的CPU占用就低了很多,sleep可以让出当前CPU的使用权,不霸占CPU,否则,一个死循环监听功能势必造成其他业务线程的阻塞

  • 可以用wait或调价变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要响应的唤醒操作,一般适用于要进行同的场景
  • sleep适用于无需锁同步的场景

join()

为什么需要 join?下面的代码执行,打印 r 是什么?

@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r = 0;
    public static void main(String[] args)  {
        test1();
    }
    private static void test1()   {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

17:26:07.040 [main] DEBUG c.TestJoin - 开始
17:26:07.128 [main] DEBUG c.TestJoin - 结果为:0
17:26:07.131 [main] DEBUG c.TestJoin - 结束
17:26:07.132 [Thread-0] DEBUG c.TestJoin - 开始
17:26:07.134 [Thread-0] DEBUG c.TestJoin - 结束

打印结果是0,原因是因为主线程和Thread-0是异步的,主线程在打印r 时,Thread-0线程还没给r赋上值,可以让主线程Sleep一段时间再打印r,但是sleep多久合适呢,因为我们不知道Thread-0需要执行多久,因此join的作用就体现了
使用join:

@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r = 0;
    public static void main(String[] args)  throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

17:30:31.290 [main] DEBUG c.TestJoin - 开始
17:30:31.386 [Thread-0] DEBUG c.TestJoin - 开始
17:30:31.388 [Thread-0] DEBUG c.TestJoin - 结束
17:30:31.388 [main] DEBUG c.TestJoin - 结果为:10
17:30:31.390 [main] DEBUG c.TestJoin - 结束

在主线程中使用 t1.join();
主线程就会阻塞在join处,等待t1执行完,才放行。

应用之同步(案例1)

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步
    在这里插入图片描述

等待多个结果
问,下面代码 cost 大约多少秒?

@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r1 = 0;
    static int r2 = 0;
    public static void main(String[] args) throws InterruptedException {
        test2();
    }
    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r2 = 20;

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


17:38:37.889 [main] DEBUG c.TestJoin - join begin
17:38:38.894 [main] DEBUG c.TestJoin - t1 join end
17:38:39.890 [main] DEBUG c.TestJoin - t2 join end
17:38:39.890 [main] DEBUG c.TestJoin - r1: 10 r2: 20 cost: 2005

分析如下

  • 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
  • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s

也就说由多个join阻塞的时候,它的时间使用阻塞在第一个join开始,如果计算在第二个join阻塞的时间的时候,也要算上在前面join的阻塞时间

如果颠倒两个 join 呢?

        log.debug("join begin");
        t2.join();
        log.debug("t2 join end");
        t1.join();
        log.debug("t1 join end");
17:41:37.012 [main] DEBUG c.TestJoin - join begin
17:41:39.023 [main] DEBUG c.TestJoin - t2 join end
17:41:39.023 [main] DEBUG c.TestJoin - t1 join end
17:41:39.023 [main] DEBUG c.TestJoin - r1: 10 r2: 20 cost: 2016

因为主线程已经在t2阻塞了两秒,而t1只需要阻塞1秒,已经包含在之前阻塞的时间里面了,也可以认为在主线程阻塞在等待t2运行结束的时候,t1已经执行结束了。

在这里插入图片描述

有时效的 join

Join的也可以设置等待时间

@Slf4j(topic = "c.TestJoin")
public class TestJoin {
    static int r1 = 0;
    public static void main(String[] args) throws InterruptedException {
        test2();
    }
    private static void test2() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        
        t1.start();
        t1.join(1000);
        log.debug("r1: {}", r1);
    }
}

17:47:16.689 [main] DEBUG c.TestJoin - r1: 0

因为t1 线程的执行需要2秒,而主线程只阻塞等待t1执行完毕1秒,所以t1不能给r1赋上值

修改主线程等待t1时间:

 t1.join(3000);
17:48:46.082 [main] DEBUG c.TestJoin - r1: 10

这样就赋上值了,也就是说我们可以设置一个合理的阻塞时,如果时间内,t1线程不能执行完,我们就不阻塞当前线程了。

interrupt 方法

打断 sleep,wait,join 的线程(打断阻塞线程)

这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例,同时抛出InterruptedException异常,打断标记为false

@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {

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

        Thread t1 = new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        Thread.sleep(500);
        t1.interrupt();
        log.debug(" 打断状态: {}", t1.isInterrupted());

    }
}

18:37:55.844 [main] DEBUG c.TestInterrupt -  打断状态: false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.text.TestInterrupt.lambda$main$0(TestInterrupt.java:12)
	at java.lang.Thread.run(Thread.java:748)

打断正常运行的线程

不会抛出InterruptedException异常,打断标记为true

@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {

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

        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()){//被打断,退出循环,终止线程
                    log.debug("被打断了,退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();
        Thread.sleep(500);
        t1.interrupt();
        log.debug(" 打断状态: {}", t1.isInterrupted());

    }
}

18:41:27.523 [t1] DEBUG c.TestInterrupt - 被打断了,退出循环
18:41:27.523 [main] DEBUG c.TestInterrupt -  打断状态: true

可以通过打断标记判断要做其他的事,比如终止线程

两阶段终止模式

什么是两阶段终止模式?
在一线程T1中“优雅”终止线程T2,这里的优雅指的是,给T2一个料理后事的机会

1、错误的思路

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

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

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

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

2、两阶段终止模式设计模型
在这里插入图片描述

@Slf4j(topic = "c.Test10")
public class Test10 {

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

        TwoPhaseTermination tpt = new TwoPhaseTermination();
        tpt.start();
        Thread.sleep(3500);
        tpt.stop();

    }


}

@Slf4j(topic = "c.TwoPhaseTermination")
class TwoPhaseTermination {
    private Thread monitor;

    //启动监控线程

    public void start() {

        monitor = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    Thread thread = Thread.currentThread();
                    if (thread.isInterrupted()) {
                        log.debug("料理后事");
                        break;
                    }

                    try {
                       Thread.sleep(1000);//处于阻塞状态的线程,如果被打断,打断标记会设置为true,同时抛出异常
                        log.debug("执行监控记录");//线程处于正常运行状态,被打断设置为true,不会抛出异常
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        thread.interrupt();//因为是阻塞状态被打断,所以打断标记为false,需要重置打段标记为ture,
                    }
                }
            }
        });
        monitor.start();

    }


    public void stop() {
        monitor.interrupt();
    }

}

19:03:07.655 [Thread-0] DEBUG c.TwoPhaseTermination - 执行监控记录
19:03:08.668 [Thread-0] DEBUG c.TwoPhaseTermination - 执行监控记录
19:03:09.689 [Thread-0] DEBUG c.TwoPhaseTermination - 执行监控记录
19:03:10.159 [Thread-0] DEBUG c.TwoPhaseTermination - 料理后事
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.text.TwoPhaseTermination$1.run(Test10.java:40)
	at java.lang.Thread.run(Thread.java:748)

isInterrupted()和Interrupted()的区别:Interrupted()也可以用来判断当前线程是否被打断,但同时会清除打断标记(ture置为fasle),前者不会清除打断标记。
测试:

isInterrupted:

@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {

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

        Thread t1 = new Thread(() -> {

            while (true) {
                Thread t = Thread.currentThread();
                if (t.isInterrupted()){//被打断,退出循环,终止线程
                    log.debug("被打断了,退出循环");
                    log.debug(" 打断状态: {}", t.isInterrupted());
                    break;
                }

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    t.interrupt();
                }

            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
       

    }
}

java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.text.TestInterrupt.lambda$main$0(TestInterrupt.java:21)
	at java.lang.Thread.run(Thread.java:748)
19:33:16.284 [t1] DEBUG c.TestInterrupt - 被打断了,退出循环
19:33:16.288 [t1] DEBUG c.TestInterrupt -  打断状态: true

interrupted:

@Slf4j(topic = "c.TestInterrupt")
public class TestInterrupt {

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

        Thread t1 = new Thread(() -> {

            while (true) {
                Thread t = Thread.currentThread();
                if (Thread.interrupted()){//被打断,退出循环,终止线程
                    log.debug("被打断了,退出循环");
                    log.debug(" 打断状态: {}", t.isInterrupted());
                    break;
                }

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    t.interrupt();
                }

            }
        }, "t1");
        t1.start();
        Thread.sleep(1000);
        t1.interrupt();


    }
}
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.text.TestInterrupt.lambda$main$0(TestInterrupt.java:21)
	at java.lang.Thread.run(Thread.java:748)
19:33:42.537 [t1] DEBUG c.TestInterrupt - 被打断了,退出循环
19:33:42.544 [t1] DEBUG c.TestInterrupt -  打断状态: false

注意:二者的调用都不一样。

打段park

看一下park是个啥:

@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();
    }
}
19:39:20.421 [t1] DEBUG c.TestInterruptPark - park...

t1 阻塞是在【 LockSupport.park();】

如果我们打断正处于park阻塞的线程

@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt();
    }
}
19:42:20.001 [t1] DEBUG c.TestInterruptPark - park...
19:42:20.006 [t1] DEBUG c.TestInterruptPark - unpark...
19:42:20.007 [t1] DEBUG c.TestInterruptPark - 打断状态:true

注意:如果打断标记已经是 true, 则 park 会失效

@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());

            LockSupport.park();
            log.debug("unpark...");

        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt();
    }
}

发现unpark…打印

19:43:09.919 [t1] DEBUG c.TestInterruptPark - park...
19:43:09.928 [t1] DEBUG c.TestInterruptPark - unpark...
19:43:09.928 [t1] DEBUG c.TestInterruptPark - 打断状态:true
19:43:09.932 [t1] DEBUG c.TestInterruptPark - unpark...

改进,把isInterrupted()改为interrupted(),因为后者会情况打断标记,把true置为fasle

@Slf4j(topic = "c.TestInterruptPark")
public class TestInterruptPark {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.interrupted());

            LockSupport.park();
            log.debug("unpark...");

        }, "t1");
        t1.start();
        Thread.sleep(1);
        t1.interrupt();
    }
}
19:45:08.262 [t1] DEBUG c.TestInterruptPark - park...
19:45:08.269 [t1] DEBUG c.TestInterruptPark - unpark...
19:45:08.269 [t1] DEBUG c.TestInterruptPark - 打断状态:true

不推荐的方法

在这里插入图片描述

六、主线程与守护线程

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

@Slf4j(topic = "c.TestDaemon")
public class TestDaemon {
    public static void main(String[] args) throws  Exception{
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("开始运行...");
            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("运行结束...");
        }, "daemon");

        t1.start();
        Thread.sleep(1000);
        log.debug("运行结束...");
    }
}

在这里插入图片描述
把t1线程设置为守护线程: t1.setDaemon(true)

@Slf4j(topic = "c.TestDaemon")
public class TestDaemon {
    public static void main(String[] args) throws  Exception{
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.debug("开始运行...");
            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("运行结束...");
        }, "daemon");
        //把t1线程设置为守护线程
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(1000);
        log.debug("运行结束...");
    }
}

在这里插入图片描述

七 、线程的状态

从操作系统层面来说,五种状态

在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

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

  • 【运行状态】指获取了 CPU 时间片运行中的状态

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

  • 【阻塞状态】

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

从 Java API 层面来描述,根据 Thread.State 枚举,分为六种状态

在这里插入图片描述

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
    【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为
    是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED 当线程代码运行结束
@Slf4j(topic = "c.TestState")
public class TestState {
    public static void main(String[] args) {
        Thread t1 = new Thread("t1"){
            @Override
            public void run() {
                log.debug("running...");
            }
        };

        Thread t2 = new Thread("t2"){
            @Override
            public void run() {
               while (true){

               }
            }
        };
        t2.start();

        Thread t3 = new Thread("t3"){
            @Override
            public void run() {
              
            }
        };
        t3.start();

        Thread t4 = new Thread("t4"){
            @Override
            public void run() {
                synchronized (TestState.class){
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t4.start();

        Thread t5 = new Thread("t5") {
            @Override
            public void run() {
                try {
                    t2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t5.start();

        Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized (TestState.class){
                    try {
                        Thread.sleep(1000000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t6.start();



        log.debug("t1 state {}",t1.getState());
        log.debug("t2 state {}",t2.getState());
        log.debug("t3 state {}",t3.getState());
        log.debug("t4 state {}",t4.getState());
        log.debug("t5 state {}",t5.getState());
        log.debug("t6 state {}",t6.getState());

    }
}

20:15:57.951 [main] DEBUG c.TestState - t1 state NEW
20:15:57.953 [t3] DEBUG c.TestState - running...
20:15:57.957 [main] DEBUG c.TestState - t2 state RUNNABLE
20:15:57.957 [main] DEBUG c.TestState - t3 state TERMINATED
20:15:57.957 [main] DEBUG c.TestState - t4 state TIMED_WAITING
20:15:57.957 [main] DEBUG c.TestState - t5 state WAITING
20:15:57.957 [main] DEBUG c.TestState - t6 state BLOCKED
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值