Java高并发之认识线程

171 阅读5分钟

在认识线程之前首先讨论什么是并发和并行,这个问题在面试中经常被问到。这里我们引用黄俊在《深入理解Java高并发编程》中给出的定义。

并发:在有限的CPU中执行超过CPU数量的任务,任务之间交替执行。

并行: 在有限的GPU中执行任务的数量小于等于CPU的数量,这些任务同时执行。

从上面的给出的定义可以看出,并发和并行的主要区别就是执行的任务数量和CPU数量之间的关系。

1. 什么是线程?

这里引用百度百科给出线程的定义:

线程是操作系统和能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并发执行不同的任务,在Unix系统中也称为轻量进程,但轻量进程更多指的是内核线程,而把用户线程称为线程。

关于内核级线程用户级线程的概念可以参考这篇文章

541_a70_683.jpg 上图左侧展示的用户级线程,可以看出线程表由进程维护,因此线程的切换由进程完成,而进程的切换由内核完成,右图展示的内核级线程,可以看到线程表由内核维护,因此线程的切换由内核完成。因此Java中的线程是内核级线程。

2. 线程的创建和启动

日程生活中,我们经常在刷微博的同时播放音乐,接下来我们将创建两个线程,一个线程用于浏览新闻,另外一个线程用户欣赏音乐。

public class TryConcurrency {
    public static void main(String[] args) {
        // browseNews();
        // enjoyMusic();
        new Thread(TryConcurrency::browseNews).start();
        new Thread(TryConcurrency::enjoyMusic).start();
    }

    private static void browseNews() {
        for (;;) {
            System.out.println("Uh-huh, the good news.");
            sleep(1);
        }
    }

    private static void enjoyMusic() {
        for (;;) {
            System.out.println("Uh-huh, the nice music.");
            sleep(1);
        }
    }

    private static void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后使用JConsole工具观察该进程的线程执行情况。

image.png 图中的Thread-0和Thread-1分别是执行browseNews和enjoyMusic函数的线程。

3. 线程生命周期

image.png

线程的生命周期分为五个阶段:NEW、RUNNABLE、RUNNING、BLOCKED、TERMINATED。

3.1 NEW阶段

Thread t1 = new Thread(() -> {
    
});

当一个Thread对象被创建出来后,未调用start方法前,Thread对象处于NEW阶段。

3.2 RUNNABLE阶段

t1.start();

Thread对象调用了start方法之后进入到RUNNABLE阶段,该阶段表示线程具备执行的资格,正在等待CPU的调度。当被调度器放弃执行时或主动调用yield后,从RUNNING阶段进入到RUNNALBE阶段。

3.3 RUNNING阶段

CPU通过轮询或其他方式选择了某个线程进行执行时,该线程就处于RUNNING状态。线程处于RUNNING状态表示该线程正在被CPU执行。

3.4 BLOCKED阶段

当处于RUNNING的编程执行了sleep函数或者wait函数后,线程进入BLOCKED阶段;当线程中执行了I/O操作后,也会进入BLOCKED阶段;当线程获取锁阻塞时,会进入BLOCKED阶段。

3.5 TERMINATED阶段

TERMINATED是线程的最终状态,在该状态的线程不能再切换到其他状态,当线程进入到TERMINATED状态,意味着该线程的整个生命周期都结束了。

4. Start方法源码分析

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 * <p>
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the
 * <code>start</code> method) and the other thread (which executes its
 * <code>run</code> method).
 * <p>
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

从start函数的代码注释可以看出,该函数的作用是触发线程执行,JVM虚拟机会调用Thread对象创建时指定的run方法。执行的结果是两个线程并发执行,一个是当前线程(执行start方法的线程),一个是执行run方法代码逻辑的线程。那么run方法是怎么被执行的呢?

我们可以看到在start方法的源码中有一个start0 JNI方法,该方法就是用来执行run方法等操作的。

private native void start0();

通过阅读start方法我们可以得出如下的结论:

  • Thread被构造之后处于NEW状态,即threadStatus等0。
  • 同一个Thread对象只能执行一次start方法,执行多次会抛出IllegalThreadStateException异常。
  • 线程启动之后会被添加到ThreadGroup中。 Thread的run和start方法采用了模板设计模式,父类负责编写算法结构代码,子类负责实现逻辑细节。
public void run() {
    if (target != null) {
        target.run();
    }
}