likes
comments
collection
share

学习java.util.concurrent包1:基础线程机制-Thread、Runnable和Callable

作者站长头像
站长
· 阅读数 23

引言

学习java.util.concurrent包下的类,建议按照以下顺序进行,这样可以逐步理解并发编程的基本概念和高级特性:

学习java.util.concurrent包1:基础线程机制-Thread、Runnable和Callable

  1. 基础线程机制:

    • Thread: 虽然不在java.util.concurrent包中,但理解基本的Thread类是开始学习并发编程的基础。
    • RunnableCallable: 了解如何定义可以在线程中执行的任务。
  2. 基本的任务执行框架:

    • ExecutorExecutors: 学习如何使用这些接口和类来管理线程的创建和任务的提交。
    • ExecutorService: 深入了解如何管理任务的生命周期,包括关闭线程池。
  3. 同步工具类:

    • Future: 理解如何表示异步计算的结果,并等待执行完成。
    • Semaphore, CountDownLatch, CyclicBarrier, Exchanger, Phaser: 学习这些同步辅助类,了解它们如何协调多个线程间的合作。
  4. 并发集合:

    • ConcurrentHashMap: 学习如何使用线程安全的Map实现。
    • ConcurrentLinkedQueue, CopyOnWriteArrayList: 了解不同的线程安全集合及其适用场景。
  5. 锁机制:

    • LockReentrantLock: 学习如何使用显式锁来控制同步。
    • ReadWriteLockReentrantReadWriteLock: 理解如何使用读写锁来提高并发读取性能。
  6. 原子变量:

    • AtomicInteger, AtomicLong, AtomicReference等: 掌握如何使用原子变量进行无锁编程。
  7. 高级并发工具:

    • ThreadPoolExecutorScheduledThreadPoolExecutor: 深入理解线程池的工作原理和如何自定义线程池的行为。
    • CompletionService: 学习如何管理异步任务的执行和结果收集。
  8. Fork/Join框架:

    • ForkJoinPoolRecursiveTask: 学习分而治之的并发策略,以及如何利用这个框架来提高并行计算的效率。

概述

并发编程是Java开发中不可或缺的一部分,它允许开发者编写能够充分利用多核处理器性能的应用程序。本文从Java并发编程的基石——Thread类、Runnable接口以及Callable接口入手,详细解释了如何使用它们来创建和管理线程。讨论了如何正确启动和停止线程,如何等待线程的完成,以及为什么不能重启一个已经运行结束的线程。

正文

在现代软件开发中,利用多核处理器的能力通过并发编程提高应用性能已经成为一项必备技能。Java作为一门历史悠久的编程语言,提供了一套丰富的并发编程工具,其中Thread类、Runnable接口和Callable接口是最基础的组件。本文将深入理解这些组件的使用方法和最佳实践。

学习基础线程机制时,理解Thread类、Runnable接口和Callable接口的工作原理是至关重要的。这些构建块为在Java中进行并发编程提供了基础。

Thread 类

Thread类代表了一个线程的实例。在Java中,线程是程序中的一个独立执行路径。每个线程都有自己的程序计数器、栈和局部变量,但可以访问共享的内存空间和对象。

当创建了Thread类的一个实例并调用它的start()方法时,JVM会为这个新线程分配资源,并调用它的run()方法来执行指定的代码。

创建和启动线程

package com.dereksmart.crawling.core;
/**
 * @Author derek_smart
 * @Date 2024/7/29 7:55
 * @Description Thread测试类
 */
public class MyThread extends Thread {
    public void run() {
        // Code that executes on the new thread
        System.out.println("Hello,Derek Smart");
    }
}

 class TMain {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // This will call MyThread's run() method on a new thread
    }
}

停止线程

class MyThread1 extends Thread {
    public void run() {
        int i = 0;
        while (!interrupted()) {
            // 执行任务
            System.out.println("Hello,Derek Smart1");
        }
    }
}

 MyThread1 thread1 = new MyThread1();
        thread1.start(); // This will call MyThread's run() method on a new thread
        Thread.sleep(1);
        thread1.interrupt(); // 请求中断

学习java.util.concurrent包1:基础线程机制-Thread、Runnable和Callable

等待线程完成

myThread.join(); // 在当前线程中等待myThread线程完成

Runnable 接口

Runnable接口应该由任何类实现,如果实例打算通过某个线程执行。它是一个函数式接口,定义了一个无参数的方法run()

实现Runnable接口允许类不必继承Thread类就能在新线程中执行。可以创建一个实现了Runnable接口的实例,并将它作为参数传递给Thread类的构造器。

创建和启动线程

package com.dereksmart.crawling.core;
/**
 * @Author derek_smart
 * @Date 2024/7/29 7:55
 * @Description Runnable测试类
 */
public class MyRunnable implements Runnable {
    public void run() {
        // Code that executes on the new thread
        System.out.println("Hello,Derek Smart");
    }
}

 class RMain {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // This will call MyRunnable's run() method on a new thread
    }
}

Callable 接口

Callable接口是一个泛型接口,定义了一个返回值的call()方法,并且可以抛出异常。Callable接口通常用于那些需要返回结果的场景。

Runnable不同,Callablecall()方法可以返回一个值,并且可以抛出一个异常。Callable任务需要提交给ExecutorService,它在执行后返回一个Future对象,通过这个Future对象可以获取Callable的返回值。

创建和启动线程

package com.dereksmart.crawling.core;

import java.util.concurrent.*;
/**
 * @Author derek_smart
 * @Date 2024/7/29 7:55
 * @Description Callable测试类
 */
