java并发编程 2:java线程基础知识

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

创建和运行线程

方法一: 直接使用 Thread

public class CreateThread01 {

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
              	// run 方法内实现了要执行的任务
                System.out.println("线程执行啦");
            }
        };
        // 设置名称
        thread.setName("线程1");
        // 运行
        thread.start();

        System.out.println("主线程结束");
    }
}

方法二使用 Runnable配合 Thread

public class CreateThread02 {

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("线程执行啦");
            }
        };

        // 创建线程
        Thread thread = new Thread(runnable, "线程2");
        // 执行
        thread.start();

        System.out.println("主线程结束");
    }
}

Java 8 以后可以使用 lambda 精简代码:

public class CreateThread02 {

    public static void main(String[] args) {
        // 创建线程
        Thread thread = new Thread(() -> System.out.println("线程执行啦"), "线程2");
        // 执行
        thread.start();

        System.out.println("主线程结束");
    }

这种方法把线程和任务分开了用 Runnable 更容易与线程池等高级 API 配合。

方法三FutureTask配合Thread

FutureTask 能够接收 Callable 类型的参数主要用来处理有返回结果的情况后面还会详细讲解。

示例

public class CreateThread03 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建FutureTask对象
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("线程开始啦");
                // 暂停1秒
                Thread.sleep(1000);
                return 100;
            }
        });


        // 创建线程
        Thread thread = new Thread(task, "线程1");
        // 执行
        thread.run();

        // 获取线程的结果会等到线程结果返回才执行
        System.out.println(task.get());

        System.out.println("主线程结束");
    }
}

也可以使用lambda表达式简化

public class CreateThread03 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建FutureTask对象
        FutureTask<Integer> task = new FutureTask<>(() -> {
            System.out.println("线程开始啦");
            // 暂停1秒
            Thread.sleep(1000);
            return 100;
        });


        // 创建线程
        new Thread(task, "线程1").start();

        // 获取线程的结果会等到线程结果返回才执行
        System.out.println(task.get());

        System.out.println("主线程结束");
    }
}

查看进程线程

linux

ps -fe 查看所有进程

ps -fT -p 查看某个进程PID的所有线程

kill 杀死进程

top 按大写 H 切换是否显示线程

top -H -p 查看某个进程PID的所有线程

Java

jps 命令查看所有 Java 进程

jstack 查看某个 Java 进程PID的所有线程状态

jconsole 来查看某个 Java 进程中线程的运行情况图形界面

线程运行原理

栈与栈帧

Java Virtual Machine Stacks Java 虚拟机栈

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

每个栈由多个栈帧Frame组成对应着每次方法调用时所占用的内存

每个线程只能有一个活动栈帧对应着当前正在执行的那个方法

线程上下文切换Thread Context Switch

有时候线程并不是可以一直执行的有一些原因会导致 cpu 不再执行当前的线程转而执行另一个线程的代码

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

当发生线程切换时并不是直接换另一个线程运行那么简单。为了下次再运行相同的线程就需要上下文切换也就是需要由操作系统保存当前线程的状态并恢复另一个线程的状态。Java 中对应的概念就是程序计数器Program Counter Register它的作用是记住下一条 jvm 指令的执行地址是线程私有的状态包括程序计数器、虚拟机栈中每个栈帧的信息如局部变量、操作数栈、返回地址等。

需要知道的是线程上下文切换太频繁发生会影响程序性能。

常见方法了解

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

start与run

调用run

示例

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CreateThread01 {

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                log.info("子线程执行啦");
            }
        };
        thread.setName("线程1");
        thread.run();

        log.info("主线程结束");
    }
}

结果如下

2023-04-16 15:59:16,712 - 0    INFO  [main] up.cys.chapter01.CreateThread01$1:18  - 子线程执行啦
2023-04-16 15:59:16,719 - 7    INFO  [main] up.cys.chapter01.CreateThread01:27  - 主线程结束

根据打印的日志看到"子线程执行啦"虽然打印出来了但是其执行是在[main]线程中。

调用start

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CreateThread01 {

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                log.info("子线程执行啦");
            }
        };
        thread.setName("线程1");
        thread.start();

        log.info("主线程结束");
    }
}

打印如下

2023-04-16 16:01:53,905 - 0    INFO  [main] up.cys.chapter01.CreateThread01:27  - 主线程结束
2023-04-16 16:01:53,905 - 0    INFO  [线程1] up.cys.chapter01.CreateThread01$1:18  - 子线程执行啦

根据打印的日志看到"子线程执行啦"是在[线程1]中执行的这才是真的开启开启一个线程了。

结论

  • 直接调用 run 是在主线程中直接执行了 run方法会把run 方法当成一个main 线程下的普通方法去执行没有启动新的线程
  • 使用 start 会启动一个线程并使线程进入了就绪状态当分配到 时间片 后就可以开始运行了

一般开发人员使用start来启动线程。

sleep与yield

sleep

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

yield

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

