Java并发编程学习(1):线程的创建、启动、常用方法与线程的状态

概念

进程与线程

进程

  • 程序由指令和数据组成,当一个程序被运行,从磁盘加载这个程序的的代码到内存,这是就开启了一个进程
  • 进程可以视为程序的一个实例

线程

  • 一个进程之类可以分为一个或多个线程
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行

两者对比

  • Java中,线程作为最小的调度单位,进程作为资源分配的最小单位
  • 在Windows中进程是不活动的,只是作为线程的容器
  • 进程基本上相互独立,而线程存在于进程内,是进程的一个子集
  • 进程拥有共享的资源,如内存空间,供其内部的线程共享
  • 进程间通信较为复杂
    • 同一台计算机的进程通信成为IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如HTTP
  • 线程通信较为简单,因为它们共享进程的内存
  • 线程更轻量,线程上下文切换成本一般要比继承上下文切换低

并行与并发

并发(concurrent):线程轮流使用CPU(微观串行,宏观并发)
并发(parrel):多核CPU下,每个核都可以调度运行线程(微观并行)

同步与异步

同步与异步的概念是从方法调用的角度来讲的:

  • 同步:调用者需要等待返回结果
  • 异步:调用者不需要等待返回结果

栈与栈帧

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

线程上下文切换

以下原因会导致cpu不再执行当前线程,转而执行另一个线程

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

jvm保存当前当前线程状态的工具是程序计数器,它的作用是记住下一条jvm指令的执行地址,属于线程私有。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

频繁地上下文切换会影响性能,因此线程数并不是越多越好。

创建和启动线程

方法1

直接创建一个新的Thread类,并覆盖其run()方法

// 创建线程
Thread hello = new Thread(){
    @Override
    public void run() {
        log.debug("hello");
    }
};
hello.setName("hello");
// 启动线程
hello.start();
log.debug("hello");

日志打印结果如下

21-05-11 15:36:16.557 [DEBUG][hello] i.k.e.c.e.CreatThread : hello
21-05-11 15:36:16.557 [DEBUG][main] i.k.e.c.e.CreatThread : hello

方法2

先创建一个Runnable实例,在实例化Thread时将Runnable对象传入。
通过Runnable对象,可以将【任务】与【线程】分开

Runnable runnable = () -> log.debug("hello");
Thread hello = new Thread(runnable,"hello");
hello.start();
log.debug("hello");

日志打印结果如下

21-05-11 15:46:33.081 [DEBUG][main] i.k.e.c.e.CreatThread : hello
21-05-11 15:46:33.081 [DEBUG][hello] i.k.e.c.e.CreatThread : hello

方法2的原理

在创建Thread对象是,如果传入了Runnable对象,则会执行到Thread私有的构造方法private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals),其中参数target就是我们传入的Runnable对象,在该方法中有

this.target = target;

也就是说,我们传入的Runnable对象会与创建的Thread对象进行组合。
而在Thread的run()方法中,其逻辑如下,

public void run() {
   if (target != null) {
        target.run();
    }
}

因此,run()方法实际上会执行到我们传入的Runnable对象本身的run()方法。
在方法1中,我们会直接覆盖Thread的run()方法,让他执行我们期望的逻辑。

方法3

使用FutureTask创建任务对象,并且会得到任务的返回结果。创建FutureTask对象前需要先创建一个Callable对象,并且实现其中的call()方法。

FutureTask<Integer> task = new FutureTask<>(() -> {
    log.debug("future-task");
    Thread.sleep(1000);
    return 100;
});
Thread thread = new Thread(task,"task");
thread.start();
// 通过get方法得到返回结果
Integer i = task.get();
log.debug("ret: {}",i);

日志打印结果如下

21-05-11 15:48:52.822 [DEBUG][task] i.k.e.c.e.CreatThread : future-task
21-05-11 15:48:53.826 [DEBUG][main] i.k.e.c.e.CreatThread : ret: 100

方法3的原理

首先,FutureTask本身实现了Runnable接口,因此在创建Theard对象时,传入FutureTask对象会被视为传入了Runnable对象,根据上面的分析,线程启动最终会调用FutureTask的run()方法。
在FutureTask的run()方法中,其执行逻辑的核心代码如下

// 获取传入的Callable对象
Callable<V> c = callable;
if (c != null && state == NEW) {
    V result;
    boolean ran;
    try {
        // 执行Callable对象的call方法,并拿到结果
        result = c.call();
        // 标记call方法顺利执行
        ran = true;
    } catch (Throwable ex) {
        // 标记call方法抛出异常,并记录异常
        result = null;
        ran = false;
        setException(ex);
    }
    // 如果call方法顺利执行,则将结果保存到类中,方便后续取用
    if (ran)
        set(result);
}