public  class MyCallable implements Callable<String> {
    public String call() throws Exception {
        return "Callable completed";
    }
}

 class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<String> future = executorService.submit(new MyCallable());

        // 等待执行完成并获取结果
        String result = future.get(); // 阻塞直到任务完成

        try {
            future.get(1000,TimeUnit.MINUTES);
        }catch (InterruptedException e){

        }catch (ExecutionException e){

        }catch (TimeoutException e){

        }

        future.cancel(true);
        System.out.println(result);
        future.cancel(true);
        // 关闭线程池
       // executorService.shutdown();
    }
}

停止线程

Callable任务一旦开始执行,就不能直接中断。但是,可以通过Future调用cancel(true)方法来尝试取消它,如果任务正在运行,会尝试中断它。

future.cancel(true); // 尝试取消正在执行的任务

等待线程完成

使用Future.get()方法等待Callable任务完成。如果任务已经完成,这个方法会立即返回结果;否则,它会阻塞直到任务完成。

String result = future.get(); // 阻塞直到任务完成

等待线程完成 超时机制

使用Future.get()方法等待Callable任务完成。如果任务已经完成,这个方法会立即返回结果;否则,它会阻塞直到任务完成。

try {
    future.get(1000,TimeUnit.MINUTES); //1秒超时报错
}catch (InterruptedException e){

}catch (ExecutionException e){

}catch (TimeoutException e){

}

重启线程

在Java中,线程一旦完成执行就不能重新启动。如果需要再次执行任务,需要创建一个新的线程实例。

其他核心方法

  • Thread.sleep(long millis): 当前线程暂停指定的毫秒数。
  • Thread.yield(): 提示线程调度器当前线程愿意放弃其当前使用的处理器。这是对线程调度器的一个提示,调度器可能会忽略这个提示。
  • Thread.currentThread(): 返回当前正在执行的线程对象的引用。
  • Thread.setDaemon(boolean on): 将线程标记为守护线程或用户线程。守护线程是指在后台为其他线程提供服务的线程,如垃圾回收线程。

请注意,Thread.stop()方法已经被废弃,因为它是不安全的。正确的停止线程的方式是使用中断或者设置一个标志变量。

原理

  • Thread: 每个Thread对象代表一个执行线程。start()方法会告诉JVM启动一个新线程,这个新线程会调用run()方法。Thread类提供了管理线程生命周期的方法,比如interrupt(), join(), sleep(), yield()等。
  • Runnable: Runnable接口允许将任务的定义与执行分离。它没有返回值,也不能抛出检查型异常。它可以用于创建可以运行在Thread上的任务。
  • Callable: Callable接口与Runnable类似,但它可以返回一个结果,并且可以抛出检查型异常。Callable任务通常用于那些需要返回值的场景,并且需要提交给ExecutorService来执行。

在Java中,创建线程的两种常见方式是使用Thread类和实现Runnable接口。下面是各自的优缺点:

使用 Thread 类优点:

  • 简单直观:继承Thread类并重写run方法的方式简单直观,对于新手来说容易理解。
  • 直接控制线程:由于Thread类本身控制线程的执行,因此可以直接调用start, interrupt等方法来管理线程。

缺点:

  • 不支持多重继承:在Java中,继承了Thread类就不能再继承其他类,这限制了类的灵活性。
  • 资源消耗较大:每个线程都是一个重量级的对象,涉及与操作系统的交互。
  • 扩展性差:如果需要将线程的执行逻辑与Thread类的管理分开,或者需要将线程逻辑与线程池等并发工具结合使用,直接使用Thread类就不那么方便了。

实现 Runnable 接口

优点:

  • 分离任务和执行Runnable接口只代表一个要执行的任务,它不控制线程的生命周期。这使得任务代码可以被多个执行者(如线程或线程池)重用。
  • 更好的资源共享:实现Runnable的类更容易共享资源。多个线程可以接收同一个Runnable实例,并且可以访问相同的资源,无需创建多个副本。
  • 更高的扩展性:由于Runnable是一个接口,你的类可以实现Runnable同时还可以继承其他类,提供了更好的扩展性。
  • 适用于并发工具Runnable接口与java.util.concurrent包中的并发工具兼容,可以与ExecutorService等高级并发工具一起使用,方便管理线程生命周期和任务执行。

缺点:

  • 无法直接控制线程:因为Runnable只是任务的抽象,它本身不提供对线程的直接控制,如中断或获取线程状态等操作。这些需要在Thread实例上进行。
  • 需要额外的步骤创建线程:实现Runnable后,还需要创建一个Thread实例并将Runnable传递给它,然后才能启动线程。

实现 Callable 接口

Callable是类似于Runnable的接口,但它允许任务返回值,并且可以抛出异常。

优点:

  • 有返回值Callable可以返回执行结果,这是Runnable无法提供的。
  • 能抛出异常:与Runnable不同,Callable中的call()方法允许抛出异常,使得错误处理更加灵活。

缺点:

  • 需要配合Future使用:为了获取Callable任务的返回值,通常需要使用Future对象,这增加了编程的复杂性。
  • 不直接与Thread关联:和Runnable一样,Callable任务需要提交给ExecutorService,由线程池管理执行。

总的来说,实现RunnableCallable接口通常比继承Thread类更灵活,特别是在使用线程池和执行器框架的现代并发编程中。然而,直接使用Thread类在某些简单的情况下可能更方便,尤其是当需要直接管理线程的生命周期时。

结论

通过本文的学习,了解了Java并发编程的基础,并掌握了如何使用ThreadRunnableCallable来创建并发程序。同时,也认识到了现代并发工具的重要性,并学会了如何将它们应用于实际开发中,以实现更高效、可靠的并发解决方案。随着对这些工具的深入理解,将能够编写出更加健壮和高性能的Java应用程序。

转载自:https://juejin.cn/post/7396157773313212454
评论
请登录