Java并发系列:线程介绍

/ JavaSE / 237浏览

什么是线程

操作系统在运行一个应用程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。操作系统调度的最小单元就是线程,也可以说是轻量级进程 Light Weigth Process,在一个进程中可以创建多个线程,这些线程都拥有自己的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程是在同时执行。

一个Java程序从 main() 方法开始执行,然后按照既定的代码逻辑执行,看似没有其它线程参与,但实际上Java程序天生就是多线程程序,因为执行 main() 方法的是一个名称为 main 的线程。下面使用JMX看下普通的Java程序包含哪些线程:

/**
 * @auther: Zealon
 */
public class JMXTest {
    public static void main(String[] args){
        // 获取Java线程管理 MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的 monitor 和 synchronizer信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
        // 遍历线程,打印线程ID 和 名称
        for(ThreadInfo threadInfo : threadInfos){
            System.out.println(threadInfo.getThreadId()+":"+threadInfo.getThreadName());
        }
    }
}

输出如下结果:

6:Monitor Ctrl-Break
5:Attach Listener
4:Signal Dispatcher
3:Finalizer
2:Reference Handler
1:main

可以看到。一个Java程序的运行不仅仅是main方法的运行,而实main线程和多个其它线程同时运行。

为什么要使用多线程

执行了一个简单的 “Hello,World”,却启动了那么多其它线程,是不是把简单的问题复杂化了?

当然不是,因为正确使用多线程,总是能够给开发人员带来显著的好处,而使用多线程的原因主要包括一下几点:

1.更多的处理器核心
随着处理器上的核心数量越来越多,以及超线程技术的广泛应用,现在大多数计算机都比以往更加擅长并行计算,而处理器性能的提升方式,也从更高的主频向更多的核心发展。如何利好处理器上的多个核心也成了现在的主要问题。

线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。试想一下,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该应用程序的执行效率。

相反,如果该程序使用多线程技术,将计算机逻辑分配到多个处理器核心上,就会显著减少程序的处理时间,并且随着更多处理器核心的加入而变得更有效率。

2.更快的响应时间
有时候,我们编写一些较为复杂的代码(不是说算法的复杂,而而复杂的业务逻辑),例如一笔订单的创建它包括:插入订单数据、生成订单快照、发送邮件通知等。用户从点击“订购”按钮开始,就要等待这些操作全部完成才能看到订购成功的结果。但是这么多业务操作,如何能够让其更快完成呢?

在上面的场景中,可以使用多线程技术,将 数据一致性不强 的操作派发给其它线程处理(也可以使用消息中间件),如发邮件等。这样做的好处是响应用户请求的线程能够尽快执行完成,缩短了响应时间,提升了用户体验。

3.更好的编程模型
Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,一旦开发人员建立好了模型,稍作修改总是能够方便地映射到Java提供的多线程编程模型上。

线程优先级

在Java线程中,通过一个整形成员变量 priority 来控制优先级,优先级的范围从 1 ~ 10,在线程构建的时候可以通过 setPriority(int) 方法来设置优先级,默认优先级是5,优先级越高的线程分配时间片的数量要多于优先级较低的线程。

设置优先级时,针对频繁阻塞(休眠或I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。在不同的JVM以及操作系统上,线程规划会存在差异,有些操作系统甚至会忽略对线程优先级的设定。

示例代码,定义3个线程,优先级分别为10、5、1,3个线程同时运行:

/**
 * @auther: Zealon
 */
public class PriorityTest {
    public static void main(String[] args) throws Exception{
        Job job1 = new Job(10);
        Thread t1 = new Thread(job1);
        t1.setPriority(10);
        t1.setName("优先级较高");

        Job job2 = new Job(5);
        Thread t2 = new Thread(job2);
        t2.setPriority(5);
        t2.setName("优先级平均");

        Job job3 = new Job(1);
        Thread t3 = new Thread(job3);
        t3.setPriority(1);
        t3.setName("优先级较低");

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

class Job implements Runnable{
    private int prority;
    public Job(int prority){
        this.prority = prority;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+",prority:" + prority);
    }
}

运行结果:

优先级较高,prority:10
优先级平均,prority:5
优先级较低,prority:1

输出结果可以看出,优先级高的先执行,越低的就靠后执行。

线程状态说明

Java线程在运行的声明周期中可能处于6种不同的状态,在给定的一个时刻,线程只能处于其中一种状态。

状态说明
NEW初始状态:线程被构建,但是还没有调用start()方法
RUNNABLE运行状态:Java线程将操作系统中的就绪和运行两种状态笼统地称作 “运行中”
BLOCKED阻塞状态:表示线程阻塞于锁
WAITING等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其它线程做出
一些特定动作(通知或中断)
TIME_WAITING超时等待状态:该状态不同于WAITING ,它是可以在指定的时间自行返回的
TERMINATED终止状态:表示当前线程已经执行完毕

线程状态变迁图: alt 如图所示,线程创建之后,调用start()方法开始运行:

守护线程(Daemon)

Daemon 线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程时,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。

注意:Daemon属性需要在启动线程之前设置,不能在启动线程之后设置哦!

Daemon线程被用作完成支持性工作,但是在JVM退出时Daemon线程中的finally块并不一定会执行,示例如下代码:

public class Daemon {

    public static void main(String[] args){
        Thread t1 = new Thread(new DaemonRunner(),"DaemonRunner");
        t1.setDaemon(true);
        t1.start();
        System.out.println("Daemon Thread run.");
    }

    static class DaemonRunner implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("Daemon Thread finally run.");
            }
        }
    }
}

运行Daemon程序,可以看到输出结果:

Daemon Thread run.
Process finished with exit code 0

Daemon Thread finally run. 并没有打印出来,main 线程在启动了线程 DaemonRunner 之后随着 main 方法执行完毕而终止,而此时Java虚拟机中已经没有非 Daemon 线程,迅即需要退出。Java虚拟机中的所有 Daemon 线程都需要立即终止,因此 DaemonRunner立即终止,但是 DaemonRunner 中的 finally 块并没有被执行。

🔑 所以,在构建 Daemon 线程时,不能依靠 finally 块中的内容来确保执行关闭或清理资源的逻辑!