Java笔记 - 线程基础知识

前言

进程是一个执行中程序的实例,是操作系统进行资源分配和调度的一个独立单元。线程是进程中一个单一的程序控制流,是 CPU 调度和分派的基本单元。进程在执行时拥有独立的内存空间,进程中的线程可以共享进程的内存空间。在 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 版)

时间: 2024-10-15 08:46:52

Java笔记 - 线程基础知识的相关文章

Java并发(基础知识)—— Executor框架及线程池

在Java并发(基础知识)—— 创建.运行以及停止一个线程中讲解了两种创建线程的方式:直接继承Thread类以及实现Runnable接口并赋给Thread,这两种创建线程的方式在线程比较少的时候是没有问题的,但是当需要创建大量线程时就会出现问题,因为这种使用方法把线程创建语句随意地散落在代码中,无法统一管理线程,我们将无法管理创建线程的数量,而过量的线程创建将直接使系统崩溃. 从高内聚角度讲,我们应该创建一个统一的创建以及运行接口,为我们管理这些线程,这个统一的创建与运行接口就是JDK 5的Ex

线程基础知识系列(三)线程的同步

本文是系列的第三篇,前面2篇,主要是针对单个线程如何管理,启动等,没有过多涉及多个线程是如何协同工作的. 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 线程基础知识系列(一)线程的创建和启动  :线程的创建和启动,join(),daemon线程,Callable任务. 本文的主要内容 何谓线程安全? 何谓共享可变变量? 认识synchronized关键字 认识Lock synchronized vs Lock 1.何谓线程安全 多线程是把双刃剑,带

线程基础知识系列(二)线程的管理

本篇是线程基础知识系列的第二篇,主要简单江夏线程管理相关知识点. 线程基础知识系列(一)线程的创建和启动:说明了线程的2种创建和启动,join(),daemon线程,Callable 任务. 本文的主要内容 线程的状态 线程的优先级 sleep vs wait 线程的流程控制 Interrupt yield让出你的CPU 1.线程的状态 以<线程基础知识系列(一)线程的创建和启动>这张图,是程序的运行时线程信息截图.有main线程,user Threads,daemon Threads.现在咱

Java语言的基础知识9

第十一章(线程) 1.通过String name=Thread.currentThread().getName();来获取当前线程的名称. 2.多次启动一个线程或者启动一个已经运行的线程是非法的,会抛出IllegalThreadStateException异常对象. Thread.sleep((int)Math.random()*10000); 3.java提供了Runnable接口使继承了其他类之后同样可以实现该接口达到创建线程的目的Runabble接口同样定义了Run方法. 实现Runnab

线程基础知识系列(四)线程的同步2 线程通信和Condition变量

本文是系列的第四篇. 线程基础知识系列(三)线程的同步  :同步控制,锁及synchronized 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 线程基础知识系列(一)线程的创建和启动  :线程的创建和启动,join(),daemon线程,Callable任务. 第三篇文章,重点阐述了如何使用锁和同步块对线程间共享可变变量保护,保证只有一个线程可以进入临界区.其实,没有过多的涉及另一个重要的同步概念:线程协作.第三篇中涉及的线程间并没有有效的协调.

java笔记--多线程基础

多线程技术 在java中实现多线程技术有两种方式: 1.继承Thread类: 2.实现Runnable接口 这两种方法都需要重写run()方法:通常将一个新线程要运行的代码放在run()方法中(这是创建没有返回值线程的方法)由于java只支持单继承,当类已经继承有其他类时,只能选择实现Runnable接口在启动线程时需要使用Thread类的start()方法,而不是直接使用run()方法: 如: public static void function() { for (int i = 0; i

线程基础知识系列(五)认识volatile

线程基础知识系列(四)线程的同步2  :线程的notify-wait通信机制,以及Condition条件变量 线程基础知识系列(三)线程的同步  :同步控制,锁及synchronized 线程基础知识系列(二)线程的管理 :线程的状态,控制,休眠,Interrupt,yield等 线程基础知识系列(一)线程的创建和启动  :线程的创建和启动,join(),daemon线程,Callable任务. 本篇文章主要讨论的关键字是volatile. volatile使用场景 volatile介绍 vol

线程基础知识

什么是线程: 在一个程序里的一个执行路线就叫做线程(thread).更准确的定义是:线程是"一个进程内部的控制序列" 一切进程至少都有一个执行线程 进程与线程 进程是资源竞争的基本单位 线程是程序执行的最小单位 线程共享进程数据,但也拥有自己的一部分数据 线程ID 一组寄存器 栈 errno 信号状态 优先级 fork和创建新线程的区别 当一个进程执行一个fork调用的时候,会创建出进程的一个新拷贝,新进程将拥有它自己的变量和它自己的PID.这个新进程的运行时间是独立的,它在执行时几乎

Java语言的基础知识

第三章 1.在java源文件编辑器中,选择某个成员变量,然后按住shift+alt+j,Eclipse会自动添加JavaDoc文档注释结构,如果选择的是方法,还会自动添加参数名称. 2.Java语言规定标示符是由任意的字母.下划线.美元符号和数字组成,并且第一个字符不能使数字,标示符不能使java中的保留关键字. 3.在Java语言中允许使用汉字或其他语言文字作为变量名,如int 年龄 =21;在程序运行时不会报错,但建议尽量不要使用这些语言作为变量. 4.java用关键字final来声明常量,