深入浅出Java多线程(二):Java多线程类和接口

引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第二篇内容:Java多线程类和接口。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在现代计算机系统中,多线程技术是提升程序性能、优化资源利用和实现并发处理的重要手段。特别是在Java编程语言中,多线程机制被深度集成并广泛应用于高并发场景,如服务器响应多个客户端请求、大规模数据处理以及用户界面的实时更新等。理解并熟练掌握Java中的多线程创建与管理方式,不仅能帮助开发者充分利用硬件资源,还能有效避免竞态条件、死锁等并发问题,确保应用程序在多核处理器架构下运行得更为高效且稳定。

本文将深入探讨Java多线程编程的基本概念和技术细节。首先从最基础的Thread类入手,介绍如何通过继承Thread类或实现Runnable接口来定义并启动一个线程,强调start()方法对于激活线程执行的关键作用,并对比两种实现方式的优劣。同时,我们将揭开Thread类构造方法背后的秘密,详述各个参数的意义及初始化过程。

进一步地,文档将阐述Thread类中的一系列常用方法,包括获取当前线程引用的currentThread()方法、启动线程执行逻辑的start()方法、释放CPU时间片的yield()方法、控制线程暂停执行的sleep()方法,以及用于同步等待其他线程完成的join()方法。通过对这些方法的详细解读,读者能够更好地掌握Java线程间的协作和调度原理。

此外,为了满足异步任务执行和结果返回的需求,Java提供了Callable接口及其配套的Future和FutureTask类。Callable允许我们在新的线程中执行有返回值的任务,而Future作为异步计算的结果容器,可以用来查询任务是否完成、取消正在执行的任务以及获取计算结果。FutureTask则是对Future接口和Runnable接口功能的完美融合,它不仅封装了任务的执行逻辑,还提供了一种便捷的方式来管理和跟踪异步操作的状态。

综上所述,本文旨在引导逐步了解和掌握Java多线程编程的核心类与接口,并通过实际示例解析它们的工作机制和应用场景,为开发高性能、高并发的Java应用程序奠定坚实的基础。接下来的内容将逐一展开对上述关键知识点的详细讲解。

Java中创建与启动线程


Java中创建与启动线程(约800字)

在Java中,我们可以通过继承Thread类或实现Runnable接口来创建自定义的线程对象,并通过调用start()方法启动执行。这两种方式分别具有不同的应用场景和特点。

继承Thread类 通过直接继承Thread类并重写run()方法,可以便捷地创建一个具备特定任务逻辑的线程。以下是一个简单的示例:

public class MyCustomThread extends Thread {
    @Override
    public void run() {
        System.out.println("Inheriting from Thread class: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyCustomThread myThread = new MyCustomThread();
        myThread.start(); // 启动线程
    }
}

在这个例子中,MyCustomThread继承了Thread类并覆盖了run()方法,当调用start()方法时,JVM会为该线程分配资源并安排它在适当的时候执行run()方法中的代码。

注意:每个线程只能调用一次start()方法。如果试图再次调用start(),将会抛出IllegalThreadStateException异常。这是因为一旦线程开始运行后,其生命周期已经进入执行阶段,不能重复初始化和启动。

实现Runnable接口 相较于继承Thread类,实现Runnable接口更为灵活,因为Java语言遵循单继承原则,而接口可以多重实现。这使得我们的类可以在继承其他类的同时实现多线程功能。以下是使用Runnable接口创建线程的示例:

public class RunnableTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Implementing Runnable interface: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        Thread thread = new Thread(task, "MyRunnableThread");
        thread.start();
    }
}

在上述代码中,RunnableTask实现了Runnable接口并提供了run()方法的具体实现。然后,我们将RunnableTask实例传给Thread类的构造函数,创建了一个新的线程,并通过thread.start()来启动它。

此外,从Thread类的源码分析可知,Thread类是Runnable接口的一个实现类,其构造方法接收Runnable类型的参数target,并通过内部的init方法对其进行初始化。这样,无论我们是继承Thread还是实现Runnable,最终都是为了提供一个Runnable实例给Thread来执行具体的任务逻辑。

总结来说,Java提供了两种途径创建线程,各有优劣。继承Thread类的方式直观简洁,适用于轻量级的线程封装;而实现Runnable接口则更符合面向对象设计原则,避免了类层次结构的限制,提高了代码的可复用性和灵活性。在实际编程中,推荐优先考虑实现Runnable接口以保持代码结构清晰、易扩展。

Thread类构造方法详解

