Java并发编程

时间:2022-07-22
本文章向大家介绍Java并发编程,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

进程与线程

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指定运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就启动了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士等)。

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行(最终执行指令的还是进程)。
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器。

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。
  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享。
  • 进程间通信较为复杂
    • 同一台计算机的进程通信称为 IPC (Inter-process communication)。
    • 不同计算机直接的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低。

并行与并发

单核CPU下,线程实际还是 串行执行 的。操作系统中有一个组件叫任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的 。总结一句话: 微观串行,宏观并行

一般会将这种 线程轮流使用 CPU 的做法称为并发(concurrent)。

引用 Rob Pike 的一段描述:

  • 并发(concurrent)是同一时间对应(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

生活例子:

  • 家庭主妇做饭、打扫卫生、洗衣服,她一个人轮流交替做多件事,这时就是并发
  • 家庭主妇雇了一个保姆,她们一起做这些事,这时既有并发、也有并行(这时会产生竞争,例如洗衣机只有一台,一个人用洗衣机时,另一个人只能等待)
  • 家庭妇女雇了三个保姆,一个专门做饭、一个专门打扫卫生、一个专门洗衣服,互不干扰,这时就是并行

应用

应用之异步调用

以调用方角度来讲,如果

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

设计

多线程可以让方法执行变为异步的。

比如说读取磁盘文件时,假设读取操作花费了5秒钟,如果没有线程调度机制,这5秒钟CPU什么都做不了,其他代码只能等待。

简单异步例子:

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

t1.setName("t1");
t1.start();

log.debug("main");

运行结果

13:51:29.131 [main] DEBUG io.ray.threadstudy.test.Test01 - main
13:51:30.130 [t1] DEBUG io.ray.threadstudy.test.Test01 - t1

结果说明

main 线程不需要等待 t1 线程执行完才输出,此时程序是异步进行的。

结论

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat 的工作线程
  • ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

应用之提高效率

充分利用多核 CPU 的优势,提高运行效率。想象下面的场景,执行3个计算,最后将计算结果汇总

计算 1 花费 10ms
计算 2 花费 11ms
计算 3 花费 9ms
汇总需要 1ms
  • 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
  • 如果是四核CPU,各个核心分别执行计算,那么此时的线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms

注意: 需要多核 CPU 才能提高效率,单核仍然是轮流执行

结论

  1. 单核 CPU 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 CPU,不至于一个线程总占用CPU,别的线程无法干活。
  2. 多核 CPU 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
    • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考【阿姆达尔定律】)
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没意义
  3. IO 操作不占用 CPU,只是我们一般拷贝文件使用的是【阻塞IO】,这时相当于线程虽然不同 CPU,但需要一直等待IO结束,没能充分利用线程。所以才有了后面的【非阻塞IO】和【异步IO】优化

Java 线程

创建和运行线程

每个Java程序启动的时候,默认都创建了一个主线程(main方法),如果想在主线程外创建线程,可以使用如下方法。

方法一:直接使用Thread

@Slf4j(topic = "io.ray")
public class Test02 {

    public static void main(String[] args) {

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

        // 启动线程
        t1.start();

        log.debug("main running");
    }
}

输出

13:27:35.538 [main] DEBUG io.ray - main running
13:27:35.542 [t1] DEBUG io.ray - t1 running

方法二:使用 Runnable 配合 Thread

把【Thread 线程】 和 【Runnable 任务】分开,更为灵活,推荐这种写法。

@Slf4j(topic = "io.ray")
public class Test03 {

    public static void main(String[] args) {

        // 创建任务对象
        Runnable r1 = new Runnable() {
            // run 方法内部实现了要执行的任务
            @Override
            public void run() {
                // 要执行的任务
                log.debug("r1 running");
            }
        };

        // 创建线程对象
        Thread t1 = new Thread(r1, "r1");
        // 启动线程
        t1.start();

        log.debug("main running");
    }
}

结果

13:37:07.419 [main] DEBUG io.ray - main running
13:37:07.419 [r1] DEBUG io.ray - r1 running

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

// ------- lambda - 1
Runnable r2 = () -> { log.debug("r2 running"); };
Thread t2 = new Thread(r2, "r2");
t2.start();

// ------- lambda - 2
Thread t3 = new Thread(() -> { log.debug("r3 running"); }, "r3");
t3.start();

源码分析

当创建 Thread 指定参数 Runnable时,会调用其 init 方法

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

// 继续往下寻找

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

// 继续往下寻找

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        // 将参数的runnable对象赋给了成员变量target
        this.target = target;
        setPriority(priority);
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

    // 可以看到,target 成员变量实际在run方法中用到
    @Override
    public void run() {
        // 如果 target 不为空的时候,调用的是 target 中的 run 方法
        // 反之,调用的是自己重写的 run 方法
        if (target != null) {
            target.run();
        }
    }

小结

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

方法三: 使用 FutureTask 配合 Thread

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

@Slf4j(topic = "io.ray")
public class Test04 {

