Skip to main content

什么是线程

huhxOriginalAbout 6 minjavaThreadConcurrency

在java中稍微高级一点儿的话题,一定会涉及到多线程。那么线程到底是个什么东西,为什么让人如此的着迷和津津乐道?今天我们就展开对Thread的学习。

什么是线程

跟线程相关的概念

说服务端编程还是大量需要并行计算的,而Java也主要占领着服务端市场,那么对Java的并行计算的研究也就显得非常的必要。但首先,我想在这里先介绍几个重要的相关概念。

同步(Synchronous)和异步(Asynchronous)

同步和异步通常用来形容一次方法调用。同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像一个消息传递,一旦开始方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中真实地执行。整个过程不会阻碍调用者的工作。

并发(Concurrency)和并行(Parallelism)

并发和并行都可以表示多个任务一起执行,但是并发是多个任务交替执行,同一时刻只有一个任务在执行,而并行同一时刻可以有多个任务在同时执行。

并发与并行
并发与并行

什么是线程上下文切换

https://zhuanlan.zhihu.com/p/52845869open in new window

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。

就好比卫生间是临界区,一个人先进去使用并关门上锁。其他的人如果要用此卫生间的话,就得在外等候了,直到用卫生间的人欣然走出😌,释放了卫生间这个资源。

阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要这个资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。

非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断前向执行。

线程生命周期

讲线程生命周期之前,我们需要了解线程有哪些状态。其实在Thread中的State枚举中定义了如下状态:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • NEW:线程对象被创建,但还没有调用start()方法
  • RUNNABLE:调用start()方法后,线程处于runnable状态,但线程调度程序尚未选择它作为正在运行的线程。这个状态其实包含就绪(ready)和运行中(running)
  • BLOCKED:线程在执行中遇到了类似于synchronized同步块,就会进入BLOCKED阻塞状态,这时线程就会暂停执行,直到获得请求的锁
  • WAITING:进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)
  • TIMED_WAITING:类似于WAITING,但是它可以在指定的时间过后自行返回
  • TERMINATED:当run()方法退出时,该线程已经执行完毕,处于终止或死亡状态。

Java中线程的生命周期由JVM控制,它的状态图如下:

Thread Lifecycle
Thread Lifecycle

初始(NEW)

线程被创建到start()被调用这期间,线程的状态是:NEW

var thread = new Thread(() -> System.out.println("run"));
System.out.println(thread.getState()); // NEW

可运行(RUNNABLE)

当创建的线程调用了start()方法,线程的状态从NEW变成了RUNNABLE。处于此状态的线程要么正在运行,要么准备运行,但它们正在等待系统的资源分配。

在多线程环境中,线程调度程序(JVM的一部分)为每个线程分配固定的时间片。因此线程会运行一段特定的时间,然后将控制权交给其他RUNNABLE线程

var t1 = new Thread(() -> {});
t1.start();
System.out.println(t1.getState());

 

上述很大可能是打印:RUNNABLE,原因是第四行的代码是在main线程中执行,而main线程和线程t1可能是并行执行,那么在第四行输出之前,线程t1没准已经执行完改变了状态。

阻塞(BLOCKED)

当线程当前不符合运行条件时,该线程就处于BLOCKED状态。当它等待监视器锁并尝试访问被其他线程锁定的代码段时,它会进入此状态。

public class ThreadLifecycle {
    public static void main(String[] args) throws InterruptedException {
        var t1 = new Thread(new BlockRunnable());
        var t2 = new Thread(new BlockRunnable());

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

        Thread.sleep(1000);

        System.out.println(t2.getState());
        System.exit(0);
    }
}

class BlockRunnable implements Runnable {
    @Override
    public void run() {
        synchronized (BlockRunnable.class) {
            while (true) {}
        }
    }
}

上述创建两个线程t1和t2,当t1启动之后首先进入synchronized代码块中,拿到了BlockRunnable.class的锁。这里用while模拟t1拥有锁的时间,以便其他线程无法进入此synchronized方法。而当t2启动时尝试进入synchronized的代码块,发现锁已经被t1拥有,此时t2线程就处于BLOCKED的状态。

等待(WAITING)

当线程等待其他线程执行特定操作时,该线程处于WAITING状态。根据JavaDocs,任何线程都可以通过调用以下三种方法之一进入此状态:

  • object.wait()
  • thread.join()
  • LockSupport.park()
public class WaitingState {
    public static void main(String[] args) throws InterruptedException {
        var t1 = new Thread(() -> {
            synchronized (WaitingState.class) {
                try {
                    System.out.println("before wait.");
                    WaitingState.class.wait();
                    System.out.println("after wait.");
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            }
        });
        t1.start();
        t1.setDaemon(true);
        TimeUnit.SECONDS.sleep(1);

        System.out.println(t1.getState());
    }
}






 













我们创建线程t1,并在synchronized块中调用了wait()方法。而在main线程中睡眠了1秒钟,当我们打印线程t1状态时,保证此刻t1已经执行完了wait方法。上述的输出结果为:

before wait.
WAITING

Info

之所以调用setDaemon方法,是想让程序可以正常退出。要不然线程t1没有被唤醒,会一直wait被挂起,程序没办法退出。

关于setDaemon可以参考:守护线程

超时等待(TIMED_WAITING)

当线程A等待另一个线程B在规定的时间内执行特定操作时,那么线程A处于TIMED_WAITING状态。

根据java文档,有以下五种方法可以将线程置于TIMED_WAITING状态:

  • thread.sleep(long millis)
  • wait(int timeout) or wait(int timeout, int nanos)
  • thread.join(long millis)
  • LockSupport.parkNanos(long millis)
  • LockSupport.parkUntil(long millis)
public class TimedWaitingState {
    public static void main(String[] args) throws InterruptedException {
        var t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        });

        t1.start();

        TimeUnit.SECONDS.sleep(1);
        System.out.println(t1.getState());
    }
}

我们创建并启动了一个线程t1,该线程进入睡眠状态,超时时间为5秒,输出将是:TIMED_WAITING

终止(TERMINATED)

当线程完成执行或被异常终止时,它处于TERMINATED状态。

public class TerminatedState {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t1.stop();
        TimeUnit.SECONDS.sleep(1);

        System.out.println(t1.getState());
    }
}

我们除了可以使用getState()得到线程的状态,也可以使用isAlive()判断线程是否存活。

t1.isAlive();

上述例子返回false,简而言之:当且仅当线程已启动且尚未死亡时,该线程才处于活动状态,isActive方法返回true

FAQ

单核CPU设定多线程是否有意义?

有意义

总结

参考资料