在Java中,Thread类的构造方法是创建线程对象并为其设置属性的核心途径。Thread类提供了多个构造函数以满足不同场景下的初始化需求,但它们最终都会调用到一个私有的init方法来完成线程对象的初始化。

// Thread类的部分源码片段:
private void init(ThreadGroup g, Runnable target, String name,
                        long stackSize, AccessControlContext acc,
                        boolean inheritThreadLocals)
 
{...}

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

上述代码揭示了Thread类的一个重要构造方法:接受一个Runnable类型的target参数,用于指定线程要执行的任务;同时为新创建的线程生成一个默认名称,并分配默认的栈大小。当通过这个构造器实例化Thread对象时,会调用内部的init方法进行详细的初始化操作:

  • g: ThreadGroup - 线程组,若不指定,默认值为null,表示线程将加入到当前应用程序的主要线程组中。
  • target: Runnable - 这个参数至关重要,它定义了线程执行体,即run()方法的具体内容。当我们实现Runnable接口或者继承Thread类重写run()方法时,实际就是给target赋值。
  • name: String - 指定线程的名字,如果没有提供名字,则系统会自动为其生成一个唯一的线程名。
  • stackSize: long - 栈的大小,通常情况下我们不会显式设置线程栈大小,这里使用默认值0,由JVM自行决定合适的栈空间大小。
  • acc: AccessControlContext - 安全控制上下文,用于控制线程执行权限,这是一个相对复杂且较少直接使用的概念,主要用于安全管理框架,如Java安全模型中的访问控制列表等。
  • inheritThreadLocals: boolean - 控制线程是否从父线程继承ThreadLocal变量。在多线程环境下,ThreadLocal可以为每个线程维护一个独立的变量副本,此处涉及到线程局部变量的传递问题。

此外,Thread类内部还包含了与ThreadLocal相关的两个私有属性threadLocalsinheritableThreadLocals,它们用于支持线程间的数据隔离以及特定情况下的线程本地变量继承。

总之,通过Thread类的构造方法,我们可以灵活地定制线程的各种属性,包括任务目标、线程名以及其他可能影响线程行为的因素。这些构造方法的设计充分体现了Java对线程管理的灵活性和可配置性。

Thread类常用方法


在Java中,Thread类提供了多种方法来管理和控制线程的生命周期及行为。以下将详细解析Thread类的几个核心方法,并对比使用Runnable接口与继承Thread类创建线程的方式。

Thread类的常用方法

  • currentThread(): 这是一个静态方法,返回对当前正在执行的线程对象的引用。例如:

    Thread currentThread = Thread.currentThread();
    System.out.println(currentThread.getName());

    通过这个方法可以获取当前线程信息并进行相应的操作。

  • start(): 用于启动一个线程,使其从新建状态进入就绪状态,然后等待操作系统调度执行。调用start()方法后,虚拟机内部会调用该线程的run()方法。多次调用start()会导致异常,因此确保只调用一次。

  • yield(): 表示当前线程愿意放弃CPU时间片,使其他同等优先级的线程有机会运行。但这并不是强制性的,实际调度结果取决于JVM和操作系统的实现。

    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            Thread.yield();
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    });
    t1.start();

  • sleep(long millis): 让当前线程暂停指定毫秒数的时间,交出CPU使用权给其他线程。此方法会抛出InterruptedException,需妥善处理。

  • join(): 使当前线程等待另一个线程结束。当在一个线程上调用t.join()时,当前线程将被阻塞直到线程t完成其任务。

    Thread threadA = new Thread(() -> {
        // 执行耗时任务
    });
    Thread threadB = new Thread(() -> {
        try {
            threadA.join(); // 等待threadA执行完毕
            System.out.println("Thread B continues after A");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    threadA.start();
    threadB.start();

Thread类与Runnable接口的比较

  • 继承Thread类的方式可以直接访问Thread类中的诸多方法,但受到Java单继承的限制,若需要扩展已有类则不适用。
  • 实现Runnable接口更符合面向对象原则,因为Runnable是接口,可以实现多继承,降低了线程对象和线程任务之间的耦合度。并且,采用Runnable时,可以通过灵活组合Thread类的各种构造方法来创建线程实例。

例如,考虑以下两个实现方式:

// 继承Thread类方式
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

// 实现Runnable接口方式
public class RunnableTask implements Runnable {
    @Override
    public void run() {
        // 线程执行逻辑
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        Thread thread = new Thread(task, "MyRunnableThread");
        thread.start();
    }
}

总结来说,尽管两种方式都可以创建并启动线程,但在复杂应用中,由于其灵活性和设计原则上的优势,通常推荐优先采用实现Runnable接口的方式来定义线程任务。同时结合Thread类提供的各种方法,可以更好地控制线程的行为和状态。

异步模型与Future接口


异步模型与Future接口

在Java多线程编程中,为了支持执行有返回值的任务并获取其结果,JDK引入了Callable接口和Future接口。这两种接口为开发者提供了处理异步任务的强大工具。

Callable接口 Callable接口提供了一个call()方法,它具有返回类型并且可以抛出异常,这使得线程能够执行一个可能需要较长时间且有明确结果的计算任务。例如:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CallableExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建可缓存线程池
        ExecutorService executor = Executors.newCachedThreadPool();

        // 自定义实现Callable接口的任务类
        Callable<Integer> task = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Thread.sleep(1000); // 模拟耗时操作
                return 42// 返回计算结果
            }
        };

        // 提交任务到线程池并获取Future对象
        Future<Integer> futureResult = executor.submit(task);

        // 使用get方法阻塞等待结果,并打印出来
        System.out.println("Future result: " + futureResult.get());

        // 关闭线程池
        executor.shutdown();
    }
}