    public static void main(String[] args) {

        // 创建任务对象
        FutureTask<Integer> task3 = new FutureTask<>(() -> {
            log.debug("hello");
            return 100;
        });

        // 参数1 是任务对象;参数2 是线程名称,推荐
        new Thread(task3, "t3").start();

        try {
            // 主线程阻塞,同步等待 task 执行完毕的结果
            Integer result = task3.get();
            log.debug("结果是:[{}]", result);

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

结果

13:47:22.044 [t3] DEBUG io.ray - hello
13:47:22.049 [main] DEBUG io.ray - 结果是:[100]

原理之线程运行

栈和栈帧

Java Virtual Machine Stacks (Java虚拟机栈)

JVM 由堆、栈、方法区所组成,其中栈内存是给谁用的呢?

其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

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

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

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

  1. 线程的CPU时间片用完
  2. 垃圾回收
  3. 有更高优先级的线程需要运行
  4. 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock等方法

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

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

常见方法

方法名

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 API 中线程状态是用 6个 enum 表示,分别是:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED

isInterrupted()

判断是否被打断

不会清除打断标记

isAlive()

线程是否存活(还没执行完毕)

interrupt()

打断线程

如果被打断的线程正在 sleep、wait、join 会导致被打断的线程抛出 InterruptedException ,并清除打断标记;如果打断的是正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记。

interrupted()

static

判断当前线程是否被打断

会清除打断标记

currentThread()

static

获取当前正在执行的线程

sleep(long n)

static

让当前执行的线程休眠 n 毫秒,休眠时会让出 CPU 的时间片给其他线程

yield()

static

提示线程调度器让出当前线程对 CPU 的使用

主要是为了测试和调试

start 与 run

  1. 直接调用 run 是在主线程中执行了 run,没有启动新的线程
  2. 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
@Slf4j
public class Test05 {

    public static void main(String[] args) {

        Test05 test05 = new Test05();
        test05.testRun();
        test05.testStart();

    }

    /**
     * @Description: 调用run
     * 方法的执行还是在 main 线程
     **/
    public void testRun() {

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

        t1.run();
        log.debug("do other things ..");
    };


    /**
     * @Description: 调用start
     * 方法的执行在 t2 线程
     **/
    public void testStart() {

        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t2.start();
        log.debug("do other things ..");
    };

}

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 闲时,优先级几乎没有作用
@Slf4j
public class Test06 {

    public static void main(String[] args) {

        Runnable task1 = new Runnable() {
            int count = 0;

            @Override
            public void run() {
                for (;;) {
                    log.debug(" ----> 1 {}", count++);
                }
            }
        };

        Runnable task2 = new Runnable() {
            int count = 0;

            @Override
            public void run() {
                for (; ; ) {
                    log.debug(" ----> 2 {}", count++);
                }
            }
        };

        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");

        // 在启动前设置优先级(可选,默认 NORM_PRIORITY = 5)
        t1.setPriority(Thread.MIN_PRIORITY);
        t2.setPriority(Thread.MAX_PRIORITY);

        t1.start();
        t2.start();
    }
}

join 方法详解

为什么需要 join?

join 的作用: 等待线程运行结束

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

@Slf4j(topic = "io.ray")
public class Test07 {

    static int r = 0;

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

    private static void test() throws InterruptedException {
        log.debug("开始 - main");
        Thread t1 = new Thread(() -> {
            log.debug("开始 - t1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("结束 - t1");
            r = 10;
        }, "t1");

        t1.start();

        // 使用 join,main 线程进入阻塞状态,等待 t1 线程的结束
        //t1.join();

        log.debug("r 的结果:[{}]", r);
        log.debug("结束 - main");
    }
}

结果(没使用 join)

07:38:27.364 [main] DEBUG io.ray - 开始 - main
07:38:27.531 [main] DEBUG io.ray - r 的结果:[0]
07:38:27.537 [main] DEBUG io.ray - 结束 - main
07:38:27.541 [t1] DEBUG io.ray - 开始 - t1
07:38:28.544 [t1] DEBUG io.ray - 结束 - t1

结果(使用 join)

07:38:57.085 [main] DEBUG io.ray - 开始 - main
07:38:57.205 [t1] DEBUG io.ray - 开始 - t1
07:38:58.205 [t1] DEBUG io.ray - 结束 - t1
07:38:58.205 [main] DEBUG io.ray - r 的结果:[10]
07:38:58.208 [main] DEBUG io.ray - 结束 - main

分析

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

用 sleep 行不行 ?

解答:用 sleep 也可以,但时间不好把握,不知道 t1 线程要运行多久。

等待多个结果

@Slf4j(topic = "io.ray")
public class Test08 {

    static int r1 = 0;
    static int r2 = 0;

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

    private static void test() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
                r1 = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
                r2 = 20;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");

        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);
    }
}

结果

08:01:25.764 [main] DEBUG io.ray - join begin
08:01:26.763 [main] DEBUG io.ray - t1 join end
08:01:27.762 [main] DEBUG io.ray - t2 join end
08:01:27.762 [main] DEBUG io.ray - r1: 10  r2: 20  cost: 2000

分析执行流程

  1. 25秒的时候,join 开始
  2. 26秒的时候,t1 线程的 join 结束,等待了1秒
  3. 27秒的时候,t2 线程的 join 结束,等待了2秒,实际上真的等待2秒吗?结果是不需要的,因为 t1 、t2 线程是同时启动的,所以 t1 线程执行的时候, t2 线程也在执行,所以实际上是从启动线程开始算2秒,相对 t1 线程而言,只需要等待 1秒
  4. 因此总耗时是 2000 毫秒

有时效的 join

@Slf4j(topic = "io.ray")
public class Test09 {

    static int r1 = 0;
    static int r2 = 0;

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

    public static void test() throws InterruptedException {

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                r1 = 10;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");

        long start = System.currentTimeMillis();
        t1.start();

        log.debug("join begin");
        // 有时效的 join,等待2秒
        t1.join(2000);

        long end = System.currentTimeMillis();

        log.debug("r1: {}  cost: {}", r1, end-start);
    }
}

结果

08:09:54.335 [main] DEBUG io.ray - join begin
08:09:56.340 [main] DEBUG io.ray - r1: 0  cost: 2007

分析(超过时效)

t1 线程需要等待 3秒后才给 r1 赋值,但 t1 的 join 方法等待如果超过 2秒,直接往下执行,不需要等待 t1 线程结束,此时输出 r1 的值为 0,因为还差 1秒线程 t1 才给 r1 赋值,所以程序总耗时是 2 秒。

分析(未超过时效)

如果未超过时效,以 t1 实际的调用完毕为准,程序会提前结束,不会说等超过时效才结束程序。

interrupt 方法详解

interrupt 打断阻塞

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

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

        Thread t1 = new Thread(() -> {
            log.debug("sleep..");
            try {
                Thread.sleep(5000); // sleep、wait、join 方法同理
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");

        t1.start();

        // 主线程要等待一下才执行打断,不然并发执行,主线程打断的不是 t1 的阻塞线程,而是正常运行的线程,打断标记返回 true,不是我们想要的结果
        Thread.sleep(100);

        // 打断线程
        t1.interrupt();

        log.debug("打断标记: {}", t1.isInterrupted());
    }
}

结果

08:32:04.102 [t1] DEBUG io.ray - sleep..
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at io.ray.threadstudy.test.Test10.lambda$main$0(Test10.java:19)
	at java.lang.Thread.run(Thread.java:748)
08:32:04.201 [main] DEBUG io.ray - 打断标记: false

分析

对于 sleepwaitjoin 这种方法被打断后,以异常的方式表示被打断了,并且会抹去打断标记,设置为 false,将来这个打断标记可以用于被打断后,程序是否继续运行,还是结束程序。

sequenceDiagram
participant t1 as 线程一

打断正常运行的线程

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

@Slf4j(topic = "io.ray")
public class Test11 {

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

    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();

        Thread.sleep(500);
        t2.interrupt();
    }
}

输出

16:42:58.213 [t2] DEBUG io.ray - 打断状态:true

不推荐的方法

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

方法名

static

功能说明

stop()

停止线程运行

suspend()

挂起(暂停)线程运行

resume()

恢复线程运行

主线程与守护线程

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

@Slf4j(topic = "io.ray")
public class Test12 {

    public static void main(String[] args) {

        log.debug("开始运行...");

        Thread t1 = new Thread(() -> {
            log.debug("t1 开始运行");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("t1 结束运行");
        }, "t1");

        // 设置该线程为守护线程,必须在启动前设置
        t1.setDaemon(true);
        t1.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("运行结束...");
    }
}

输出

16:51:58.073 [main] DEBUG io.ray - 开始运行...
16:51:58.223 [t1] DEBUG io.ray - t1 开始运行
16:51:59.221 [main] DEBUG io.ray - 运行结束...

t1 线程是守护线程,所以main线程结束后,程序直接结束了,不会等待 t1 线程执行完毕

注意:

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

五种状态

五种状态是以操作系统层面来描述的

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

六种状态

六种状态是从 Java API 层面来描述的

根据 Thread.State 枚举,分为六种状态

  1. NEW:线程刚被创建,但是还没有调用 start() 方法
  2. RUNNABLE:当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】、【阻塞状态】
  3. BLOCKED、WAITING、TIMED_WAITING:都是 Java API 层面对【阻塞状态】的细分
  4. TERMINATED:当线程代码运行结束

共享模型之管程

两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0吗?

@Slf4j(topic = "io.ray")
public class Test13 {

    // 静态变量
    static int counter = 0;

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                counter--;
            }
        }, "t2");

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

        log.debug("counter = [{}]", counter);
    }
}

输出

17:11:18.889 [main] DEBUG io.ray - counter = [-393]

问题分析

以上的结果可能是正数、负数、零。 为什么呢?因为 Java 中对静态变量的自增、自减并不是原子操作,要彻底理解,必须从字节码来进行分析。