一文详解如何在Java中启动线程
作者:老谭说架构
1. 简介
今天要跟大家聊一聊Java多线程的原理和使用方式,Java多线程是提高程序性能的重要机制。了解线程生命周期、同步机制、线程池和线程间通信等概念,可以帮助我们编写出高效、可靠的多线程程序。
Java 线程的生命周期可以分为以下几个状态:
1. 新建 (New): 当使用 new Thread() 创建线程对象时,线程处于新建状态。此时线程尚未启动,还没有分配系统资源。
2. 就绪 (Runnable): 当调用线程对象的 start() 方法时,线程进入就绪状态。此时线程已准备好运行,但尚未获得 CPU 时间片,需要等待操作系统分配。
3. 运行 (Running): 线程获得 CPU 时间片,开始执行线程体代码,进入运行状态。
4. 阻塞 (Blocked): 当线程遇到以下情况时,会进入阻塞状态:
- 等待 (Waiting): 线程调用 Object.wait() 方法等待某个特定事件发生。
- 睡眠 (Sleeping): 线程调用 Thread.sleep() 方法进入睡眠状态,等待一段时间后自动恢复运行。
- I/O 阻塞: 线程等待 I/O 操作完成,例如读取文件或网络数据。
- 锁阻塞: 线程试图获取某个锁,但锁被其他线程占用,导致阻塞。
5. 终止 (Terminated): 线程执行完 run() 方法中的代码,或者遇到异常而退出,进入终止状态。
线程状态转换:
- 新建 -> 就绪: 调用 start() 方法。
- 就绪 -> 运行: 操作系统分配 CPU 时间片。
- 运行 -> 阻塞: 线程遇到等待、睡眠、I/O 阻塞、锁阻塞等情况。
- 阻塞 -> 就绪: 等待的事件发生、睡眠时间结束、I/O 操作完成、获得锁。
- 运行 -> 终止: run() 方法执行完毕、遇到异常。
其他重要概念:
- 守护线程 (Daemon Thread): 守护线程是为其他线程服务的线程,例如垃圾回收线程。当所有非守护线程都终止后,守护线程也会自动终止。
- 线程优先级 (Thread Priority): 线程可以设置优先级,优先级高的线程更容易获得 CPU 时间片。
2. 运行线程的基础知识
我们可以利用Thread框架轻松的编写一些在并行线程中运行的逻辑。
让我们尝试一个基本的例子,通过扩展Thread类:
public class NewThread extends Thread { public void run() { long startTime = System.currentTimeMillis(); int i = 0; while (true) { System.out.println(this.getName() + ": New Thread is running..." + i++); try { //Wait for one sec so it doesn't print too fast Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ... } } }
现在我们编写第二个类来初始化并启动我们的线程:
public class SingleThreadExample { public static void main(String[] args) { NewThread t = new NewThread(); t.start(); } }
我们应该 对处于NEW状态(相当于未启动)的线程 调用start()方法。否则,Java 将抛出IllegalThreadStateException异常的实例。
现在假设我们需要启动多个线程:
public class MultipleThreadsExample { public static void main(String[] args) { NewThread t1 = new NewThread(); t1.setName("MyThread-1"); NewThread t2 = new NewThread(); t2.setName("MyThread-2"); t1.start(); t2.start(); } }
我们的代码看起来仍然非常简单并且与我们在网上找到的示例非常相似。
当然,这与可用于生产的代码还相去甚远,以正确的方式管理资源至关重要,以避免过多的上下文切换或过多的内存使用。
因此,为了做好生产准备,我们现在需要编写额外的样板来处理:
- 持续创建新线程
- 并发活动线程数
- 线程释放:对于守护线程来说非常重要,可以避免泄漏
如果我们愿意,我们可以为所有这些情况甚至更多情况编写自己的代码,但我们为什么要重新发明轮子呢?
3. ExecutorService框架
ExecutorService实现了线程池设计模式(也称为复制工作器或工作组模型),并负责我们上面提到的线程管理,此外它还添加了一些非常有用的功能,如线程可重用性和任务队列 。
线程可重用性尤其重要:在大型应用程序中,分配和释放许多线程对象会产生大量内存管理开销。
利用工作线程,我们可以最大限度地减少线程创建造成的开销。 为了简化池配置,ExecutorService带有一个简单的构造函数和一些自定义选项,例如队列的类型、最小和最大线程数及其命名约定。
4. 使用执行器启动任务
有了这个强大的框架,我们可以把思维从启动线程转变为提交任务。
让我们看看如何向执行器提交异步任务:
ExecutorService executor = Executors.newFixedThreadPool(10); ... executor.submit(() -> { new Task(); });
我们可以使用两种方法:execute(不返回任何内容)和submit(返回封装了计算结果的Future )。
5. 使用CompletableFutures启动任务
要从Future对象中检索最终结果,我们可以使用 该对象中可用的get 方法,但这会阻塞父线程,直到计算结束。
或者,我们可以通过在任务中添加更多逻辑来避免阻塞,但我们必须增加代码的复杂性。 Java 1.8 在Future构造之上引入了一个新框架,以便更好地处理计算结果:CompletableFuture。
CompletableFuture 实现了CompletableStage,它增加了大量方法来附加回调并避免了在结果准备好后运行操作所需的所有管道。
提交任务的实现就简单多了:
CompletableFuture.supplyAsync(() -> "Hello");
supplyAsync接受一个Supplier,其中包含我们想要异步执行的代码——在我们的例子中是 lambda 参数。
该任务现在隐式提交给 ForkJoinPool.commonPool() ,或者我们可以指定我们喜欢的Executor作为第二个参数。
6. 运行延迟或周期性任务
在处理复杂的 Web 应用程序时,我们可能需要在特定时间(或许是定期)运行任务。
Java 有一些工具可以帮助我们运行延迟或重复的操作:
- 定时器
- java.util.concurrent.ScheduledThreadPoolExecutor
6.1. 计时器
计时器是一种用于安排任务在后台线程中将来执行的工具。
任务可以被安排一次性执行,或者定期重复执行。
如果我们想在延迟一秒钟后运行任务,让我们看看代码是什么样的:
TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on: " + new Date() + "n" + "Thread's name: " + Thread.currentThread().getName()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; timer.schedule(task, delay);复制
现在让我们添加一个重复的计划:
timer.scheduleAtFixedRate(repeatedTask, delay, period);复制
这一次,任务将在指定的延迟后运行,并且在经过一段时间后重复运行。
6.2. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor具有与 Timer类 类似的方法:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); ScheduledFuture<Object> resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
为了结束我们的示例,我们使用 scheduleAtFixedRate() 来执行重复任务:
ScheduledFuture<Object> resultFuture = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);
上述代码将在初始延迟 100 毫秒后执行一项任务,之后每 450 毫秒执行一次相同的任务。
如果处理器不能在下一次发生之前及时完成处理任务,则 ScheduledExecutorService 将等到当前任务完成后再开始下一个任务。
为了避免这段等待时间,我们可以使用 scheduleWithFixedDelay() ,正如其名称所述,它可以保证任务迭代之间的固定长度延迟。
6.3. 哪个工具更好?
如果我们运行上述例子,计算的结果看起来是一样的。
那么, 我们如何选择正确的工具?
当一个框架提供多种选择时,了解底层技术以做出明智的决定非常重要。
让我们尝试更深入地探究一下。
计时器:
- 不提供实时保证:它使用 Object.wait (long) 方法来安排任务
- 只有一个后台线程,因此任务按顺序运行,长时间运行的任务可能会延迟其他任务
- TimerTask中抛出的运行时异常会杀死唯一可用的线程,从而杀死Timer
ScheduledThreadPoolExecutor:
- 可以配置任意数量的线程
- 可以利用所有可用的 CPU 核心
- 捕获运行时异常并让我们根据需要处理它们(通过重写ThreadPoolExecutor的 afterExecute方法)**
- 取消引发异常的任务,同时让其他任务继续运行
- 依靠操作系统调度系统来跟踪时区、延迟、太阳时等。
- 如果我们需要多个任务之间的协调,则提供协作 API,例如等待所有提交的任务完成
- 提供更好的 API 来管理线程生命周期
7. Future和ScheduledFuture之间的区别
在我们的代码示例中,我们可以观察到ScheduledThreadPoolExecutor 返回特定类型的Future:ScheduledFuture。
ScheduledFuture 扩展了Future和Delayed接口,因此继承了额外的方法getDelay ,该方法返回与当前任务相关的剩余延迟。它由RunnableScheduledFuture扩展,并添加了一个方法来检查任务是否是周期性的。
ScheduledThreadPoolExecutor 通过内部类ScheduledFutureTask实现所有这些构造,并使用它们来控制任务生命周期。
8. 结论
Java 多线程在各种应用场景中都发挥着重要作用,可以显著提高程序效率和响应速度。以下列举一些常见的 Java 多线程使用场景:
1. 并发处理:
- 网络应用程序: 多个线程可以同时处理来自多个客户端的请求,提高服务器的吞吐量和响应速度。
- 文件处理: 多个线程可以同时读取、写入或处理多个文件,加速文件操作。
- 数据库操作: 多个线程可以同时执行数据库查询或更新操作,提高数据库的并发性能。
2. 用户界面响应:
- 图形界面应用程序: 多个线程可以将耗时的操作(如图片加载、数据处理)放到后台执行,避免阻塞主线程,保持用户界面的流畅响应。
- 游戏开发: 多个线程可以同时处理游戏逻辑、渲染图形、播放声音等任务,提高游戏流畅度和响应速度。
3. 并行计算:
- 科学计算: 多个线程可以同时执行复杂的数学计算,加速计算过程。
- 数据分析: 多个线程可以同时处理大量数据,加快数据分析速度。
- 机器学习: 多个线程可以同时训练模型,提高模型训练效率。
4. 任务调度:
- 任务管理系统: 多个线程可以同时执行不同的任务,提高任务处理效率。
- 定时任务: 多个线程可以按照指定的时间间隔或时间点执行任务。
5. 异步操作:
- 网络通信: 使用多线程可以异步发送和接收网络数据,提高网络通信效率。
- 文件上传/下载: 多线程可以异步执行文件上传/下载操作,避免阻塞主线程。
6. 其他应用场景:
- 多线程测试: 可以使用多线程模拟多个用户同时访问系统,进行压力测试。
- 并发编程框架: 许多并发编程框架(如 Akka、RxJava)都是基于多线程实现的。
以上就是一文详解如何在Java中启动线程的详细内容,更多关于Java启动线程的资料请关注脚本之家其它相关文章!