学习java.util.concurrent包1:基础线程机制-Thread、Runnable和Callable
引言
学习java.util.concurrent
包下的类,建议按照以下顺序进行,这样可以逐步理解并发编程的基本概念和高级特性:
-
基础线程机制:
Thread
: 虽然不在java.util.concurrent
包中,但理解基本的Thread
类是开始学习并发编程的基础。Runnable
和Callable
: 了解如何定义可以在线程中执行的任务。
-
基本的任务执行框架:
Executor
和Executors
: 学习如何使用这些接口和类来管理线程的创建和任务的提交。ExecutorService
: 深入了解如何管理任务的生命周期,包括关闭线程池。
-
同步工具类:
Future
: 理解如何表示异步计算的结果,并等待执行完成。Semaphore
,CountDownLatch
,CyclicBarrier
,Exchanger
,Phaser
: 学习这些同步辅助类,了解它们如何协调多个线程间的合作。
-
并发集合:
ConcurrentHashMap
: 学习如何使用线程安全的Map实现。ConcurrentLinkedQueue
,CopyOnWriteArrayList
: 了解不同的线程安全集合及其适用场景。
-
锁机制:
Lock
和ReentrantLock
: 学习如何使用显式锁来控制同步。ReadWriteLock
和ReentrantReadWriteLock
: 理解如何使用读写锁来提高并发读取性能。
-
原子变量:
AtomicInteger
,AtomicLong
,AtomicReference
等: 掌握如何使用原子变量进行无锁编程。
-
高级并发工具:
ThreadPoolExecutor
和ScheduledThreadPoolExecutor
: 深入理解线程池的工作原理和如何自定义线程池的行为。CompletionService
: 学习如何管理异步任务的执行和结果收集。
-
Fork/Join框架:
ForkJoinPool
和RecursiveTask
: 学习分而治之的并发策略,以及如何利用这个框架来提高并行计算的效率。
概述
并发编程是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(); // 请求中断
等待线程完成
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
不同,Callable
的call()
方法可以返回一个值,并且可以抛出一个异常。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
,由线程池管理执行。
总的来说,实现Runnable
或Callable
接口通常比继承Thread
类更灵活,特别是在使用线程池和执行器框架的现代并发编程中。然而,直接使用Thread
类在某些简单的情况下可能更方便,尤其是当需要直接管理线程的生命周期时。
结论
通过本文的学习,了解了Java并发编程的基础,并掌握了如何使用Thread
、Runnable
和Callable
来创建并发程序。同时,也认识到了现代并发工具的重要性,并学会了如何将它们应用于实际开发中,以实现更高效、可靠的并发解决方案。随着对这些工具的深入理解,将能够编写出更加健壮和高性能的Java应用程序。
转载自:https://juejin.cn/post/7396157773313212454