在调用FutureTask的get()方法时,如果方法未执行结束,则会发生阻塞,如果程序运行运行结束,则会返回运行结果或者运行出现的异常。

线程的状态

五种状态(操作系统层面)

在这里插入图片描述

  • 初始状态:仅仅是语言层面创建了线程对象,还没有与操作系统关联
  • 可运行状态(就绪状态):指该线程已经被创建(与操作系统关联),可以由CPU调度执行
  • 运行状态:指获取了CPU时间片运行中的状态
    • 当CPU时间片用完,会从运行状态切换到可运行状态,会导致上下文切换
  • 阻塞状态
    • 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致上下文切换
    • 当BIO等阻塞API操作完毕,会由操作系统唤醒阻塞的线程,转换到可运行状态
  • 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

六种状态(Java层面)

六种状态是从Java API层面来描述的,根据Thread.State枚举类,分为六种状态。
在这里插入图片描述

  • NEW:线程刚刚被创建,但是还没有调用start()方法,对应操作系统的初始状态
  • RUNNABLE:当线程调用了start()方法后会进入RUNNABLE状态
    • Java API层面的RUNNABLE状态涵盖了操作系统层面的可运行状态运行状态阻塞状态
  • BLOCKEDWAITINGTIMED_WAITING是Java种的阻塞状态,也属于操作系统的阻塞状态
  • TERMINATED:终止状态,表示线程已经运行完毕,对应操作系统的终止状态

线程的常见方法

常用方法一览

方法名功能注意
start()启动一个新线程,在新的线程运行 run()方法中的代码start()方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start()方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
getState()获取线程状态
isAlive()线程是否存活(还没有运行完毕)
isInterrupted()判断是否被打断不会清除打断标记
interrupt()打断线程
static interrupted()判断当前线程是否被打断会清除打断标记
static currentThread()获取当前正在执行的线程
static sleep(long n)让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程
static yield()提示线程调度器让出当前线程对CPU的使用

sleep与yield

sleep

  1. 调用sleep()方法会将线程从RUNNABLE变成TIMED_WAITTING状态(阻塞)
  2. 其它线程可以使用interrupt()方法打断正在睡眠的方法,此时sleep()方法会抛出InterruptedException异常
  3. 睡眠结束后的线程未必就会立即执行
  4. 建议使用TimUnit的sleep()方法代替Thread的sleep()方法,以提供更好地可读性

