Java笔记 - 线程基础知识

Posted demonyan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java笔记 - 线程基础知识相关的知识,希望对你有一定的参考价值。

前言

进程是一个执行中程序的实例,是操作系统进行资源分配和调度的一个独立单元,比如打开一个浏览器就启动了一个浏览器进程。线程是进程中一个单一的程序控制流,是 CPU 调度和分派的基本单元,比如采用多线程多窗口的浏览器,每个浏览器窗口都是一个线程。进程在执行时拥有独立的内存空间,进程中的线程可以共享进程的内存空间。在 Java 的世界中,进程可以拥有多个并发执行的线程,多线程是实现并发任务的方式。本文主要就 Java 线程的基础知识做一个梳理。

线程创建和启动

1. 实现 java.lang.Runnable 接口
定义线程执行的任务,需要实现 Runnable 接口并编写 run 方法。

public interface Runnable {

    /**
     * Starts executing the active part of the class' code. This method is
     * called when a thread is started that has been created with a class which
     * implements {@code Runnable}.
     */
    public void run();
}

以下示例通过实现 Runnable 接口来模拟火箭发射之前倒计时的任务:

public class LiftOff implements Runnable {
    private int countDown;

    public LiftOff(int countDown) {
        this.countDown = countDown;
    }

    private String status() {
        return countDown > 0 ? String.valueOf(countDown) : "LiftOff!";
    }

    @Override
    public void run() {
        while (countDown-- > 0) {
            System.out.println(status());
        }
    }
}

2. 实现 java.util.concurrent.Callable 接口
Callable 接口的 call 方法可以在任务执行结束后产生一个返回值,以下是 Callable 接口的定义:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以使用 ExecutorService 类的 submit 方法提交实现了 Callable 接口的任务,submit 方法会产生一个 Future 对象,它用 call 方法返回结果的类型进行了参数化。可以通过 isDone 方法来查询 Future 是否已经完成,如果已完成,则可以调用 get 方法获取结果。也可以不使用 isDone 方法进行检测就直接调用 get 方法获取结果,此时如果结果还未准备就绪,get 方法将阻塞直到结果准备就绪。

class TaskWithResult implements Callable<String> {
    private int id;

    public TaskWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() throws Exception {
        return "Result of TaskWithResult " + id;
    }
}

public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        // save Future object of submitted task
        List<Future<String>> futureList = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            futureList.add(exec.submit(new TaskWithResult(i)));
        }

        // waiting for all results
        while (futureList.size() > 0) {
            for (Future<String> future : futureList) {
                if (future.isDone()) {
                    try {
                        System.out.println(future.get());
                    } catch (InterruptedException | ExecutionException e) {
                        e.printStackTrace();
                    } finally {
                        futureList.remove(future);
                    }
                }
            }
        }

        exec.shutdown();
    }
}

3. 继承 java.lang.Thread 类
Thread 类的构造方法包含一个 Runnable 任务,以下是 Thread 类的构造方法:

public Thread(Runnable runnable);
public Thread(Runnable runnable, String threadName);
public Thread(ThreadGroup group, Runnable runnable);
public Thread(ThreadGroup group, Runnable runnable, String threadName);
public Thread(ThreadGroup group, Runnable runnable, String threadName, long stackSize);

调用 Thread 类的 start 方法启动线程,其 start 方法为新线程执行必要的初始化,然后调用 Runnable 的 run 方法,从而在新线程中启动该任务,当 run 方法执行结束时该线程终止。

public class BasicThreads {
    public static void main(String[] args) {
        Thread thread = new Thread(new LiftOff(10));
        thread.start();
    }
}

综上,Runnable 和 Callable 是工作单元,而 Thread 即充当工作单元,又是执行机制。一般而言,Runnable 和 Callable 优先于 Thread,因为可以获得更大的灵活性。

线程状态

在任意一个时间点,线程只可以处于以下六种状态之一:

    public enum State {
        /**
         * The thread has been created, but has never been started.
         */
        NEW,
        /**
         * The thread may be run.
         */
        RUNNABLE,
        /**
         * The thread is blocked and waiting for a lock.
         */
        BLOCKED,
        /**
         * The thread is waiting.
         */
        WAITING,
        /**
         * The thread is waiting for a specified amount of time.
         */
        TIMED_WAITING,
        /**
         * The thread has been terminated.
         */
        TERMINATED
    }