在这个例子中,我们创建了一个实现了Callable接口的匿名内部类,该类在call()方法中模拟了一个耗时计算过程,并返回一个整数值作为结果。通过ExecutorService提交这个任务后,得到一个Future对象,然后调用其get()方法来阻塞等待计算结果。

Future接口 Future接口代表了一个异步计算的结果,它提供了几个关键方法:

  • cancel(boolean mayInterruptIfRunning):试图取消正在执行的任务,如果mayInterruptIfRunning为true,则会尝试中断线程。
  • isCancelled():检查是否已经取消了此任务。
  • isDone():判断任务是否已完成,无论正常结束还是被取消。
  • get()get(long timeout, TimeUnit unit):阻塞等待直到计算完成或超时,然后获取计算结果。如果不希望无限期等待,可以选择带有超时参数的方法。

使用Future接口的一个重要优势在于,它可以让我们以同步或异步的方式控制任务的执行和结果的获取。同时,由于Future提供了取消任务的能力,因此相比Runnable更适合那些需要随时中止的任务场景。

此外,JDK还提供了FutureTask类,它是Future接口和Runnable接口的实现类,既可以作为一个Runnable对象交给Thread或者ExecutorService执行,又能持有并管理计算结果。通过FutureTask,我们可以更方便地进行异步计算以及状态跟踪。

FutureTask类与用途

FutureTask类在Java多线程编程中扮演着关键角色,它是对Runnable接口和Future接口的融合实现。作为一个可运行且具有未来结果的任务封装器,FutureTask可以将任务提交给线程执行,并通过Future接口提供对任务状态、取消操作以及获取计算结果的支持。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class FutureTaskExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建一个FutureTask实例,使用Callable实现的任务
        Callable<Integer> callable = () -> {
            Thread.sleep(1000); // 模拟耗时计算
            return 123// 返回计算结果
        };

        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        // 将FutureTask作为Runnable对象提交到线程池或直接创建新线程启动
        Thread thread = new Thread(futureTask);
        thread.start();

        // 当前线程等待FutureTask完成并获取结果
        Integer result = futureTask.get();
        System.out.println("FutureTask计算结果: " + result);
    }
}

在上述示例中,我们首先定义了一个实现了Callable接口的任务对象,然后通过FutureTask来包装这个任务。FutureTask既可以直接传递给Thread对象使其成为一个可运行的任务,也可以提交给ExecutorService进行异步执行。当调用其get()方法时,当前线程会阻塞直到任务完成,之后返回计算得到的结果。

FutureTask的主要优势在于它为异步任务提供了生命周期管理功能,包括:

  • 任务调度:FutureTask可以被多个线程安全地调度,确保任务仅被执行一次。
  • 任务状态跟踪:内部维护了任务的状态机,可以通过isDone()等方法检查任务是否已完成或已被取消。
  • 结果获取:get()方法允许在任务完成后获取计算结果,支持阻塞等待和超时机制。
  • 任务取消:调用cancel方法可以尝试中断正在执行的任务,或者防止尚未开始的任务执行。

总之,FutureTask是Java并发框架中的重要组件,它结合了Runnable和Future的优点,使得异步任务的管理和控制更为灵活便捷,极大地提高了程序设计的效率和代码的可读性。

FutureTask的状态变迁


