Java并发编程基础--3Java线程

Java并发编程基础--3Java线程

  • 3 Java线程
    • 3.1 进程创建的三种方式
      • 方法1:直接使用Thread
      • 方法2:Rnnable配合Thread
        • Thread与Rnnable的关系
        • lambda表达式
      • 方法3:FutureTask配合Thread
    • 3.2 观察多个线程同时运行
      • windows
      • linux
      • Java
    • 3.3 线程运行原理
      • 栈与栈帧
      • 线程上下文切换(Thread Content Switch)
    • 3.5 常见方法
    • 3.6 start与run
    • 3.7 sleep与yield
      • sleep
      • yield
      • 线程优先级
      • sleep防止线程CPU占用100%
    • 3.8 join方法
    • 3.9 interrupt方法详解
      • 打断sleep、wait、jion的线程
      • 打断正常运行的进程
      • ***模式之两阶段终止**
      • 打断park方法
    • 3.10 不推荐的方法
    • 3.11 守护线程
    • 3.12 线程的五种状态
    • 线程的六种状态
    • 本章小结

注:本系列仅供个人学习使用,原视频:https://www.bilibili.com/video/BV16J411h7Rd?p=49

3 Java线程

3.1 进程创建的三种方式

方法1:直接使用Thread

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

方法2:Rnnable配合Thread

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

Thread与Rnnable的关系

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

lambda表达式

// 创建任务对象
Runnable task2 = () -> log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

:仅有单个方法的类的可以使用lambda表达式。

方法3: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);

3.2 观察多个线程同时运行

windows

  • 任务管理器
  • tasklist显示进程
  • taskkill杀死进程

linux

  • ps -fg查看所有进程
  • ps -fT -p 查看某个进程的所有线程
  • kill杀死进程
  • top按大写H切换是否显示线程
  • top -H -p 查看某个进程(PID)的所有线程状态

Java

  • jps命令查看所有Java进程
  • jstack 插卡韩某个Java进程(PID)的所有线程状态
  • jconsole来查看某个Java进程中线程的运行状况(图形界面),可远程查看。

3.3 线程运行原理

栈与栈帧

个人理解:进程与方法

JVM中由堆、栈、方法区组成。其中栈内存是为线程准备的。每当线程启动后,虚拟机就会为其分配一块栈内存。栈内存主要存放字节码(即程序指令)。

  • 每个栈有多个栈帧组成,对应着每次方法调用所占的内存
  • 每个线程只能有一个活动帧,对应着当前正在执行的那个方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ZTFuP0A-1590490310478)(C:\Users\dnm\AppData\Roaming\Typora\typora-user-images\image-20200526140248221.png)]

线程上下文切换(Thread Content Switch)

有以下原因导致线程切换:

  • 线程时间片用完
  • 有更高优先级的线程
  • JVM垃圾回收(会暂停所有先线程)
  • 主动:线程调用了sleep、yield、wait、join、park、synchronized、lock等方法

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

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

3.5 常见方法

方法名 static 功能说明 注意
start() 启动新县城,在新的线程中运行run方法 start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run() 新线程启动后会调用新的方法 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程结束,最多等待n秒
getId() 获取线程id id唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判断时候被打断 不会清除打断标记
isAlive() 线程是否运行完毕
interrupt() 打断线程 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记
interrupted() 判断当前线程是否被打断 会清除打断标记
currentThread() 获取当前正在执行的线程
yield() 提示调度器让当前线程让出CPU的使用 主要为了测试和调试
sleep(long n) 让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程

3.6 start与run

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

3.7 sleep与yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  3. 睡眠结束后的线程未必会立刻得到执行
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器

线程优先级

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

sleep防止线程CPU占用100%

3.8 join方法

等待其它线程执行结束后在执行当前线程。

3.9 interrupt方法详解

打断sleep、wait、jion的线程

这几个方法会让线程进入阻塞状态,打断sleep线程会清空打断状态

private static void test1() throws InterruptedException {
 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

打断正常运行的进程

private static void test2() throws 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

*模式之两阶段终止

如何在一个线程中优雅的终止另一个线程(允许另一个线程处理“后事”)。

无异常
有异常
while true
有没有被打断
料理后事
结束循环
睡眠2s
执行监控记录
执行监控记录
设置打断标记
package cn.itcast.pattern;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestTwoPhaseTermination")
public class TestTwoPhaseTermination {
    public static void main(String[] args) throws InterruptedException {
        TPTVolatile t = new TPTVolatile();
        t.start();

        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}
@Slf4j(topic = "c.TPTInterrupt")
class TPTInterrupt {
    private Thread thread;

    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) {
                    current.interrupt();
                }

            }
        },"监控线程");
        thread.start();
    }

    public void stop() {
        thread.interrupt();
    }
}
@Slf4j(topic = "c.TPTVolatile")
class TPTVolatile {
    private Thread thread;
    private volatile boolean stop = false;

    public void start(){
        thread = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    log.debug("将结果保存");
                } catch (InterruptedException e) {
                }
            }
        },"监控线程");
        thread.start();
    }

    public void stop() {
        stop = true;
        thread.interrupt();
    }
}
  • 设置打断标记的意义在于,防止线程在睡眠时被打断,打断标记字被清空。
  • 当在sleep结束后被打断,此时不会触发异常,但是此时打断标记字为True,在下次循环中依然会执行收尾工作。

打断park方法

park方法,是JUC.locks.LockSupport中的静态方法。该方法具有打断即失效的特性。

private static void test3() throws InterruptedException {
 Thread t1 = new Thread(() -> {
 log.debug("park...");
 LockSupport.park();
 log.debug("unpark...");
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }, "t1");
 t1.start();
 sleep(0.5);
 t1.interrupt();
}
21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark... 
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true

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

private static void test4() {
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 5; i++) {
 log.debug("park...");
 LockSupport.park();
 log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
 }
 });
 t1.start();
 sleep(1);
 t1.interrupt();
}
21:13:48.783 [Thread-0] c.TestInterrupt - park... 
21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.812 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true 
21:13:49.813 [Thread-0] c.TestInterrupt - park... 
21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态:true

但是可以通过调用Thread.interruped()方法来清除打断标记。

3.10 不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。

方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

3.11 守护线程

默认情况下,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 - 运行结束...

可以调用setDaemon方法,将某线程设置为守护线程。

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

3.12 线程的五种状态

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

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

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

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

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

线程的六种状态

从Java层面描述,线程具有六种状态,分别使用枚举类型表示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnEQY0L4-1590490310483)(C:\Users\dnm\AppData\Roaming\Typora\typora-user-images\image-20200526164532614.png)]

  • NEW 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  • TERMINATED 当线程代码运行结束

示例:

  • BLOCKED :获取锁的等待情况
  • WAITING :调用join()方法,等待另一个线程结束
  • TIMED_WAITING :带时限的阻塞,Thread.sleep

本章小结

本章重点在于

  • 线程创建
  • 重要api,start、run、sleep、join、interrupt等
  • 线程状态
  • 应用
    • 异步调用
    • 提高效率:多处理器
    • 同步等待join
    • 统筹规划:合理使用线程
  • 原理方面
    • 线程运行流程:栈、栈帧、上下文切换、程序计数器
    • Thread两种创建方式的源码
  • 模式方面
    • 终止模式之两阶段终止。

你可能感兴趣的:(Java并发编程基础)