1. 新建(new)状态
线程此时已经分配了必要的系统资源,并执行了初始化,之后线程调度器将线程转变为可运行状态或者阻塞状态。
2. 可运行(Runnable)状态
线程此时正在运行或者等待线程调度器把时间片分配给它。
3. 阻塞(Blocked)状态
线程此时由于需要获取某个排他锁而阻塞,线程调度器将不会分配时间片给该线程,直到线程满足条件后进入可运行状态。
4. 等待(Waiting)状态
线程此时不会被分配时间片,要等待被其他线程显式地唤醒。
线程从可运行状态进入等待状态,可能有如下原因:
[1] 调用没有设置 Timeout 参数的 Object.wait 方法。
[2] 调用没有设置 Timeout 参数的 Thread.join 方法。
[3] 线程在等待某个 I/O 操作完成。
5. 限期等待(Timed_waiting)状态
线程此时不会被分配时间片,如果限期时间到期后还没有被其他线程显式地唤醒,则由系统自动唤醒。
线程从可运行状态进入限期等待状态,可能有如下原因:
[1] 调用 Thread.sleep 方法。
[2] 调用设置了 Timeout 参数的 Object.wait 方法。
[3] 调用设置了 Timeout 参数的 Thread.join 方法。
6. 终止(Terminated)状态
线程此时不再是可调度的。线程终止通常方式是从 run 方法返回,或者线程被中断。

线程各种状态之间的关系如下图所示:
这里写图片描述
线程的当前状态可以通过 getState 方法获得,getState 方法定义如下所示:

    /**
     * Returns the current state of the Thread. This method is useful for
     * monitoring purposes.
     *
     * @return a {@link State} value.
     */
    public State getState() {
        return State.values()[nativeGetStatus(hasBeenStarted)];
    }

线程调度是指系统为线程分配 CPU 使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。
使用协同式调度的多线程系统,线程的执行时间由线程本身来控制。协同式多线程的优点:实现简单,而且线程完成任务后才会请求进行线程切换,切换操作对线程而言是可知的,所以没有太多线程同步问题;它的缺点也很明显:线程执行时间不可控,如果某个线程编写有问题,会导致整个进程阻塞。
抢占式多线程每个线程将由系统来分配执行时间,线程切换不是由线程本身来决定的(在 Java 中,Thread.yield 方法可以出让 CPU 使用权,但没有办法绕过线程调度器获得 CPU 使用权),所以执行时间是系统可控的,不会出现某个线程导致整个进程阻塞的问题。目前 Java 使用的线程调度方式是抢占式线程调度。

线程常用方法

1. sleep 方法
sleep 方法会让当前运行线程暂停执行指定的时间,将 CPU 让给其他线程使用,但线程仍然保持对象的锁,因此休眠结束后线程会回到可运行状态。

public static void sleep(long time) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException;

2. join 方法
如果线程 A 调用目标线程 B 的 join 方法,则线程 A 将会被挂起,直到线程 B 结束才恢复。或者在调用 join 方法时传入超时参数,如果目标线程在这段时间到期还没有结束的话,join 方法也会返回。

public final void join() throws InterruptedException;
public final void join(long millis) throws InterruptedException;
public final void join(long millis, int nanos) throws InterruptedException;

3. yield 方法
yield 方法让当前运行线程回到可运行状态,使得优先级相同或者更高的线程有机会被调度执行。和 sleep 方法一样,线程仍然保持对象的锁,因此调用 yield 方法后也会回到可运行状态。

public static native void yield();

4. interrupt 方法
interrupt 方法将给线程的发送中断请求,行为取决于线程当时的状态。如果线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将会抛出 InterruptedException。当抛出 InterruptedException 异常或者调用 Thread.interrupted 方法时,中断状态将被清除,这样确保不会在某个线程被中断时出现两次通知。

public void interrupt();

在线程上调用 interrupt 方法时,中断发生的唯一时刻是线程要进入到阻塞操作,或者已经在阻塞操作过程中。其中 sleep/wait/join/NIO 阻塞是可中断的,I/O 阻塞和 synchronized 同步阻塞是不可中断的。

后台线程

后台(daemon)线程是指程序运行时在后台提供服务的线程。后台线程不属于程序中不可或缺的部分,当所有的非后台线程结束时,进程会终止,同时会结束进程中所有的后台进程。
在线程启动之前调用 setDaemon 方法,才能把线程设置为后台进程。通过使用 isDaemon 方法可以测试线程是否属于后台线程,后台线程创建的任何线程将被自动设置成为后台线程。示例如下所示:

public class SimpleDaemons implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            // must call before start
            daemon.setDaemon(true);
            daemon.start();
            System.out.println("Daemon or not: " + (daemon.isDaemon() ? " yes" : "not"));
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECONDS.sleep(200);
    }
}

后台线程也比较常见,比如 Zygote 就有四个后台线程,分别是 HeapTaskDaemon(堆整理线程), ReferenceQueueDaemon(引用队列线程), FinalizerDaemon(终结方法线程), FinalizerWatchdogDaemon(终结方法监控线程) 。

线程之间的协作

当多个线程一起工作完成某些任务时,线程彼此之间需要协作。线程之间的协作关键问题在于握手,这种握手可以通过 Object 对象的 wait 和 notify/notifyAll 方法来实现,也可以通过 Java SE5 提供的具有 await 和 signal 方法的 Condition 对象来实现。

