
博主 默语带您 Go to New World. ✍ 个人主页—— 默语 的博客👦🏻 《java 面试题大全》 🍩惟余辈才疏学浅,临摹之作或有不妥之处,还请读者海涵指正。☕🍭 《MYSQL从入门到精通》数据库是开发者必会基础之一~ 🪁 吾期望此文有资助于尔,即使粗浅难及深广,亦备添少许微薄之助。苟未尽善尽美,敬请批评指正,以资改进。!💻⌨
Java作为一门强大而广泛使用的编程语言,多线程编程是其重要的特性之一。在本文中,我们将深入探讨Java多线程编程与并发控制的方方面面。我们将从多线程的基本概念入手,了解多线程编程的优势和挑战。然后,我们会介绍Java中创建和管理线程的几种方式,并探讨如何避免常见的并发问题。通过本文的学习,将能够优雅地掌控Java多线程编程,构建高效、稳定的并发应用。
我们将从多线程编程的基本概念入手,讨论为什么在某些场景下使用多线程可以提高程序性能。同时,我们也会明确多线程编程所带来的一些挑战,例如线程安全性和竞态条件等。通过实例演示,我们将学会如何创建和启动线程,以及控制线程的执行流程。
当处理涉及并发任务的程序时,多线程编程可以显著提高程序性能。在某些场景下,使用多线程可以让程序同时处理多个任务,从而利用多核处理器的优势,加快任务执行速度。特别是在涉及大量计算、IO操作或网络请求的情况下,多线程可以充分利用系统资源,提高程序的效率和响应性。
在多线程编程中,每个线程都是独立执行的,它们拥有自己的执行流程和栈空间。这意味着在某些情况下,多个线程可以并行执行任务,从而加快整体处理速度。这与单线程程序的顺序执行不同,多线程使得程序可以同时执行多个任务,从而更好地利用CPU和其他资源。
然而,多线程编程也带来了一些挑战,其中最重要的挑战之一是线程安全性。在多线程环境下,多个线程可能会同时访问共享的数据或资源,如果没有适当地进行同步和控制,可能会导致竞态条件和数据不一致的问题。竞态条件是指多个线程在没有正确同步的情况下,以不可预测的方式相互影响,从而破坏程序的正确性。
为了确保线程安全性,我们可以使用不同的机制,例如使用synchronized关键字来保护共享资源的访问,或者使用Lock接口提供更细粒度的控制。此外,Java还提供了一些并发工具,如Atomic类和Concurrent集合,来简化多线程编程并减少竞态条件的发生。
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class MultiThreadedAverageCalculation {
private static final int NUM_THREADS = 4;
private static final int LIST_SIZE = 10000;
private static List<Integer> numbers = new ArrayList<>();
public static void main(String[] args) {
// 初始化列表数据
Random random = new Random();
for (int i = 0; i < LIST_SIZE; i++) {
numbers.add(random.nextInt(100));
}
List<Thread> threads = new ArrayList<>();
// 创建并启动多个线程
for (int i = 0; i < NUM_THREADS; i++) {
Thread thread = new Thread(new AverageCalculator(), "Thread-" + i);
threads.add(thread);
thread.start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 计算最终平均值
int totalSum = 0;
for (int num : numbers) {
totalSum += num;
}
double average = (double) totalSum / LIST_SIZE;
System.out.println("Final average: " + average);
}
// 定义一个Runnable实现类,用于在多线程中执行计算
private static class AverageCalculator implements Runnable {
@Override
public void run() {
int sum = 0;
for (int i = 0; i < LIST_SIZE; i++) {
sum += numbers.get(i);
}
// 注意:在多线程环境下,这里可能存在竞态条件
numbers.add(sum);
}
}
}运行结果
Final average: 39.9543我们创建了一个包含10000个随机整数的列表numbers,然后使用4个线程并行地对其中的元素进行求和。然而,注意到在AverageCalculator的run方法中,对numbers列表的写入操作没有进行同步处理,这可能导致竞态条件和结果的不确定性。
为了解决这个问题,我们需要在AverageCalculator类的run方法中使用适当的同步机制,例如synchronized关键字或Lock接口。这样可以确保多个线程正确地对numbers列表进行操作,从而得到正确的平均值。多线程编程在某些场景下可以显著提高程序性能,但也需要仔细处理线程安全性和竞态条件等问题。合理地使用同步机制和并发工具可以确保多线程程序的正确性和高效性。
我们将深入研究线程同步与互斥机制,以确保多个线程之间的正确协作。我们会介绍Java中的锁机制,包括synchronized关键字和Lock接口的使用。同时,我们将说明如何避免死锁和其他常见的并发陷阱,以保证程序的稳定性和可靠性。
我们深入研究线程同步与互斥机制,重点介绍Java中的锁机制,包括synchronized关键字和Lock接口的使用。同时,我们还将讨论如何避免死锁和其他常见的并发陷阱,以确保程序的稳定性和可靠性。
线程同步与互斥
在多线程环境下,多个线程可能同时访问共享的资源,例如共享变量或共享数据结构。为了确保线程安全,我们需要保证在任意时刻只有一个线程能够访问共享资源,从而避免竞态条件和数据不一致的问题。这就是线程同步与互斥的关键。
Java提供了两种主要的线程同步与互斥机制:synchronized关键字和Lock接口。
使用synchronized关键字
synchronized关键字可以应用于方法或代码块,用于保护共享资源的访问。当一个线程进入synchronized方法或代码块时,它会自动获得锁,其他线程在此期间无法进入该方法或代码块,直到持有锁的线程释放锁。
public class SynchronizedDemo {
private static int sharedResource = 0;
public synchronized void synchronizedMethod() {
// 这里的代码是线程安全的
sharedResource++;
}
public void someOtherMethod() {
// 非同步代码
// ...
synchronized (this) {
// 这里的代码也是线程安全的
sharedResource--;
}
// 非同步代码
// ...
}
}使用Lock接口
Lock接口提供了更灵活的锁机制,相比synchronized关键字,它提供了更多的功能,例如可重入锁、超时获取锁、条件等待等。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockDemo {
private static int sharedResource = 0;
private static Lock lock = new ReentrantLock();
public void lockMethod() {
lock.lock();
try {
// 这里的代码是线程安全的
sharedResource++;
} finally {
lock.unlock();
}
}
}避免死锁和并发陷阱
在编写多线程程序时,我们需要格外小心避免死锁和其他常见的并发陷阱。死锁是指两个或多个线程被永久地阻塞,因为每个线程都在等待其他线程释放它所需要的锁。
为了避免死锁,我们需要注意以下几点:
Java提供了一系列并发集合类和原子操作,用于简化多线程编程的复杂性。在这一部分,我们将学习如何使用ConcurrentHashMap、ConcurrentLinkedQueue等并发集合类,以及如何利用Atomic包下的原子类来实现线程安全的计数和更新操作。通过这些工具,我们能够更高效地处理并发访问的数据结构,避免手动加锁带来的麻烦。
ConcurrentHashMap: 它是HashMap的线程安全版本,适用于多线程环境下的并发访问。与传统的synchronizedMap相比,ConcurrentHashMap通过分段锁的方式实现高效的并发操作,允许多个线程同时读取不同部分的数据而不会发生阻塞。
ConcurrentLinkedQueue: 它是一个基于链表的线程安全队列,适用于多线程环境下的并发添加和删除操作。与传统的synchronizedList相比,ConcurrentLinkedQueue提供了更好的性能,尤其在高并发环境下。
Atomic包下的原子类:Atomic包提供了一系列原子类,用于实现基本类型的原子操作。这些原子类使用了硬件级别的原子性,避免了锁竞争,能够高效地进行线程安全的计数和更新操作。例如,AtomicInteger、AtomicLong等。
使用ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 多线程同时添加键值对
Thread thread1 = new Thread(() -> map.put("A", 1));
Thread thread2 = new Thread(() -> map.put("B", 2));
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ConcurrentHashMap: " + map);
}
}使用ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentLinkedQueue;
public class ConcurrentLinkedQueueDemo {
public static void main(String[] args) {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// 多线程同时添加元素
Thread thread1 = new Thread(() -> queue.offer("A"));
Thread thread2 = new Thread(() -> queue.offer("B"));
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ConcurrentLinkedQueue: " + queue);
}
}使用Atomic包下的原子类
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
// 多线程同时增加计数
Thread thread1 = new Thread(() -> count.incrementAndGet());
Thread thread2 = new Thread(() -> count.incrementAndGet());
thread1.start();
thread2.start();
// 等待两个线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Atomic Integer: " + count);
}
}上面这些工具能够极大地简化多线程编程,避免手动加锁的麻烦,并提供高效的线程安全操作。
线程池是Java多线程编程的关键组件之一,它能够管理和复用线程,提高线程的利用率。在本节中,我们将学习如何使用ThreadPoolExecutor类来创建和管理线程池。我们还会讨论适当的线程池大小和拒绝策略选择,以满足不同应用场景的需求。
正如大家所提到的,线程池是Java多线程编程的关键组件之一,它能够管理和复用线程,提高线程的利用率,并且避免不必要的线程创建和销毁开销。在本节中,我们将学习如何使用ThreadPoolExecutor类来创建和管理线程池,并讨论适当的线程池大小和拒绝策略选择,以满足不同应用场景的需求。
使用ThreadPoolExecutor创建线程池
ThreadPoolExecutor是Java提供的一个灵活的线程池实现类。通过ThreadPoolExecutor,我们可以创建一个具有指定核心线程数、最大线程数、线程存活时间、工作队列等属性的线程池。
以下是使用ThreadPoolExecutor创建线程池的示例:
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池
int corePoolSize = 5; // 核心线程数
int maxPoolSize = 10; // 最大线程数
long keepAliveTime = 60; // 非核心线程的空闲线程存活时间(秒)
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
);
// 提交任务给线程池
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
System.out.println("Thread: " + Thread.currentThread().getName() + " is executing.");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPool.shutdown();
}
}ThreadPoolExecutor.AbortPolicy(默认):直接抛出RejectedExecutionException异常,拒绝新任务的提交。
ThreadPoolExecutor.CallerRunsPolicy:在调用线程中执行被拒绝的任务。也就是由调用execute方法的线程来执行该任务。
ThreadPoolExecutor.DiscardOldestPolicy:抛弃队列中等待时间最长的任务,然后尝试提交新的任务。
ThreadPoolExecutor.DiscardPolicy:直接抛弃被拒绝的任务,不做任何处理。
可以根据具体应用场景选择合适的拒绝策略。
在这一部分,我们将总结一些Java多线程编程的最佳实践。我们会强调避免使用全局变量、尽量使用不可变对象以及选择合适的同步策略等重要原则。通过遵循这些最佳实践,我们能够写出更健壮、可维护的并发代码。
在Java多线程编程中,遵循一些最佳实践是非常重要的,可以帮助我们编写更健壮、可维护的并发代码。以下是一些Java多线程编程的最佳实践:
下面是一个综合示例,展示了如何遵循上述最佳实践来编写一个线程安全的计数器:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadSafeCounter {
private final AtomicInteger count = new AtomicInteger(0);
// 线程安全的计数方法
public void increment() {
count.incrementAndGet();
}
// 获取计数值
public int getCount() {
return count.get();
}
public static void main(String[] args) throws InterruptedException {
final int NUM_THREADS = 5;
final int NUM_INCREMENTS = 10000;
ThreadSafeCounter counter = new ThreadSafeCounter();
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
System.out.println("Final count: " + counter.getCount());
}
}我们使用了AtomicInteger来实现线程安全的计数器。AtomicInteger是一个原子类,它保证了计数操作的原子性,避免了线程安全问题。通过使用原子类,我们避免了手动加锁的复杂性,并提高了程序的性能。
同时,我们在主线程中创建了多个线程来对计数器进行增加操作。在这个例子中,计数器的最终值将会是NUM_THREADS * NUM_INCREMENTS,并且由于使用了AtomicInteger,计数器是线程安全的。
通过学习,我们已经掌握了Java多线程编程与并发控制的核心概念和技巧。多线程编程是Java中必不可少的一部分,它能够充分利用现代计算机多核处理器的性能,实现高效并发处理。然而,多线程编程也伴随着一些挑战和风险,例如死锁、竞态条件等。通过灵活运用线程同步、并发集合类和线程池等工具,以及遵循并发编程的最佳实践,我们能够优雅地驾驭多线程,构建出高性能、可靠的Java应用。
建议:不熟悉的多线程尽量不要使用(有一定的底蕴再去使用),“高效使用多线程:避免滥用与死锁,合理设置优先级与变量共享”