学习目标
- 掌握线程启动(start)、休眠(sleep)和中断(interrupt)的正确使用方式
- 理解线程Join操作的原理与适用场景
- 学习如何设置和管理线程优先级
- 了解守护线程(Daemon Thread)的特性及其应用
1. 线程启动(start)、休眠(sleep)、中断(interrupt)
1.1 线程启动(start)
在Java中,调用线程的start()方法才能真正启动一个线程。这个方法会创建新的执行线程,并使该线程开始执行run()方法中的代码。
package org.devlive.tutorial.multithreading.chapter03;/*** 线程启动示例*/public class ThreadStartDemo {public static void main(String[] args) {// 创建线程Thread thread = new Thread(() -> {System.out.println("线程ID: " + Thread.currentThread().getId());System.out.println("线程名称: " + Thread.currentThread().getName());System.out.println("线程执行中...");});// 设置线程名称thread.setName("MyCustomThread");System.out.println("线程启动前的状态: " + thread.getState());// 启动线程thread.start();// 等待一点时间,让线程有机会执行try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程启动后的状态: " + thread.getState());}}
⚠️ 重要提示: 不要多次调用同一个线程的
start()方法。线程一旦启动并终止,就无法再次启动。如果尝试再次启动同一个线程,会抛出IllegalThreadStateException异常。
来看一个错误示例:
package org.devlive.tutorial.multithreading.chapter03;/*** 线程重复启动错误示例*/public class ThreadDoubleStartErrorDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {System.out.println("线程执行中...");});// 第一次启动线程(正确)thread.start();// 等待线程执行完毕try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程状态: " + thread.getState());try {// 尝试再次启动同一个线程(错误)System.out.println("尝试再次启动同一个线程...");thread.start();} catch (IllegalThreadStateException e) {System.out.println("捕获到异常: " + e.getMessage());System.out.println("线程一旦终止,就不能再次启动!");}}}
📌 正确做法: 如果需要再次执行相同的任务,应该创建一个新的线程对象。
1.2 线程休眠(sleep)
Thread.sleep()方法可以使当前执行线程暂停执行指定的时间,让出CPU时间给其他线程,但不会释放当前线程所持有的锁。
package org.devlive.tutorial.multithreading.chapter03;import java.time.LocalTime;import java.time.format.DateTimeFormatter;/*** 线程休眠示例*/public class ThreadSleepDemo {public static void main(String[] args) {// 时间格式化器DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");// 创建并启动3个线程for (int i = 1; i <= 3; i++) {final int threadId = i;new Thread(() -> {for (int j = 1; j <= 5; j++) {// 打印当前时间和线程信息String time = LocalTime.now().format(formatter);System.out.println(time + " - 线程" + threadId + " 执行第" + j + "次");try {// 线程休眠随机时间(0.5-2秒)long sleepTime = 500 + (long)(Math.random() * 1500);Thread.sleep(sleepTime);} catch (InterruptedException e) {System.out.println("线程" + threadId + " 被中断");return; // 提前结束线程}}System.out.println("线程" + threadId + " 执行完毕");}, "线程" + i).start();}}}
sleep()方法有两个重载版本:
public static void sleep(long millis) throws InterruptedExceptionpublic static void sleep(long millis, int nanos) throws InterruptedException
📌 注意:
sleep()方法会抛出InterruptedException,这是一个检查型异常,必须处理。通常有两种处理方式:向上抛出或捕获处理。
sleep()方法与锁的关系示例:
package org.devlive.tutorial.multithreading.chapter03;/*** 演示sleep方法不会释放锁*/public class ThreadSleepWithLockDemo {private static final Object lock = new Object();public static void main(String[] args) {// 创建第一个线程,获取锁后休眠Thread thread1 = new Thread(() -> {synchronized (lock) {System.out.println("线程1获取到锁");System.out.println("线程1开始休眠3秒...");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1休眠结束,释放锁");}});// 创建第二个线程,尝试获取锁Thread thread2 = new Thread(() -> {System.out.println("线程2尝试获取锁...");synchronized (lock) {System.out.println("线程2获取到锁");}System.out.println("线程2释放锁");});// 启动线程thread1.start();// 确保线程1先执行try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}thread2.start();}}
在上面的例子中,当线程1调用sleep()方法时,它不会释放锁,因此线程2必须等待线程1的睡眠时间结束并释放锁后才能获取锁继续执行。
1.3 线程中断(interrupt)
Java提供了一种协作式的线程中断机制,它不会强制终止线程,而是通过设置线程的中断标志,让线程自己决定如何响应中断请求。
通过以下方法可以实现线程中断:
public void interrupt():中断线程,设置中断标志public boolean isInterrupted():检查线程是否被中断,不清除中断状态public static boolean interrupted():检查当前线程是否被中断,并清除中断状态
package org.devlive.tutorial.multithreading.chapter03;/*** 线程中断示例*/public class ThreadInterruptDemo {public static void main(String[] args) {// 创建一个简单的可中断线程Thread thread = new Thread(() -> {int count = 0;// 检查线程中断标志while (!Thread.currentThread().isInterrupted()) {System.out.println("线程执行中... " + (++count));// 模拟工作try {Thread.sleep(1000);} catch (InterruptedException e) {// sleep方法被中断会清除中断状态,需要重新设置System.out.println("线程在sleep期间被中断");Thread.currentThread().interrupt(); // 重新设置中断状态}}System.out.println("线程检测到中断信号,正常退出");});// 启动线程thread.start();// 主线程休眠3秒后中断线程try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程发送中断信号");thread.interrupt();}}
⚠️ 重要说明: 当线程在
sleep()、wait()或join()等阻塞方法中被中断时,这些方法会抛出InterruptedException并清除中断状态。因此,在捕获这些异常时,通常需要重新设置中断状态。
处理可中断阻塞操作的标准模式:
try {// 可中断的阻塞操作Thread.sleep(timeoutMillis);// 正常处理} catch (InterruptedException e) {// 可以记录日志// 重新设置中断状态Thread.currentThread().interrupt();// 可以选择提前返回或终止循环return;}
2. 线程Join操作及其应用场景
join()方法允许一个线程等待另一个线程完成。当在线程A中调用线程B的join()方法时,线程A将被阻塞,直到线程B完成执行。
package org.devlive.tutorial.multithreading.chapter03;/*** 线程Join示例*/public class ThreadJoinDemo {public static void main(String[] args) {// 创建3个执行特定任务的线程Thread thread1 = new Thread(() -> {System.out.println("线程1开始执行...");try {// 模拟耗时操作Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1执行完毕");});Thread thread2 = new Thread(() -> {System.out.println("线程2开始执行...");try {// 模拟耗时操作Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程2执行完毕");});Thread thread3 = new Thread(() -> {System.out.println("线程3开始执行...");try {// 模拟耗时操作Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程3执行完毕");});System.out.println("启动所有线程");// 启动线程thread1.start();thread2.start();thread3.start();System.out.println("等待所有线程完成...");try {// 等待线程1完成thread1.join();System.out.println("线程1已完成");// 等待线程2完成thread2.join();System.out.println("线程2已完成");// 等待线程3完成thread3.join();System.out.println("线程3已完成");} catch (InterruptedException e) {System.out.println("主线程被中断");}System.out.println("所有线程已完成,继续执行主线程");}}
join()方法有三个重载版本:
public final void join() throws InterruptedException:等待线程终止public final void join(long millis) throws InterruptedException:等待线程终止,最多等待指定的毫秒数public final void join(long millis, int nanos) throws InterruptedException:等待线程终止,最多等待指定的毫秒数加纳秒数
2.1 Join操作的典型应用场景
- 等待所有工作线程完成
当主线程需要等待多个工作线程全部完成后再继续执行时,可以使用join()方法。
package org.devlive.tutorial.multithreading.chapter03;import java.util.ArrayList;import java.util.List;/*** 等待多个工作线程完成示例*/public class MultiThreadJoinDemo {public static void main(String[] args) {List<Thread> threads = new ArrayList<>();// 创建5个工作线程for (int i = 1; i <= 5; i++) {final int threadNum = i;Thread thread = new Thread(() -> {System.out.println("工作线程" + threadNum + "开始执行...");// 模拟不同的工作负载try {Thread.sleep(1000 + threadNum * 500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("工作线程" + threadNum + "执行完毕");});threads.add(thread);thread.start();}System.out.println("等待所有工作线程完成...");// 等待所有线程完成for (Thread thread : threads) {try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("所有工作线程已完成,主线程继续执行");}}
- 顺序执行多个任务
当多个任务必须按照特定顺序执行时,可以使用join()方法确保前一个任务完成后再开始下一个任务。
package org.devlive.tutorial.multithreading.chapter03;/*** 顺序执行任务示例*/public class SequentialTasksDemo {public static void main(String[] args) {System.out.println("开始执行顺序任务");// 步骤1:准备数据Thread step1 = new Thread(() -> {System.out.println("步骤1:准备数据...");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("步骤1完成:数据准备好了");});// 步骤2:处理数据Thread step2 = new Thread(() -> {System.out.println("步骤2:处理数据...");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("步骤2完成:数据处理好了");});// 步骤3:保存结果Thread step3 = new Thread(() -> {System.out.println("步骤3:保存结果...");try {Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("步骤3完成:结果已保存");});try {// 启动第一个任务并等待完成step1.start();step1.join();// 启动第二个任务并等待完成step2.start();step2.join();// 启动第三个任务并等待完成step3.start();step3.join();} catch (InterruptedException e) {System.out.println("任务执行被中断");}System.out.println("所有步骤已完成");}}
- 实现简单的任务分解与合并
在并行计算中,可以将大任务分解为多个小任务并行执行,然后使用join()等待所有小任务完成后合并结果。
package org.devlive.tutorial.multithreading.chapter03;import java.util.concurrent.atomic.AtomicLong;/*** 并行计算示例:计算从1到n的总和*/public class ParallelSumDemo {public static void main(String[] args) {long n = 1_000_000_000L; // 计算1到10亿的和int numThreads = 4; // 使用4个线程并行计算// 存储最终结果AtomicLong totalSum = new AtomicLong(0);// 创建并启动工作线程Thread[] threads = new Thread[numThreads];for (int i = 0; i < numThreads; i++) {final long start = i * (n / numThreads) + 1;final long end = (i == numThreads - 1) ? n : (i + 1) * (n / numThreads);threads[i] = new Thread(() -> {System.out.println("计算从 " + start + " 到 " + end + " 的和");long partialSum = 0;for (long j = start; j <= end; j++) {partialSum += j;}totalSum.addAndGet(partialSum);System.out.println("部分和 (" + start + "-" + end + "): " + partialSum);});threads[i].start();}// 等待所有线程完成try {for (Thread thread : threads) {thread.join();}} catch (InterruptedException e) {e.printStackTrace();}// 输出最终结果System.out.println("并行计算结果: " + totalSum.get());// 验证结果long expectedSum = n * (n + 1) / 2;System.out.println("正确结果: " + expectedSum);System.out.println("结果正确: " + (totalSum.get() == expectedSum));}}
3. 线程优先级的设置与影响
Java线程的优先级为1~10的整数,默认优先级是5。线程优先级高的线程会获得更多的执行机会,但这只是一个提示,不能保证高优先级的线程一定先于低优先级的线程执行。
package org.devlive.tutorial.multithreading.chapter03;/*** 线程优先级示例*/public class ThreadPriorityDemo {public static void main(String[] args) {// 创建3个不同优先级的线程Thread lowPriorityThread = new Thread(() -> {System.out.println("低优先级线程开始执行");long count = 0;for (long i = 0; i < 5_000_000_000L; i++) {count++;if (i % 1_000_000_000 == 0) {System.out.println("低优先级线程计数: " + i / 1_000_000_000);}}System.out.println("低优先级线程执行完毕,计数: " + count);});Thread normalPriorityThread = new Thread(() -> {System.out.println("普通优先级线程开始执行");long count = 0;for (long i = 0; i < 5_000_000_000L; i++) {count++;if (i % 1_000_000_000 == 0) {System.out.println("普通优先级线程计数: " + i / 1_000_000_000);}}System.out.println("普通优先级线程执行完毕,计数: " + count);});Thread highPriorityThread = new Thread(() -> {System.out.println("高优先级线程开始执行");long count = 0;for (long i = 0; i < 5_000_000_000L; i++) {count++;if (i % 1_000_000_000 == 0) {System.out.println("高优先级线程计数: " + i / 1_000_000_000);}}System.out.println("高优先级线程执行完毕,计数: " + count);});// 设置线程优先级lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // 1normalPriorityThread.setPriority(Thread.NORM_PRIORITY); // 5highPriorityThread.setPriority(Thread.MAX_PRIORITY); // 10System.out.println("低优先级线程的优先级: " + lowPriorityThread.getPriority());System.out.println("普通优先级线程的优先级: " + normalPriorityThread.getPriority());System.out.println("高优先级线程的优先级: " + highPriorityThread.getPriority());// 启动线程System.out.println("启动所有线程");lowPriorityThread.start();normalPriorityThread.start();highPriorityThread.start();}}
⚠️ 重要说明: 线程优先级依赖于操作系统的支持,不同的操作系统对线程优先级的支持不同,有些操作系统甚至会忽略线程优先级。因此,不应该依赖线程优先级来确保程序的正确性。
线程优先级的最佳实践:
- 仅在需要微调性能时使用线程优先级
- 不要依赖线程优先级来确保程序的正确性
- 避免使用过高的优先级,可能会导致其他线程饥饿
- 避免频繁改变线程优先级
4. 守护线程(Daemon Thread)的应用
守护线程是为其他线程服务的线程,当所有非守护线程结束时,Java虚拟机会自动终止所有守护线程并退出。典型的守护线程有垃圾回收器、JIT编译器等。
package org.devlive.tutorial.multithreading.chapter03;/*** 守护线程示例*/public class DaemonThreadDemo {public static void main(String[] args) {// 创建一个守护线程Thread daemonThread = new Thread(() -> {int count = 0;while (true) {try {Thread.sleep(1000);count++;System.out.println("守护线程工作中... 计数: " + count);} catch (InterruptedException e) {System.out.println("守护线程被中断");break;}}});// 设置为守护线程daemonThread.setDaemon(true);// 创建一个普通线程Thread userThread = new Thread(() -> {for (int i = 1; i <= 5; i++) {System.out.println("用户线程工作中... " + i);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("用户线程执行完毕");});System.out.println("守护线程状态: " + daemonThread.isDaemon());System.out.println("用户线程状态: " + userThread.isDaemon());// 启动线程daemonThread.start();userThread.start();System.out.println("主线程等待用户线程完成...");try {userThread.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("主线程结束,JVM即将退出");// 不需要等待守护线程结束,JVM会自动终止所有守护线程}}
⚠️ 注意: 必须在调用
start()方法之前设置线程为守护线程,否则会抛出IllegalThreadStateException异常。
4.1 守护线程的应用场景
- 后台清理任务
守护线程适合执行不需要用户交互的后台任务,如清理过期缓存、定期日志滚动等。
package org.devlive.tutorial.multithreading.chapter03;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;/*** 使用守护线程实现缓存清理*/public class DaemonThreadCacheCleanerDemo {// 简单的内存缓存private static class SimpleCache {// 缓存数据,键是缓存项的名称,值是包含数据和过期时间的CacheItemprivate final ConcurrentHashMap<String, CacheItem> cache = new ConcurrentHashMap<>();// 缓存项,包含实际数据和过期时间private static class CacheItem {private final Object data;private final long expireTime; // 过期时间戳(毫秒)public CacheItem(Object data, long ttlMillis) {this.data = data;this.expireTime = System.currentTimeMillis() + ttlMillis;}public boolean isExpired() {return System.currentTimeMillis() > expireTime;}@Overridepublic String toString() {return data.toString();}}// 添加缓存项public void put(String key, Object value, long ttlMillis) {cache.put(key, new CacheItem(value, ttlMillis));System.out.println("添加缓存项: " + key + " = " + value + ", TTL: " + ttlMillis + "ms");}// 获取缓存项public Object get(String key) {CacheItem item = cache.get(key);if (item == null) {return null; // 缓存中没有该项}if (item.isExpired()) {cache.remove(key); // 惰性删除过期项return null;}return item.data;}// 获取缓存大小public int size() {return cache.size();}// 启动清理守护线程public void startCleanerThread() {Thread cleanerThread = new Thread(() -> {System.out.println("缓存清理线程启动");while (true) {try {TimeUnit.SECONDS.sleep(1); // 每秒检查一次// 记录当前时间String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));System.out.println("\n" + now + " - 开始清理过期缓存项...");int beforeSize = cache.size();// 清理过期的缓存项cache.forEach((key, item) -> {if (item.isExpired()) {System.out.println("移除过期缓存项: " + key);cache.remove(key);}});int afterSize = cache.size();System.out.println("清理完成: 移除了 " + (beforeSize - afterSize) + " 个过期项,当前缓存大小: " + afterSize);} catch (InterruptedException e) {System.out.println("缓存清理线程被中断");break;}}});// 设置为守护线程cleanerThread.setDaemon(true);cleanerThread.start();}}public static void main(String[] args) throws InterruptedException {// 创建缓存并启动清理线程SimpleCache cache = new SimpleCache();cache.startCleanerThread();// 添加一些缓存项,设置不同的过期时间cache.put("item1", "Value 1", 3000); // 3秒后过期cache.put("item2", "Value 2", 7000); // 7秒后过期cache.put("item3", "Value 3", 5000); // 5秒后过期cache.put("item4", "Value 4", 10000); // 10秒后过期// 主线程每隔一段时间检查缓存中的项for (int i = 1; i <= 12; i++) {System.out.println("\n===== " + i + "秒后 =====");System.out.println("item1: " + cache.get("item1"));System.out.println("item2: " + cache.get("item2"));System.out.println("item3: " + cache.get("item3"));System.out.println("item4: " + cache.get("item4"));// 每检查一次,等待1秒TimeUnit.SECONDS.sleep(1);}System.out.println("\n主线程执行完毕,程序即将退出");// 当主线程结束后,守护线程也会自动结束}}
- 监控和维护服务
守护线程可以用于监控应用程序的健康状态、收集性能指标等。
package org.devlive.tutorial.multithreading.chapter03;import java.lang.management.ManagementFactory;import java.lang.management.MemoryMXBean;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.concurrent.TimeUnit;/*** 使用守护线程实现系统监控*/public class DaemonThreadMonitoringDemo {public static void main(String[] args) {// 启动系统监控守护线程startMonitoringThread();// 模拟主应用程序System.out.println("主应用程序开始运行...");// 执行一些内存密集型操作,让监控线程有些变化可以报告for (int i = 0; i < 5; i++) {System.out.println("\n===== 执行任务 " + (i + 1) + " =====");// 分配一些内存byte[][] arrays = new byte[i + 1][1024 * 1024]; // 分配 (i+1) MB的内存// 模拟一些处理try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}// 释放部分内存if (i % 2 == 0) {arrays[0] = null; // 释放第一个数组}}System.out.println("\n主应用程序执行完毕,即将退出");}private static void startMonitoringThread() {Thread monitorThread = new Thread(() -> {// 获取内存管理 MXBeanMemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");System.out.println("系统监控线程启动");try {while (true) {// 获取当前时间String time = LocalDateTime.now().format(formatter);// 收集内存使用情况long heapUsed = memoryBean.getHeapMemoryUsage().getUsed();long heapMax = memoryBean.getHeapMemoryUsage().getMax();long nonHeapUsed = memoryBean.getNonHeapMemoryUsage().getUsed();// 计算内存使用百分比double heapUsagePercent = (double) heapUsed / heapMax * 100;// 输出监控信息System.out.println("\n[" + time + "] 系统监控:");System.out.printf("堆内存使用: %.2f MB / %.2f MB (%.1f%%)\n",heapUsed / (1024.0 * 1024.0),heapMax / (1024.0 * 1024.0),heapUsagePercent);System.out.printf("非堆内存使用: %.2f MB\n",nonHeapUsed / (1024.0 * 1024.0));// 检查是否内存使用过高if (heapUsagePercent > 70) {System.out.println("警告: 内存使用率过高!");}// 每2秒收集一次信息TimeUnit.SECONDS.sleep(2);}} catch (InterruptedException e) {System.out.println("监控线程被中断");}});// 设置为守护线程monitorThread.setDaemon(true);monitorThread.setName("系统监控线程");monitorThread.start();}}
- 自动保存线程
守护线程可以用于定期保存用户工作,确保即使程序意外退出,用户也不会丢失太多数据。
package org.devlive.tutorial.multithreading.chapter03;import java.io.File;import java.io.FileWriter;import java.io.IOException;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;import java.util.Scanner;import java.util.concurrent.TimeUnit;/*** 使用守护线程实现自动保存功能*/public class DaemonThreadAutoSaveDemo {// 模拟文档内容private static StringBuilder documentContent = new StringBuilder();// 文档是否被修改private static volatile boolean documentModified = false;// 记录上次保存时间private static LocalDateTime lastSaveTime;public static void main(String[] args) {// 启动自动保存守护线程startAutoSaveThread();// 模拟文本编辑器System.out.println("简易文本编辑器 (输入'exit'退出)");System.out.println("----------------------");Scanner scanner = new Scanner(System.in);while (true) {System.out.print("> ");String line = scanner.nextLine();if ("exit".equalsIgnoreCase(line)) {System.out.println("正在退出编辑器...");// 如果有未保存的内容,强制保存if (documentModified) {saveDocument();}break;} else {// 添加用户输入到文档documentContent.append(line).append("\n");documentModified = true;System.out.println("文本已添加 (当前字符数: " + documentContent.length() + ")");}}System.out.println("编辑器已关闭,程序退出");}private static void startAutoSaveThread() {Thread autoSaveThread = new Thread(() -> {System.out.println("自动保存线程已启动 (每10秒检查一次)");while (true) {try {// 休眠10秒TimeUnit.SECONDS.sleep(10);// 检查文档是否被修改if (documentModified) {saveDocument();}} catch (InterruptedException e) {System.out.println("自动保存线程被中断");break;}}});// 设置为守护线程autoSaveThread.setDaemon(true);autoSaveThread.setName("AutoSaveThread");autoSaveThread.start();}private static void saveDocument() {File file = new File("autosave_document.txt");try (FileWriter writer = new FileWriter(file)) {writer.write(documentContent.toString());// 更新状态documentModified = false;lastSaveTime = LocalDateTime.now();System.out.println("\n[自动保存] 文档已保存到: " + file.getAbsolutePath() +" (" + lastSaveTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")) + ")");} catch (IOException e) {System.out.println("\n[自动保存] 保存文档时出错: " + e.getMessage());}}}
5. 实战案例:文件搜索工具
让我们创建一个使用多线程的文件搜索工具,它可以在指定目录中搜索符合特定条件的文件,并显示搜索结果。这个案例将综合运用我们在本章学到的线程操作知识。
package org.devlive.tutorial.multithreading.chapter03;import java.io.File;import java.io.IOException;import java.nio.file.Files;import java.util.ArrayList;import java.util.List;import java.util.concurrent.ConcurrentLinkedQueue;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/*** 多线程文件搜索工具*/public class ConcurrentFileSearcher {// 搜索结果private final ConcurrentLinkedQueue<File> results = new ConcurrentLinkedQueue<>();// 搜索线程数量private final int threadCount;// 搜索条件接口public interface SearchCriteria {boolean matches(File file);}// 构造函数public ConcurrentFileSearcher(int threadCount) {this.threadCount = threadCount;}/*** 搜索文件* @param startDir 起始目录* @param criteria 搜索条件* @return 匹配的文件列表*/public List<File> search(File startDir, SearchCriteria criteria) {if (!startDir.exists() || !startDir.isDirectory()) {throw new IllegalArgumentException("起始目录不存在或不是一个目录: " + startDir);}// 清空上次搜索结果results.clear();// 计数器,用于跟踪处理的文件数和目录数AtomicInteger processedFiles = new AtomicInteger(0);AtomicInteger processedDirs = new AtomicInteger(0);// 创建目录队列ConcurrentLinkedQueue<File> directoryQueue = new ConcurrentLinkedQueue<>();directoryQueue.add(startDir);// 创建并启动工作线程Thread[] searchThreads = new Thread[threadCount];for (int i = 0; i < threadCount; i++) {searchThreads[i] = new Thread(() -> {while (true) {// 从队列中获取下一个要处理的目录File currentDir = directoryQueue.poll();// 如果队列为空,检查是否所有线程都空闲if (currentDir == null) {// 等待一会儿,看看其他线程是否会添加新的目录try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();break;}// 再次检查队列if (directoryQueue.isEmpty()) {break; // 如果仍然为空,则结束线程} else {continue; // 如果有新目录,继续处理}}// 处理当前目录中的文件和子目录File[] items = currentDir.listFiles();if (items != null) {for (File item : items) {if (item.isDirectory()) {// 将子目录添加到队列中directoryQueue.add(item);processedDirs.incrementAndGet();} else {// 检查文件是否匹配搜索条件if (criteria.matches(item)) {results.add(item);}processedFiles.incrementAndGet();}}}}});// 设置线程名称并启动searchThreads[i].setName("SearchThread-" + i);searchThreads[i].start();}// 创建并启动一个守护线程来显示搜索进度Thread progressThread = new Thread(() -> {try {while (true) {int files = processedFiles.get();int dirs = processedDirs.get();int found = results.size();System.out.printf("\r处理中: %d 个文件, %d 个目录, 找到 %d 个匹配文件",files, dirs, found);TimeUnit.SECONDS.sleep(1);}} catch (InterruptedException e) {// 忽略中断}});progressThread.setDaemon(true);progressThread.start();// 等待所有搜索线程完成try {for (Thread thread : searchThreads) {thread.join();}} catch (InterruptedException e) {// 如果主线程被中断,中断所有搜索线程for (Thread thread : searchThreads) {thread.interrupt();}Thread.currentThread().interrupt();}// 打印最终结果System.out.println("\n搜索完成: 处理了 " + processedFiles.get() + " 个文件, "+ processedDirs.get() + " 个目录, 找到 " + results.size() + " 个匹配文件");// 将结果转换为List并返回return new ArrayList<>(results);}// 主程序示例public static void main(String[] args) {// 创建文件搜索器,使用4个线程ConcurrentFileSearcher searcher = new ConcurrentFileSearcher(4);// 定义搜索起始目录File startDir = new File("C:/"); // Windows系统// File startDir = new File("/"); // Linux/Mac系统System.out.println("开始在 " + startDir + " 中搜索...");// 定义搜索条件:查找大于10MB的Java源代码文件SearchCriteria criteria = file -> {// 检查文件扩展名if (file.getName().endsWith(".java")) {try {// 检查文件大小(字节)return Files.size(file.toPath()) > 10 * 1024; // > 10KB} catch (IOException e) {return false;}}return false;};// 执行搜索List<File> searchResults = searcher.search(startDir, criteria);// 显示搜索结果System.out.println("\n搜索结果:");if (searchResults.isEmpty()) {System.out.println("未找到匹配的文件");} else {for (int i = 0; i < searchResults.size(); i++) {File file = searchResults.get(i);try {long size = Files.size(file.toPath());String sizeStr = size < 1024 ? size + " B" :size < 1024*1024 ? String.format("%.2f KB", size/1024.0) :String.format("%.2f MB", size/(1024.0*1024.0));System.out.printf("%d. %s (大小: %s)\n", i + 1, file.getAbsolutePath(), sizeStr);} catch (IOException e) {System.out.printf("%d. %s (无法获取文件大小)\n", i + 1, file.getAbsolutePath());}}}}}
在这个实战案例中,我们创建了一个多线程文件搜索工具,它可以高效地在指定目录中搜索符合特定条件的文件。主要特点:
- 使用多个线程并行搜索,提高搜索效率
- 使用线程安全的并发集合来共享数据
- 使用守护线程来显示搜索进度
- 使用原子变量来安全地计数
- 通过
join()方法等待所有搜索线程完成 - 使用接口定义搜索条件,提高灵活性
常见问题与解决方案
问题1:主线程结束但程序不退出
问题描述: 有时主线程已经执行完所有代码,但整个Java程序却不退出。
原因: 这通常是因为程序中还有非守护线程在运行。只有当所有非守护线程都结束时,Java程序才会退出。
解决方案:
- 确保所有工作线程都能正常结束
- 对于不需要一直运行的后台任务,使用守护线程
- 使用线程池时,记得调用
shutdown()方法
问题2:线程无法被中断
问题描述: 调用interrupt()方法后,线程没有响应中断请求,继续执行。
原因: 线程必须主动检查中断状态或处理InterruptedException才能响应中断请求。
解决方案:
- 在循环中定期检查中断状态:
Thread.currentThread().isInterrupted() - 正确处理
InterruptedException,通常是重设中断状态并返回 - 避免屏蔽中断请求
// 正确处理中断的方式public void run() {try {while (!Thread.currentThread().isInterrupted()) {// 执行任务doTask();// 可中断的阻塞操作Thread.sleep(100);}} catch (InterruptedException e) {// 记录日志,清理资源// 重新设置中断状态Thread.currentThread().interrupt();} finally {// 清理资源cleanup();}}
问题3:线程优先级设置不生效
问题描述: 设置了线程的优先级,但没有观察到预期的效果。
原因: 线程优先级依赖于操作系统的支持,不同的操作系统对线程优先级的支持程度不同。
解决方案:
- 不要依赖线程优先级来保证程序的正确性
- 使用其他机制(如线程池的任务队列)来控制任务的执行顺序
- 在需要严格控制执行顺序的场景,使用明确的同步机制
小结
在本章中,我们学习了线程的基本操作及其应用:
线程的基本操作: 我们学习了如何启动线程、让线程休眠以及正确地中断线程。这些是多线程编程的基础操作,掌握这些操作可以帮助我们更好地控制线程的行为。
线程Join操作: 我们了解了
join()方法的作用及其适用场景,如等待工作线程完成、顺序执行任务和实现简单的任务分解与合并。线程优先级: 我们学习了如何设置线程优先级,以及线程优先级的影响。同时,我们也认识到线程优先级的局限性,不应该依赖线程优先级来确保程序的正确性。
守护线程: 我们了解了守护线程的概念和特性,以及其在后台任务、监控和自动保存等场景的应用。
实战应用: 通过文件搜索工具的实战案例,我们将所学知识应用到实际问题中,创建了一个高效的多线程文件搜索工具。
通过本章的学习,你应该能够熟练地操作线程,包括启动、休眠、中断和连接线程,以及合理地设置线程优先级和使用守护线程。这些知识为后续深入学习线程同步和并发控制奠定了基础。
在下一章中,我们将开始探讨线程安全问题,学习如何识别和解决多线程程序中的并发问题。