1. wait 与 notify/notifyAll 方法
wait 方法使线程等待某个条件发现变化,通常这个条件由另一个线程来改变。wait 方法被调用时,当前线程进入等待状态,对象上的锁会被释放,因此该对象中其他 synchronized 方法可以在 wait 期间被调用。
使用 notify 方法可以唤醒一个调用 wait 方法进入等待状态的线程,使用 notifyAll 方法可以唤醒所有调用 wait 方法进入等待状态的线程。由于可能有多个线程在单个对象上处于 wait 状态,因此使用 notifyAll 比调用 notify 更安全。使用 notify 而不是 notifyAll 是一种优化,在多个等待同一个条件的线程中只有一个会被唤醒,因此如果使用 notify,必须确保被唤醒的是恰当的线程。
借用 Java 编程思想中的一个示例,汽车保养时打蜡(wax)和抛光(buff)需要协调进行,先打完一层蜡,然后进行抛光,继续打下一层蜡,然后继续抛光,依次循环。以下示例通过 wait 与 notifyAll 方法来完成线程之间的协作:

class Car {
    private boolean waxOn = false;
    public synchronized void waxed() {
        // ready to buff
        waxOn = true;
        notifyAll();
    }

    public synchronized void buffed() {
        // ready to wax
        waxOn = false;
        notifyAll();
    }

    public synchronized void waitForWaxing() throws InterruptedException {
        while (!waxOn)
            wait();
    }

    public synchronized void waitForBuffing() throws InterruptedException {
        while (waxOn)
            wait();
    }
}

class WaxTask implements Runnable {
    private Car car;
    public WaxTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println("Wax On!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax On task");
    }
}

class BuffTask implements Runnable {
    private Car car;
    public BuffTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax Off task");
    }
}

public class WaxBuff {
    public static void main(String[] args) throws Exception {
        Car car = new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new WaxTask(car));
        exec.execute(new BuffTask(car));

        TimeUnit.SECONDS.sleep(3);
        // interrupt all submitted tasks
        exec.shutdownNow();
    }
}

wait, notify 和 notifyAll 方法属于基类 Object 中的方法,它们只能在同步方法或者同步控制块里调用,否则程序运行时将抛出 IllegalMonitorStateException 异常。

2. Lock 与 Condition 对象
Java SE5 的 java.util.concurrent 类库提供的 Condition 类也可以用于线程间的协作。通过 Condition 对象的 await 方法来等待某个条件发现变化。当外部条件发生变化时,通过调用 signal 方法来唤醒被挂起的线程,或者通过调用 signalAll 方法来唤醒所有在这个 Condition 上被挂起的线程。
以下使用 ReentrantLock 替换 synchronized 方法,使用 Condition 类提供的 await 和 signalAll 方法对汽车保养过程进行改写:

class Car {
    private boolean waxOn = false;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void waxed() {
        lock.lock();
        try {
            // ready to buff
            waxOn = true;
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void buffed() {
        lock.lock();
        try {
            // ready to wax
            waxOn = false;
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void waitForWaxing() throws InterruptedException {
        lock.lock();
        try {
            while (!waxOn) {
                condition.await();
            }
        } finally {
            lock.unlock();
        }
    }

    public void waitForBuffing() throws InterruptedException {
        lock.lock();
        try {
            while (waxOn)
                condition.await();
        } finally {
            lock.unlock();
        }
    }
}

class WaxTask implements Runnable {
    private Car car;
    public WaxTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                System.out.println("Wax On!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.waxed();
                car.waitForBuffing();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax On task");
    }
}

class BuffTask implements Runnable {
    private Car car;
    public BuffTask(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
                car.waitForWaxing();
                System.out.println("Wax Off!");
                TimeUnit.MILLISECONDS.sleep(200);
                car.buffed();
            }
        } catch (InterruptedException e) {
            System.out.println("Exiting via interrupt");
        }
        System.out.println("Ending Wax Off task");
    }
}

public class WaxBuff {
    public static void main(String[] args) throws Exception {
        Car car = new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new BuffTask(car));
        exec.execute(new WaxTask(car));

        TimeUnit.SECONDS.sleep(3);
        // interrupt all submitted tasks
        exec.shutdownNow();
    }
}

明显感觉正确使用以上线程协作方式还是比较困难的,所以应该使用更高级的工具比如说同步器(Synchronizer)来代替。同步器是一些可以使线程等待另一个线程的对象,允许它们之间协调推进,常用的同步器包括 CountDownLatch, Semaphore, CyclicBarrier 等。

后记

Java 线程看上去简单,但实际使用过程中有很多值得注意的地方,稍有不慎就可能碰到奇怪的问题,所以使用线程时需要非常仔细甚至保守。以上是我认为关于线程值得整理的内容,更多相关内容可以查阅参考资料。

参考资料

1. Java 编程思想(第 4 版)
2. 深入理解 Java 虚拟机(第 2 版)
3. Effective Java(第 2 版)

以上是关于Java笔记 - 线程基础知识的主要内容,如果未能解决你的问题,请参考以下文章

Java——线程池

尚硅谷_Java零基础教程(多线程)-- 学习笔记

Java线程池详解

Java线程池详解

Java 线程池详解

多线程Java多线程学习笔记 | 多线程基础知识