yield

  1. 调用```yield()``方法会让出当前线程的使用权,线程状态从运行状态变成可运行状态
  2. 其具体的实现依赖于操作系统的任务调度器

setPriority

调用setPriority()方法会修改线程的优先级(范围为1~10,默认为5)。

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

对yield和setPriority进行代码测试

在测试程序中,我们创建了两个线程"t1"与"t2",这两个线程分别在死循环中打印输出各自的cnt变量,且每打印一次,cnt就自增。主线程在外部维持1秒后强制退出。

private static final String NORMAL_MODE = "normal";
private static final String YIELD_MODE = "yield";
private static final String PRIORITY_MODE = "priority";
private static void fun(String mode) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        int cnt = 0;
        while (true) {
            log.debug("1------>{}", cnt++);
        }
    }, "t1");
    Thread t2;
    if (YIELD_MODE.equalsIgnoreCase(mode)){
        t2 = new Thread(() -> {
            int cnt = 0;
            while (true) {
                Thread.yield();
                log.debug("          2------>{}", cnt++);
            }
        }, "t2");
    }else {
        t2 = new Thread(() -> {
            int cnt = 0;
            while (true) {
                log.debug("          2------>{}", cnt++);
            }
        }, "t2");
    }

    if (PRIORITY_MODE.equalsIgnoreCase(mode)){
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
    }

    t1.start();
    t2.start();
    TimeUnit.SECONDS.sleep(1);
    System.exit(1);
}

在不做干预的情况下,程序退出时,两个线程中的cnt应该是相近的。
根据函数参数modeYIELD_MODE时,线程"t2"会在打印前调用yield()方法让出cpu时间片;当modePRIORITY_MODE时,线程"t1"会被设定为高优先级,线程"t2"会被设定为低优先级。理论上说,以上两种方法都会使得线程"t1"获得的cpu时间片多余线程"t2",从而使得线程"t1"的cnt大于线程"t2"。

modeNORMAL_MODE,程序尾部的打印如下:

21-05-11 19:05:04.487 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>23032
21-05-11 19:05:04.487 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo :           2------>23520
21-05-11 19:05:04.487 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>23033
21-05-11 19:05:04.487 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo :           2------>23521

此时,线程"t1"的cnt为23033,线程"t2"的cnt为23521,两者较为接近。

modeYIELD_MODE,程序尾部的打印如下:

21-05-11 19:24:24.319 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo :           2------>11955
21-05-11 19:24:24.320 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>21168
21-05-11 19:24:24.320 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>21169

此时,线程"t1"的cnt为21169,线程"t2"的cnt为11955,线程"t1"的cnt明显大于线程"t2"。

modePRIORITY_MODE,程序尾部的打印如下:

21-05-11 19:25:26.042 [DEBUG][t2] i.k.e.c.e.YieldAndPriorityDemo :           2------>15346
21-05-11 19:25:26.043 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>25420
21-05-11 19:25:26.043 [DEBUG][t1] i.k.e.c.e.YieldAndPriorityDemo : 1------>25421

此时,线程"t1"的cnt为21169,线程"t2"的cnt为11955,线程"t1"的cnt明显大于线程"t2"。

join

当一个线程运行时,如果调用其join()方法,则会等待该线程运行结束。
通过join()方法,可以实现线程之间的同步。
join()方法可以传入一个long类型的参数表示最长等待时间。

代码示例

测试代码的主函数中创建了一个线程"t1",该线程会在启动后1秒修改静态变量r的值。
在主函数中启动线程"t1"后会立即打印静态变量r的值。

private static int r = 0;
public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("线程醒来");
        r = 10;
    }, "t1");
    t1.start();
    log.info("r = {}",r);
}

打印日志如下

21-05-11 20:31:14.146 [INFO][main] i.k.e.c.ex.JoinDemo : r = 0
21-05-11 20:31:15.145 [INFO][t1] i.k.e.c.ex.JoinDemo : 线程醒来

可以发现,主线程打印时,静态变量r的值仍然为0,因为此时线程"t1"还没有醒来,也就没有改变r的值。
下面的测试代码中,我在主函数打印静态变量r的值之前,加上了t1.join(),此时主函数会首先等待线程"t1"执行完成,然后打印数值。

private static int r = 0;
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("线程醒来");
        r = 10;
    }, "t1");
    t1.start();
    t1.join();
    log.info("r = {}",r);
}

打印日志如下

21-05-11 20:45:55.792 [INFO][t1] i.k.e.c.ex.JoinDemo : 线程醒来
21-05-11 20:45:55.795 [INFO][main] i.k.e.c.ex.JoinDemo : r = 10

interrupt

调用interrupt()方法,既可以打断处于阻塞状态的线程,也可以打断正在运行的线程。

打断阻塞状态的线程

打断处于阻塞状态的线程时,该线程会抛出异常,同时清除打断标记。

示例代码如下

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.info("线程启动");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            log.error(e.getMessage());
        }
        log.info("线程醒来");
    }, "t1");
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    log.info("打断开始");
    t1.interrupt();
    log.info("t1的打断标记:{}",t1.isInterrupted());
}

日志如下

21-05-11 21:01:50.709 [INFO][t1] i.k.e.c.e.InterruptWaitingDemo : 线程启动
21-05-11 21:01:51.707 [INFO][main] i.k.e.c.e.InterruptWaitingDemo : 打断开始
21-05-11 21:01:51.707 [ERROR][t1] i.k.e.c.e.InterruptWaitingDemo : sleep interrupted
21-05-11 21:01:51.707 [INFO][t1] i.k.e.c.e.InterruptWaitingDemo : 线程醒来
21-05-11 21:01:51.707 [INFO][main] i.k.e.c.e.InterruptWaitingDemo : t1的打断标记:false

打断正常运行的线程

当其它线程调用正常运行的interrupt()方法时,线程不会诊断被打断,而是依然正常运行,但此时它的打断标记会被置为true,线程可以通过判断它的打断标记来观察外部是否有线程在打断它。

示例代码如下
在下面的示例代码中,主线程调用interrupt()方法预打断正在执行的线程"t1",但是线程"t1"依然能够运行完毕。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.info("线程启动");
        long  i = 0;
        while (i < Integer.MAX_VALUE){
            i ++;
        }
        log.info("线程结束:{}",i);
    }, "t1");
    t1.start();
    log.info("打断开始");
    t1.interrupt();
    log.info("t1的打断标记:{}",t1.isInterrupted());
}

日志如下

21-05-11 21:20:30.758 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程启动
21-05-11 21:20:30.857 [INFO][main] i.k.e.c.e.InterruptRunningDemo : 打断开始
21-05-11 21:20:30.857 [INFO][main] i.k.e.c.e.InterruptRunningDemo : t1的打断标记:true
21-05-11 21:20:31.353 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程结束:2147483647

而在下面的代码中,线程"t1"会检查自己的打断标记,如果打断标记为真,则会立即退出循环

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        log.info("线程启动");
        long  i = 0;
        while (i < Integer.MAX_VALUE){
            if (Thread.currentThread().isInterrupted()){
                break;
            }
            i ++;
        }
        log.info("线程结束:{}",i);
    }, "t1");
    t1.start();
    TimeUnit.MILLISECONDS.sleep(100);
    log.info("打断开始");
    t1.interrupt();
    log.info("t1的打断标记:{}",t1.isInterrupted());
}

日志如下

21-05-11 21:23:36.782 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程启动
21-05-11 21:23:36.881 [INFO][main] i.k.e.c.e.InterruptRunningDemo : 打断开始
21-05-11 21:23:36.881 [INFO][main] i.k.e.c.e.InterruptRunningDemo : t1的打断标记:true
21-05-11 21:23:36.881 [INFO][t1] i.k.e.c.e.InterruptRunningDemo : 线程结束:104598873

打断park线程

当线程进入park状态时,如果其它线程调用了该线程的interrupt()方法,则会使线程解除park状态并继续运行。此时该线程的打断标记为真,之后的park()方法在打断标记为真时会失效。可以通过interrupted()方法清除打断标记。

两阶段中止模式

针对的问题:如果在一个线程"t1"中【优雅】地中止线程"t2"?【优雅】指的是给线程"t2"一个料理后事的机会。

错误的做法
  1. 调用stop()方法:stop()方法会真正厦时线程,如果线程运行中锁住了共享资源,那么它被杀死后就再也没有机会释放锁(并且stop()方法已经被列为废弃方法)
  2. 调用System.exit()方法:会停止整个进程
正确的做法
Created with Raphaël 2.2.0 开始 while(true) 是否被打断? 执行结束逻辑 结束 执行应用逻辑 yes no

注:如果在应用逻辑中存在阻塞逻辑,则需要在阻塞逻辑被打断时设置打断标记。

代码示例

public class TwoPhaseTermination {
    public static class App{
        Thread app;
        public void start(){
            app = new Thread(() -> {
                while (true) {
                    Thread current = Thread.currentThread();
                    if (current.isInterrupted()) {
                        log.debug("执行退出逻辑");
                        break;
                    }
                    try {
                        log.debug("执行任务(上)");
                        log.debug("陷入阻塞");
                        TimeUnit.MILLISECONDS.sleep(400);
                        log.debug("执行任务(下)");
                    } catch (InterruptedException e) {
                        log.warn("阻塞时被打断");
                        current.interrupt();
                    }
                }
            }, "app");
            app.start();
        }

        public void stop(){
            app.interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        App app = new App();
        app.start();
        TimeUnit.SECONDS.sleep(1);
        app.stop();
    }
}

运行日志如下

21-05-11 21:56:28.529 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(上)
21-05-11 21:56:28.531 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 陷入阻塞
21-05-11 21:56:28.933 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(下)
21-05-11 21:56:28.933 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(上)
21-05-11 21:56:28.933 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 陷入阻塞
21-05-11 21:56:29.333 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(下)
21-05-11 21:56:29.333 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行任务(上)
21-05-11 21:56:29.333 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 陷入阻塞
21-05-11 21:56:29.528 [WARN][app] i.k.e.c.p.TwoPhaseTermination$App : 阻塞时被打断
21-05-11 21:56:29.528 [DEBUG][app] i.k.e.c.p.TwoPhaseTermination$App : 执行退出逻辑

不推荐使用的方法

这些方法已经过时,容易破坏同步代码块,造成线程死锁

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

守护线程

Java的线程可以分为守护线程和非守护线程。顾名思义,守护线程是用来保护其他线程执行的。当Java进程中只有守护进程时,即使守护线程的代码没有执行完,也会强制结束。
主函数线程为非守护线程。我们代码中创建的线程默认也为非守护线程。除此之外的线程(例如垃圾回收线程)均为守护线程。
通过setDaemon()方法可以设置该线程是否为守护线程。

代码示例
代码中,线程"t1"被设置为了守护线程,它本身的逻辑为被打断时才会退出循环,结束自身。但是在主线程中并没有打断线程"t1",而是在睡眠1秒后直接退出。

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
        }
        log.info("结束");
    }, "t1");
    t1.setDaemon(true);
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    log.info("结束");
}

打印日志如下

21-05-12 15:28:24.860 [INFO][main] i.k.e.c.e.DaemonDemo : 结束

程序在启动1秒后直接退出,说明守护线程"t1"被强制中止。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值