一文读懂Java多线程背后的故事
Java 是一种广泛使用的编程语言,而多线程是 Java 程序员必不可少的一部分。Java 的多线程支持具有确保数据同步、最大化利用 CPU 资源、并行处理任务等众多优点。多线程技术已经被广泛应用在 Web 应用、移动应用和游戏开发等领域中。
我写东西一般喜欢从实际应用场景出发,为您详细介绍 Java 多线程的各个方面的实际应用及背景。
多线程的概念最早可以追溯到计算机科学的起源之一,也是第一个电子计算机ENIAC的开发过程中。当时,计算速度相对较慢,为了充分利用CPU资源,信息科学家们开始探索将一个任务拆分成多个小部分并同时执行的方法。这就是多线程的雏形。
1970年代后期,操作系统出现了抢占式调度策略,这意味着一个进程可以被打断并转而执行另一个进程,实现了不同的程序共享CPU时间。在这种情况下,处理器需要快速地切换上下文并管理多个进程之间的共享资源,多线程编程变得更加重要。
随着工业界软件和互联网的迅速发展,更多的应用程序需要支持并发处理,以达到高效、实时的数据处理需求。而多线程编程正好可以提供这种并发性。
不仅如此,多核处理器技术的出现使得多线程编程变得更加必要。将单个进程分解成多个线程并在多核处理器上同时运行可以提高整体处理性能。
因此,多线程编程
已经成为现代计算机编程中重要的一部分,而随着新型技术的出现,如云计算、大数据等,多线程编程
的重要性将会越来越突出。
1. 多线程的使用原则
在使用Java多线程的过程中,有一些原则需要遵守,以确保程序的正确性、可靠性和高效性。以下是几个常见的多线程使用原则:
-
避免竞态条件:竞态条件是指当两个或更多的线程访问共享资源时,由于执行顺序不确定而导致的问题。为了避免竞态条件的发生,必须要使用同步机制(如synchronized关键字或Lock对象)和互斥量来保证线程的安全性。
-
最大化利用CPU资源:在多线程程序中,为了达到最高程度的并行ism,应该将大任务分解成小任务并在多个线程上同时执行,以充分利用CPU资源。
-
避免死锁:死锁是指当两个或多个线程相互等待对方的锁时,导致程序无限期地阻塞的情况。为了避免死锁,应该尽量避免嵌套锁,以及使用统一的锁顺序来避免死锁的发生。
-
减少上下文切换:上下文切换是指在多线程环境下,在处理器从一个线程转换到另一个线程时,需要保存当前上下文并加载另一个线程的上下文。上下文切换的开销是很大的,因此应该尽量减少上下文切换的次数,例如通过使用线程池来共享线程资源。
-
避免线程饥饿:线程饥饿是指一个线程由于等待某些资源而无法执行的情况。为了避免线程饥饿,可以使用公平锁来保证所有的线程都有机会获得资源,或者使用线程优先级来控制线程的调度顺序。
以上是多线程使用中的一些常见原则,实际的多线程编程中还有很多需要考虑的问题,需要根据实际情况进行相应的处理。
2. 改善用户体验
在 Web 应用程序中,用户希望尽快得到反馈信息,而长时间的等待会导致用户体验的下降。多线程可以提高 Web 应用程序的响应速度,从而改善用户体验。例如,当用户请求一个数据集合时,可以使用多线程技术在后台异步加载数据,然后立即显示首页,同时在数据准备好后再更新页面。这样用户就能立即看到页面,并在后台加载完成时获得更多数据。
下面是一个示例,演示了如何使用多线程技术实现异步加载数据:
public class DataLoader implements Runnable {
private final DataService dataService;
private final Consumer<Data> onDataLoaded;
public DataLoader(DataService dataService, Consumer<Data> onDataLoaded) {
this.dataService = dataService;
this.onDataLoaded = onDataLoaded;
}
@Override
public void run() {
Data data = dataService.loadData();
onDataLoaded.accept(data);
}
}
public class PageController {
private final DataService dataService;
public PageController(DataService dataService) {
this.dataService = dataService;
}
public void displayPage() {
// 显示页面,提供反馈信息
showLoadingIndicator();
// 异步加载数据
DataLoader dataLoader = new DataLoader(dataService, data -> {
hideLoadingIndicator();
updatePageWithData(data);
});
Thread thread = new Thread(dataLoader);
thread.start();
}
}
在这个代码示例中,DataLoader
类负责异步加载数据,而 PageController
类负责显示页面并提供反馈信息。当 PageController
操作时,它会创建一个新线程来执行 DataLoader
,然后立即显示页面,并在数据准备好后更新页面。
3. 最大化利用 CPU 资源
使用多线程可以使 CPU 资源得到充分利用,从而提高程序的性能和效率。例如,在计算密集型任务中,将任务拆分成多个部分并使用多个线程并行执行可以大大缩短任务完成时间。下面是一个示例,演示了如何在并行环境下计算斐波那契数列:
public class FibonacciTask implements Callable<Long> {
private final int n;
public FibonacciTask(int n) {
this.n = n;
}
@Override
public Long call() {
if (n <= 1) {
return (long) n;
} else {
FibonacciTask f1 = new FibonacciTask(n - 1);
FibonacciTask f2 = new FibonacciTask(n - 2);
ForkJoinTask.invokeAll(f1, f2);
return f1.join() + f2.join();
}
}
}
public class FibonacciExample {
public static void main(String[] args) throws Exception {
int n = 50;
FibonacciTask task = new FibonacciTask(n);
long result = ForkJoinPool.commonPool().invoke(task);
System.out.println("Fibonacci number at position " + n + " is " + result);
}
}
在这个代码示例中,FibonacciTask
类负责计算斐波那契数列。当 n
大于 1 时,它将任务分解成两个子任务并使用 ForkJoinTask.invokeAll()
方法并行执行。FibonacciExample
类负责启动计算,并使用 ForkJoinPool
类中的公共池来执行任务。这样,就可以将任务拆分成多个部分并使用多个线程并行执行,以最大化利用 CPU 资源。
4. 简化代码实现
多线程技术还可以帮助简化复杂的应用程序代码。例如,在面向对象编程中,程序通常需要维护一些状态,这可能导致代码变得复杂和难以理解。使用多线程技术可以分离状态和处理过程,并将处理过程分解成多个线程以提高效率。下面是一个示例,演示了如何使用多线程技术简化代码实现:
public class BankAccount {
private int balance;
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized void withdraw(int amount) {
balance -= amount;
}
public synchronized int getBalance() {
return balance;
}
}
public class TransferTask implements Runnable {
private final BankAccount sourceAccount;
private final BankAccount targetAccount;
private final int amount;
public TransferTask(BankAccount sourceAccount, BankAccount targetAccount, int amount) {
this.sourceAccount = sourceAccount;
this.targetAccount = targetAccount;
this.amount = amount;
}
@Override
public void run() {
sourceAccount.withdraw(amount);
targetAccount.deposit(amount);
}
}
public class BankExample {
public static void main(String[] args) {
BankAccount account1 = new BankAccount();
BankAccount account2 = new BankAccount();
account1.deposit(1000);
TransferTask transferTask1 = new TransferTask(account1, account2, 500);
TransferTask transferTask2 = new TransferTask(account2, account1, 300);
Thread thread1 = new Thread(transferTask1);
Thread thread2 = new Thread(transferTask2);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Account 1 balance: " + account1.getBalance());
System.out.println("Account 2 balance: " + account2.getBalance());
}
}
在这个代码示例中,BankAccount
类负责管理银行账户的余额。每个方法都使用 synchronized
关键字来确保方法调用之间的互斥性。TransferTask
类负责执行转账操作。最后,BankExample
类负责启动转账并输出账户余额。
5. 多线程编程框架
多线程编程的框架通常是一些类或接口的集合,其目的是为了帮助开发人员更容易地编写并发代码。这些框架提供了高层次的抽象,隐藏了多线程代码中的复杂性和细节,并使得程序员能够更加专注于业务逻辑的实现。
以下是多线程框架中的几个常见抽象:
-
线程池:线程池用来管理一组已经创建的线程,以便在程序需要时重复利用它们。线程池的抽象类通常包含上限和下限线程数、任务队列、拒绝策略等属性,以及方法来提交任务,启动线程池等。使用线程池可以大大提高系统的效率,降低线程创建与销毁的开销。
-
并发集合:并发集合可以让多个线程安全地访问同一个集合。Java并发框架提供了一些并发集合,例如ConcurrentHashMap、ConcurrentLinkedQueue等。这些集合实现了线程安全的读写操作,同时不会导致锁竞争。
-
信号量(Semaphore):信号量是一种控制并发访问资源的机制,它可以保证在任何时刻,只有指定数量的线程能够访问共享资源。Java并发框架中的Semaphore类提供了这个机制的实现,通过acquire()方法获取信号量,release()方法释放信号量。Semaphore可以应用于限流、资源控制等场景。
-
同步器(Synchronizer):同步器是一种比锁更高级的并发控制机制,它可以用来实现各种复杂的同步和互斥需求。Java并发框架中的ReentrantLock、CountDownLatch和CyclicBarrier等就是同步器的典型实现。
-
Fork/Join框架:Fork/Join框架是Java 7中引入的并发框架,它建立在Executor框架之上,特别适合处理计算密集型的任务。该框架采用分治策略,将大问题拆分成多个小问题,并在多个线程上并行执行,最终将结果合并返回。
多线程编程框架的抽象层次越高,越能够帮助我们避免写出低效、高风险的代码。因此,在编写多线程应用程序时,使用已经存在的高级别的抽象是一个非常明智的选择。
6. 总结
本文介绍了 Java 多线程的实际应用场景,包括改善用户体验、最大化利用 CPU 资源和简化代码实现等。我只是涉及了一些比较基础的应用场景介绍它,还有很多其他领域可以使用 Java 多线程进行优化。Java 多线程是 Java 程序员必备的技能之一,深入理解多线程技术的应用场景可以帮助您编写更高效、更健壮的程序,很开心能够分享这些。
转载自:https://juejin.cn/post/7240052076624707645