在Java多线程编程中,FutureTask类作为实现RunnableFuture接口的实例,不仅封装了任务执行逻辑,还负责管理任务状态。FutureTask内部维护了一个volatile的int型变量state来表示其生命周期中的不同状态。

  1. NEW:初始状态,表示FutureTask尚未开始执行。
  2. COMPLETING:瞬态状态,表示任务正在完成,即call()方法正在运行或者结果已经设置,等待后续的完成处理过程。
  3. NORMAL:正常结束状态,任务已成功执行并设置了结果。
  4. EXCEPTIONAL:异常结束状态,任务在执行过程中抛出了未捕获的异常,结果被设置为该异常对象。
  5. CANCELLED:取消状态,通过调用cancel方法且成功取消了任务,此时任务不会继续执行。
  6. INTERRUPTING:中断中状态,也是瞬态状态,表明正在进行取消操作,并尝试中断底层的任务执行线程。
  7. INTERRUPTED:已中断状态,意味着任务在取消过程中已被成功中断。

这些状态之间的转换路径如下:

  • NEW -> COMPLETING -> NORMAL 或 EXCEPTIONAL
  • NEW -> CANCELLED
  • NEW -> INTERRUPTING -> INTERRUPTED

FutureTask的设计确保了任务只执行一次,即使在并发环境下也能正确地管理状态变迁和结果返回。例如,在高并发场景下,如果有多个线程同时尝试启动一个FutureTask,它会保证仅有一个线程实际执行任务,其余线程等待结果。

以下是一个简单的FutureTask状态变迁的示例代码片段,但请注意,由于FutureTask内部对状态变更做了严格控制和同步处理,我们无法直接模拟所有状态变迁的过程:

public class FutureTaskStateExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Thread.sleep(1000); // 模拟耗时计算
                return 42// 正常返回结果
            }
        });

        Thread t = new Thread(futureTask);
        t.start();

        // 在任务执行期间尝试取消
        futureTask.cancel(true);

        // 判断任务是否已取消或已完成
        if (futureTask.isCancelled()) {
            System.out.println("任务已取消");
        } else if (futureTask.isDone()) {
            System.out.println("任务已完成,结果:" + futureTask.get());
        }

        // 根据实际情况,这里可能输出"任务已取消"或"任务已完成"
    }
}

这段代码创建了一个FutureTask实例并在新线程中执行。在任务执行过程中尝试取消,根据最终状态判断任务是已取消还是已完成。真实情况下,FutureTask会确保按照预定义的状态变迁规则进行切换。

总结


Java多线程编程提供了丰富的类与接口,便于开发者高效地创建、管理和控制线程。在实际应用中,我们可以通过以下几种方式来实现:

  • 继承Thread类或实现Runnable接口:前者通过重写run()方法定义线程任务;后者更符合面向对象原则且不受单继承限制,允许通过构造函数传递Runnable实例给Thread类以启动新线程。示例代码展示了如何通过这两种途径创建并运行线程。
// 继承Thread类
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

// 实现Runnable接口
public class RunnableTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable Task running");
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        Thread thread = new Thread(task);
        thread.start();
    }
}

  • 使用Future和Callable进行异步计算:当需要获取线程执行结果时,可以结合Callable和Future接口实现异步模型。FutureTask作为这两个接口的实现,兼顾了任务执行和结果返回的功能。例如:
import java.util.concurrent.*;

public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = () -> { return calculate(); };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        Thread executor = new Thread(futureTask);
        executor.start();

        Integer result = futureTask.get(); // 阻塞等待结果
        System.out.println("FutureTask returned: " + result);
    }

    private static Integer calculate() {
        try {
            Thread.sleep(1000);
            return 42;
        } catch (InterruptedException e) {
            return -1;
        }
    }
}

  • FutureTask状态变迁:FutureTask内部维护了多种状态,如NEW、COMPLETING、NORMAL等,用于准确反映任务从初始化到完成或取消的全过程,确保并发环境下的正确性。

综上所述,在Java多线程编程中,通过灵活运用Thread、Runnable、Callable以及Future/FutureTask等工具,开发者能够更好地设计和管理复杂的并发场景,并利用异步编程提高系统性能与响应速度。深入理解这些类与接口的工作机制及应用场景,是构建高效稳定多线程应用程序的关键所在。同时,学习线程组、线程优先级等相关概念,将有助于进一步提升对Java多线程编程的全面掌控能力。

本文使用 markdown.com.cn 排版

posted @ 2024-01-29 11:47  解码猿  阅读(528)  评论(2编辑  收藏  举报