线程优先级

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

join

join会使当前线程等待指定线程运行结束才继续向下运行。

前面我们使用过sleep来让主线程等待一定时间如下

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class JoinTest01 {
    static int r = 0;

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

    private static void test1() throws InterruptedException {
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("结束");
            r = 10;
        });
        t1.start();

        // 这里的等待2秒时为了等子线运行完程这样拿到的r值才是被修改后的
        TimeUnit.SECONDS.sleep(2);
        log.info("结果为:{}", r);
        log.info("结束");
    }
}

但是这样有个坏处就是我们不知道线程需要等多久。这时候就需要使用join

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class JoinTest01 {
    static int r = 0;

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

    private static void test1() throws InterruptedException {
        log.info("开始");
        Thread t1 = new Thread(() -> {
            log.info("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("结束");
            r = 10;
        });
        t1.start();
        // 使用join让主线程等待子线程结束
        t1.join();

        log.info("结果为:{}", r);
        log.info("结束");
    }
}

注意用 join需加在 t1.start() 之后。

join还可以带一个参数表示只等待多久这里就不在演示。

interrupt

此方法用来打断线程。但是打断不同状态的线程效果是不一样的。

1.打断sleepwaitjoin的线程

这几个方法都会让线程进入阻塞状态。打断 sleep 的线程, 会清空打断状态以 sleep 为例。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        Thread t1 = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
        log.info(" 打断状态: {}", t1.isInterrupted());
    }
}

输出

java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at java.base/java.lang.Thread.sleep(Thread.java:339)
	at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
	at up.cys.chapter01.InterruptTest.lambda$test1$0(InterruptTest.java:21)
	at java.base/java.lang.Thread.run(Thread.java:834)
2023-04-16 17:06:30,827 - 0    INFO  [main] up.cys.chapter01.InterruptTest:29  -  打断状态: false

2.打断正常运行的线程

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

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class InterruptTest02 {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        Thread t2 = new Thread(()->{
            while(true) {
                // 获取打断标记
                Thread current = Thread.currentThread();
                boolean interrupted = current.isInterrupted();
                // 如果标记为true停止
                if(interrupted) {
                    log.info(" 打断状态: {}", interrupted);
                    break;
                }
            }
        }, "t2");
        t2.start();
        Thread.sleep(500);
        // 主线程打断子线程注意并不会真正的打断自线程还是会正常运行需要自线程获取标记自己决定是否中断
        log.info("打断");
        t2.interrupt();
    }
}

输出如下

2023-04-16 17:20:48,575 - 0    INFO  [main] up.cys.chapter01.InterruptTest02:34  - 打断
2023-04-16 17:20:48,588 - 13   INFO  [t2] up.cys.chapter01.InterruptTest02:26  -  打断状态: true

不推荐使用的方法

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

stop() 、suspend()、resume()

主线程与守护线程

默认情况下Java 进程需要等待所有线程都运行结束才会结束。有一种特殊的线程叫做守护线程只要其它非守

护线程运行结束了即使守护线程的代码没有执行完也会强制结束。

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class DaemonTest {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug("开始运行...");
        Thread t1 = new Thread(() -> {
            log.info("开始运行...");
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("运行结束...");
        }, "daemon");
        // 设置该线程为守护线程
        t1.setDaemon(true);
        t1.start();
        Thread.sleep(1);
        log.info("运行结束...");
    }
}

输出如下

2023-04-17 00:09:48,359 - 0    INFO  [daemon] up.cys.chapter01.DaemonTest:19  - 开始运行...
2023-04-17 00:09:48,359 - 0    INFO  [main] up.cys.chapter01.DaemonTest:31  - 运行结束...

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

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

线程状态

操作系统中的线程状态

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

在这里插入图片描述

  • 【初始状态】仅是在语言层面创建了线程对象还未与操作系统线程关联
  • 【可运行状态】就绪状态指该线程已经被创建与操作系统线程关联可以由 CPU 调度执行
  • 【运行状态】指获取了 CPU 时间片运行中的状态当 CPU 时间片用完会从【运行状态】转换至【可运行状态】会导致线程的上下文切换
  • 【阻塞状态】如果调用了阻塞 API如 BIO 读写文件这时该线程实际不会用到 CPU会导致线程上下文切换进入阻塞状态等 BIO 操作完毕会由操作系统唤醒阻塞的线程转换至【可运行状态】与【可运行状态】的区别是对【阻塞状态】的线程来说只要它们一直不唤醒调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕生命周期已经结束不会再转换为其它状态

java中的线程状态

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

在这里插入图片描述

  • NEW 线程刚被创建但是还没有调用 start() 方法
  • RUNNABLE 当调用了 start() 方法之后注意Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的可运行状态】、【运行状态】和【阻塞状态】由于 BIO 导致的线程阻塞在 Java 里无法区分仍然认为是可运行
  • BLOCKED WAITING TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
  • TERMINATED 当线程代码运行结束
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Java