Java线程基础(一)

Demon.Lee 2021年02月14日 838次浏览

相关Java代码示例使用Java 11

什么是线程

先执行一段代码,打印线程的名称,这样有一个比较直观的认知:

public class ThreadMainTest {

    public static void main(String[] args) {
        System.out.println(getThreadName()+" enter...");
        sleep(30000);
        System.out.println(getThreadName()+" exit...");
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static String getThreadName(){
        return Thread.currentThread().getName();
    }
}

输出结果如下:

main enter...
main exit...

通过上面演示,我们可以看到执行main函数的线程名字就叫main。对此,JDK中Thread类的Javadoc也有相应的描述:

/**
 * A <i>thread</i> is a thread of execution in a program. The Java
 * Virtual Machine allows an application to have multiple threads of
 * execution running concurrently.
 * <p>
 * ...
 * ...
 * <p>
 * When a Java Virtual Machine starts up, there is usually a single
 * non-daemon thread (which typically calls the method named
 * {@code main} of some designated class). The Java Virtual
 * Machine continues to execute threads until either of the following
 * occurs:
 * <ul>
 * <li>The {@code exit} method of class {@code Runtime} has been
 *     called and the security manager has permitted the exit operation
 *     to take place.
 * <li>All threads that are not daemon threads have died, either by
 *     returning from the call to the {@code run} method or by
 *     throwing an exception that propagates beyond the {@code run}
 *     method.
 * </ul>
 * <p>
 * There are two ways to create a new thread of execution. 
 * ...
 * ...
 *
 * @author  unascribed
 * @see     Runnable
 * @see     Runtime#exit(int)
 * @see     #run()
 * @see     #stop()
 * @since   1.0
 */
public class Thread implements Runnable {
    ...
    ...
}

那么main线程是怎么来的呢,这个很容易想到:JVM。

也就是说,每当运行一个Java程序,操作系统都会启动一个进程运行JVM,JVM又会启动一个叫main的线程来执行代码。

那除了main线程,是否还有其他线程呢?通过jdk提供的Jconsole监控工具,我们可以看到JVM中运行的所有线程。

# 通过jps找到Java进程
> jps
5777 GradleDaemon
6888 ThreadMainTest
6262 Jps
>
# 通过jconsole监控我们启动的Java进程
> jconsole 6888

那到底什么是线程(Thread)呢?维基百科上的定义是:

线程(Thread)是操作系统能够进行运算调度的最小单位。

结合这个定义以及前面的例子,我们知道了:线程不是Java语言层面的东西,而是来自操作系统,Java中的代码是以线程为基本单位来执行的。

后面的学习中,我们会发现Java中的线程是依托于操作系统的线程,但又不完全等同。

线程为何而生

在单核CPU的时代,我们就可以一边听歌,一边编辑文档,这便是多进程带来的好处。便利的背后,正是操作系统调度多进程分时复用CPU的功劳:

支持多进程的分时复用在操作系统的发展史上具有里程碑的意义,UNIX就是因为解决了这个问题而名躁天下的。

同样,线程的出现也是为了分时复用CPU,从而实现多任务并发处理。

分时复用CPU的本质是:均衡CPU与I/O设备之间的运行速度差异。

在一个单核CPU的计算机上,假设有两个进程(或线程)A和B,A从文件中读取内容,B运行数据运算,当A读文件时,操作系统将A标记为“休眠状态”并让出CPU。此时加载B,让B获得CPU的使用权,这样B就不用等到A读取完文件后才被调度了,我们将这个过程称为“任务切换”。

这样切换带来的好处是:CPU的使用率上来了———CPU不会浪费大量的时间去等待I/O设备的响应,当I/O的数据从外围设备加载到内存之后,对应的A进(线)程会再次被唤醒,重新获取CPU的使用权。

最理想的情况,如上图所示,CPU在多个任务之间来回调度,使用率可以达到100%。如果不使用多进(线)程,就变成了下图的样子,I/O操作和CPU计算的使用率都比较低。

但进(线)程也不是越多越好,一是比较浪费资源,二是进(线)程切换频繁,会浪费大量CPU进行上下文切换,后续的文章会讨论创建多少个线程比较合适。

线程 vs 进程

既然说到线程,就一定会提到进程,我们电脑上每天打开的应用,在操作系统层面对应的就是进程。

进程在维基百科上的定义是:

进程(英语:process),是指计算机中已运行的程序。

在面向进程设计的系统(如早期的UNIX,Linux 2.4及更早的版本)中,进程是程序的基本执行实体;在面向线程设计的系统(如当代多数操作系统、Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

上面的定义中也提出来了,进程是线程的容器,也就是说线程包含在进程中,是进程的实际运作单位。

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

有了进程,为啥还需要线程呢?

这是因为线程更轻量,创建线程的代价更小。当需要CPU并发执行多个任务时,线程之间的切换相较于进程来说,需要保存和恢复的上下文更少,所耗费的资源也就更少,从而提高了任务处理的响应时间和吞吐量。

保存上下文和恢复上下文的过程不是“免费”的,需要内核在CPU上运行才能完成。

进程上下文中包含了该进程用户态的虚拟内存、栈、全局变量等资源,还包含了内核堆栈、寄存器等内核空间的状态。

而同一进程中的多个线程会共享该进程的虚拟内存和全局变量等资源,这些资源在线程上下文切换时就不需要修改(即不需要保存现场和恢复现场),而线程也有自己的私有数据,比如栈和寄存器等,这些数据是需要修改的。

Java线程的基本操作

前面简单介绍了线程和进程的基本概念,现在回到主题上来,看看如何使用Java线程。

创建线程的2种方式

如何创建线程,Thread类的Javadoc中就有对应的说明,但不管哪种方式,本质上都是构建一个Thread类的实例,线程启动(Thread#start())后,新线程会调用Thread#run()方法,而该方法便是重写了Runnable接口的run()方法。

第1种方式,就是实现java.lang.Runnable接口,该接口只有一个抽象方法: void run()。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

将Runnable接口的实现类作为Thread类的构造参数,创建完Thread实例后,调用Thread#start()方法即可启动线程,示例如下:

@Log4j2
public class RunnableExample1 implements Runnable {

    @Override
    public void run() {
        log.info("---------Thread create example...");
    }
}

public class ThreadCreateTest {

    @Test
    public void testThreadCreate() {
        log.info("-------enter...");
        Thread thread = new Thread(new RunnableExample1());
        thread.start();
        log.info("-------exit...");
    }
}

输出结果如下(这里使用了@Log4j2注解,通过log4j打印日志,可以输出线程名称、类名称等信息,方便观察):

21:27:02.379 [Test worker] INFO com.learn.core.chapter14.ThreadCreateTest - -------enter...
21:27:02.384 [Test worker] INFO com.learn.core.chapter14.ThreadCreateTest - -------exit...
21:27:02.384 [Thread-3] INFO com.learn.core.chapter14.RunnableExample1 - ---------Thread create example...

通过输出内容,可以看到测试的主线程叫Test worker,而运行RunnableExample1的线程则为Thread-3,并且Test worker线程是先结束的。

第2种方式,就是直接继承Thread类,重写run方法:

@Log4j2
public class MyThread extends Thread {

    @Override
    public void run() {
        log.info("--------create my thread...");
    }
}

@Log4j2
public class ThreadCreateTest {

    @Test
    public void testThreadCreate2(){
        log.info("-------enter2");
        MyThread myThread = new MyThread();
        myThread.start();
        log.info("-------exit2");
    }
}

输出结果如下,与前面的示例一致:

12:36:02.570 [Test worker] INFO com.learn.core.chapter14.ThreadCreateTest - -------enter2
12:36:02.576 [Test worker] INFO com.learn.core.chapter14.ThreadCreateTest - -------exit2
12:36:02.576 [Thread-3] INFO com.learn.core.chapter14.MyThread - --------create my thread...

线程的名字

Thread类的构造函数有很多,我们前面用到的主要是Thread(Runnable),Thread类的部分结构如下所示:

如果不指定线程名字,则默认线程名为: Thread-递增序号,这就是上面打印出Thread-3等线程名的原因。

public class Thread implements Runnable {
    ...
    ...

    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

		...
    ...

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

    ...
    ...

    public Thread(Runnable target, String name) {
        this(null, target, name, 0);
    }
    ...
    ...
}

如果不想使用默认的线程名,可以选择带线程名(name字段)的构造函数,或通过Thread#setName()方法修改名称(线程启动之前),示例如下:

@Log4j2
public class ThreadCreateTest {

    @Test
    public void testThreadCreate() {
        log.info("-------enter...");
        Thread thread = new Thread(new RunnableExample1(), "TestThread-1");
        thread.start();
        log.info("-------exit...");
    }

    @Test
    public void testThreadCreate2(){
        log.info("-------enter2");
        MyThread myThread = new MyThread();
        myThread.setName("MyThread-1");
        myThread.start();
        log.info("-------exit2");
    }
}

其他说明

1、启动线程调用的是Thread#start()方法,而不是直接调用Thread#run()方法,如果直接调用Thread#run()方法,并不会创建一个新线程。

2、Java8引入Lambda表达式后,创建Thread线程可以简写,比如:

    public void testThreadCreate() {
        Thread thread = new Thread(()->log.info("create thread..."));
        thread.start();
    }

但是不能将 Thread thread = new Thread(new RunnableExample1()) 简写为 Thread thread = new Thread(RunnableExample1::new),因为这样就等于将RunnableExample1::new这个方法作为了线程要调度的run方法,即创建一个RunnableExample1实例。

3、如果线程启动之后,再调用Thread#setName(String name)方法修改线程名称,程序运行不受影响,只是程序可能已经运行了一段时间,如果观察日志,就会发现,日志中的线程名前后不同,给人带来误解,所以要在线程启动前就设置好。

参考资料

[1] 阮一峰. 进程与线程的一个简单解释

[2] 廖雪峰. 进程 vs. 线程

[3] 王宝令. 可见性、原子性和有序性问题:并发编程Bug的源头

[4] 倪朋飞. 基础篇:经常说的 CPU 上下文切换是什么意思?

[5] Cay S.Horstmann. Java核心技术·卷I

[6] 臧萌. Java入门1•2•3