JAVA 多线程编程:从基础原理到实战应用

JAVA 多线程编程:从基础原理到实战应用

JAVA 多线程编程:从基础原理到实战应用

在这里插入图片描述

1.1 本章学习目标与重点

💡 掌握线程的核心概念,理解进程与线程的区别和联系。
💡 熟练掌握线程的三种创建方式,理解线程的生命周期及状态转换。
💡 掌握线程同步与锁机制,解决多线程并发安全问题。
💡 了解线程池的核心原理与使用方法,提升多线程程序性能。
⚠️ 本章重点是 线程同步机制线程池的实战应用,这是多线程开发中的核心难点和高频考点。

1.2 多线程核心概念

1.2.1 进程与线程的区别

💡 进程是操作系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间和系统资源。比如打开一个 Java 程序,就会启动一个进程。
💡 线程是进程的执行单元,是 CPU 调度和执行的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。

对比维度进程线程
资源分配拥有独立的内存空间和资源共享所属进程的内存和资源
开销成本创建和销毁开销大创建和销毁开销小
调度方式由操作系统内核调度由进程内部调度
独立性进程之间相互独立线程之间共享资源,依赖性强

✅ 核心结论:线程是轻量级的进程,多线程编程可以充分利用 CPU 资源,提升程序的并发执行效率。

1.2.2 为什么需要多线程

在单线程程序中,代码是串行执行的,当遇到耗时操作(如文件读写、网络请求)时,程序会阻塞等待,造成 CPU 资源浪费。
多线程的优势体现在以下场景:

  1. 后台任务处理:如电商系统的订单超时取消、数据同步等。
  2. 异步操作:如用户注册后发送短信验证码,不阻塞主流程。
  3. 提高 CPU 利用率:如计算密集型任务,多线程可以充分利用多核 CPU。

1.3 线程的创建方式

1.3.1 方式1:继承 Thread 类

实现步骤

① 📝 定义一个类继承 Thread 类。
② 📝 重写 run() 方法,编写线程执行的业务逻辑。
③ 📝 创建线程对象,调用 start() 方法启动线程。

代码实操
/** * 继承Thread类创建线程 */publicclassMyThreadextendsThread{// 线程名称privateString threadName;publicMyThread(String threadName){this.threadName = threadName;}// 重写run方法,定义线程执行逻辑@Overridepublicvoidrun(){for(int i =1; i <=5; i++){System.out.println(threadName +" 执行:"+ i);// 模拟线程执行耗时操作try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}}}publicstaticvoidmain(String[] args){// 创建线程对象MyThread thread1 =newMyThread("线程A");MyThread thread2 =newMyThread("线程B");// 启动线程:注意是调用start()方法,不是run()方法 thread1.start(); thread2.start();}}

输出结果(顺序不固定,体现线程并发执行)

线程A 执行:1 线程B 执行:1 线程A 执行:2 线程B 执行:2 线程A 执行:3 线程B 执行:3 线程A 执行:4 线程B 执行:4 线程A 执行:5 线程B 执行:5 

⚠️ 注意事项:启动线程必须调用 start() 方法,直接调用 run() 方法会变成普通方法的串行执行。

1.3.2 方式2:实现 Runnable 接口

实现步骤

① 📝 定义一个类实现 Runnable 接口。
② 📝 重写 run() 方法,编写线程执行逻辑。
③ 📝 创建 Runnable 实现类对象,作为 Thread 构造方法的参数。
④ 📝 调用 start() 方法启动线程。

代码实操
/** * 实现Runnable接口创建线程 */publicclassMyRunnableimplementsRunnable{privateString taskName;publicMyRunnable(String taskName){this.taskName = taskName;}@Overridepublicvoidrun(){for(int i =1; i <=5; i++){System.out.println(taskName +" 执行:"+ i);try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}}}publicstaticvoidmain(String[] args){// 创建任务对象MyRunnable task1 =newMyRunnable("任务A");MyRunnable task2 =newMyRunnable("任务B");// 创建线程对象,传入任务Thread thread1 =newThread(task1);Thread thread2 =newThread(task2);// 启动线程 thread1.start(); thread2.start();}}
方式2的优势
  1. 避免单继承的局限性:Java 是单继承机制,继承 Thread 类后无法再继承其他类,而实现 Runnable 接口可以。
  2. 资源共享:多个线程可以共享同一个 Runnable 任务对象,适合多线程处理同一份资源的场景。
资源共享案例:多线程售票
/** * 多线程共享资源:模拟售票系统 */publicclassTicketRunnableimplementsRunnable{// 共享资源:10张票privateint ticketNum =10;@Overridepublicvoidrun(){while(ticketNum >0){// 模拟售票耗时try{Thread.sleep(300);}catch(InterruptedException e){ e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" 售出第 "+ ticketNum--+" 张票");}}publicstaticvoidmain(String[] args){// 一个任务对象,多个线程共享TicketRunnable ticketTask =newTicketRunnable();// 三个线程模拟三个售票窗口Thread window1 =newThread(ticketTask,"窗口1");Thread window2 =newThread(ticketTask,"窗口2");Thread window3 =newThread(ticketTask,"窗口3"); window1.start(); window2.start(); window3.start();}}

输出结果(存在线程安全问题,后面会解决)

窗口1 售出第 10 张票 窗口2 售出第 9 张票 窗口3 售出第 8 张票 窗口1 售出第 7 张票 窗口2 售出第 6 张票 窗口3 售出第 5 张票 窗口1 售出第 4 张票 窗口2 售出第 3 张票 窗口3 售出第 2 张票 窗口1 售出第 1 张票 

1.3.3 方式3:实现 Callable 接口(带返回值)

实现步骤

① 📝 定义一个类实现 Callable<V> 接口,V 是返回值类型。
② 📝 重写 call() 方法,编写线程执行逻辑,该方法可以抛出异常。
③ 📝 创建 Callable 实现类对象,通过 FutureTask 包装。
④ 📝 将 FutureTask 作为 Thread 构造方法参数,启动线程。
⑤ 📝 调用 FutureTaskget() 方法获取返回值。

代码实操
importjava.util.concurrent.Callable;importjava.util.concurrent.FutureTask;/** * 实现Callable接口创建线程(带返回值) */publicclassMyCallableimplementsCallable<Integer>{// 计算1到n的累加和privateint n;publicMyCallable(int n){this.n = n;}@OverridepublicIntegercall()throwsException{int sum =0;for(int i =1; i <= n; i++){ sum += i;System.out.println("计算中:"+ i +",当前和为:"+ sum);Thread.sleep(200);}return sum;}publicstaticvoidmain(String[] args)throwsException{// 创建Callable任务MyCallable callableTask =newMyCallable(10);// 用FutureTask包装,用于获取返回值FutureTask<Integer> futureTask =newFutureTask<>(callableTask);// 创建线程并启动Thread thread =newThread(futureTask); thread.start();// 获取返回值:get()方法会阻塞,直到线程执行完成Integer result = futureTask.get();System.out.println("1到10的累加和为:"+ result);}}

输出结果

计算中:1,当前和为:1 计算中:2,当前和为:3 计算中:3,当前和为:6 计算中:4,当前和为:10 计算中:5,当前和为:15 计算中:6,当前和为:21 计算中:7,当前和为:28 计算中:8,当前和为:36 计算中:9,当前和为:45 计算中:10,当前和为:55 1到10的累加和为:55 

✅ 核心结论:三种创建方式对比

创建方式优点缺点
继承 Thread编程简单,直接使用 this 获取线程单继承局限,无法共享资源
实现 Runnable避免单继承,支持资源共享无返回值,无法抛出受检异常
实现 Callable有返回值,支持异常抛出编程相对复杂

1.4 线程的生命周期与状态转换

1.4.1 线程的六种状态

Java 中线程的生命周期包含六种状态,定义在 Thread.State 枚举中:

  1. NEW(新建状态):线程对象已创建,但未调用 start() 方法。
  2. RUNNABLE(可运行状态):调用 start() 方法后,线程处于就绪或运行中。
    • 就绪状态:线程等待 CPU 调度。
    • 运行状态:线程获取 CPU 资源,执行 run() 方法。
  3. BLOCKED(阻塞状态):线程等待获取锁资源时进入该状态。
  4. WAITING(等待状态):线程调用 wait()join() 等方法后进入,需要被其他线程唤醒。
  5. TIMED_WAITING(超时等待状态):线程调用 sleep(long)wait(long) 等方法后进入,超时自动唤醒。
  6. TERMINATED(终止状态):线程执行完毕或异常终止。

1.4.2 线程状态转换图(核心流程)

NEW → RUNNABLE:调用 start() 方法 RUNNABLE → BLOCKED:竞争锁失败 BLOCKED → RUNNABLE:获取到锁资源 RUNNABLE → WAITING:调用 wait()/join() 方法 WAITING → RUNNABLE:其他线程调用 notify()/notifyAll() 方法 RUNNABLE → TIMED_WAITING:调用 sleep(long)/wait(long) 方法 TIMED_WAITING → RUNNABLE:超时自动唤醒或被其他线程唤醒 RUNNABLE → TERMINATED:run() 方法执行完毕或异常终止 

1.4.3 常用线程控制方法

方法作用注意事项
start()启动线程一个线程只能调用一次
sleep(long millis)让线程休眠指定时间不会释放持有的锁资源
wait()让线程进入等待状态必须在同步代码块中调用,会释放锁
notify()唤醒一个等待的线程必须在同步代码块中调用
notifyAll()唤醒所有等待的线程必须在同步代码块中调用
join()等待该线程执行完毕可以实现线程的顺序执行
代码实操:线程的顺序执行(join 方法)
/** * 使用join方法实现线程顺序执行 */publicclassThreadJoinDemo{publicstaticvoidmain(String[] args)throwsInterruptedException{Thread threadA =newThread(()->{for(int i =1; i <=3; i++){System.out.println("线程A执行:"+ i);try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}}});Thread threadB =newThread(()->{for(int i =1; i <=3; i++){System.out.println("线程B执行:"+ i);try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}}});// 启动线程A threadA.start();// 等待线程A执行完毕,再启动线程B threadA.join(); threadB.start();}}

输出结果(线程A执行完才会执行线程B)

线程A执行:1 线程A执行:2 线程A执行:3 线程B执行:1 线程B执行:2 线程B执行:3 

1.5 线程同步与并发安全

1.5.1 并发安全问题的产生

当多个线程同时操作共享资源时,会导致数据不一致的问题。比如前面的售票案例,在极端情况下会出现超卖或重复售票的问题。

问题复现:多线程售票的并发安全问题

publicclassUnsafeTicketDemoimplementsRunnable{privateint ticketNum =10;@Overridepublicvoidrun(){while(ticketNum >0){// 模拟网络延迟,放大并发问题try{Thread.sleep(100);}catch(InterruptedException e){ e.printStackTrace();}// 核心问题:判断和操作不是原子性的System.out.println(Thread.currentThread().getName()+" 售出第 "+ ticketNum--+" 张票");}}publicstaticvoidmain(String[] args){UnsafeTicketDemo task =newUnsafeTicketDemo();newThread(task,"窗口1").start();newThread(task,"窗口2").start();newThread(task,"窗口3").start();}}

可能出现的错误结果

窗口1 售出第 10 张票 窗口2 售出第 10 张票 窗口3 售出第 9 张票 窗口1 售出第 8 张票 窗口2 售出第 7 张票 窗口3 售出第 6 张票 窗口1 售出第 5 张票 窗口2 售出第 4 张票 窗口3 售出第 3 张票 窗口1 售出第 2 张票 窗口2 售出第 1 张票 窗口3 售出第 0 张票 

可以看到出现了重复售票售出0号票的问题,这就是典型的并发安全问题。

1.5.2 解决方案1:同步代码块(synchronized)

💡 同步代码块的核心是锁机制,通过 synchronized 关键字锁定一个对象,保证同一时间只有一个线程能执行代码块中的内容。

语法格式
synchronized(锁对象){// 需要同步的代码(操作共享资源的代码)}
代码实操:同步代码块解决售票问题
publicclassSafeTicketDemo1implementsRunnable{privateint ticketNum =10;// 定义锁对象:必须是多个线程共享的对象privatefinalObject lock =newObject();@Overridepublicvoidrun(){while(true){synchronized(lock){if(ticketNum >0){try{Thread.sleep(100);}catch(InterruptedException e){ e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" 售出第 "+ ticketNum--+" 张票");}else{break;}}}}publicstaticvoidmain(String[] args){SafeTicketDemo1 task =newSafeTicketDemo1();newThread(task,"窗口1").start();newThread(task,"窗口2").start();newThread(task,"窗口3").start();}}

输出结果(无重复、无超卖,解决并发安全问题)

窗口1 售出第 10 张票 窗口1 售出第 9 张票 窗口2 售出第 8 张票 窗口2 售出第 7 张票 窗口3 售出第 6 张票 窗口3 售出第 5 张票 窗口1 售出第 4 张票 窗口1 售出第 3 张票 窗口2 售出第 2 张票 窗口2 售出第 1 张票 

⚠️ 注意事项:锁对象必须是多个线程共享的对象,否则无法实现同步效果。

1.5.3 解决方案2:同步方法(synchronized)

💡 同步方法是将 synchronized 关键字修饰在方法上,等价于锁定当前对象(this)。

语法格式
publicsynchronized 返回值类型 方法名(参数列表){// 需要同步的代码}
代码实操:同步方法解决售票问题
publicclassSafeTicketDemo2implementsRunnable{privateint ticketNum =10;@Overridepublicvoidrun(){while(true){if(!sellTicket()){break;}}}// 同步方法:锁定的是this对象privatesynchronizedbooleansellTicket(){if(ticketNum >0){try{Thread.sleep(100);}catch(InterruptedException e){ e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" 售出第 "+ ticketNum--+" 张票");returntrue;}else{returnfalse;}}publicstaticvoidmain(String[] args){SafeTicketDemo2 task =newSafeTicketDemo2();newThread(task,"窗口1").start();newThread(task,"窗口2").start();newThread(task,"窗口3").start();}}

1.5.4 解决方案3:Lock 锁(JUC 包)

💡 java.util.concurrent.locks.Lock 是 JDK 5 新增的锁机制,相比 synchronized 更加灵活,可以手动控制锁的获取和释放。
常用实现类是 ReentrantLock(可重入锁)。

实现步骤

① 📝 创建 ReentrantLock 对象。
② 📝 在需要同步的代码前调用 lock() 方法获取锁。
③ 📝 在 finally 块中调用 unlock() 方法释放锁,确保锁一定会被释放。

代码实操:Lock 锁解决售票问题
importjava.util.concurrent.locks.ReentrantLock;publicclassSafeTicketDemo3implementsRunnable{privateint ticketNum =10;// 创建可重入锁对象privatefinalReentrantLock lock =newReentrantLock();@Overridepublicvoidrun(){while(true){// 获取锁 lock.lock();try{if(ticketNum >0){Thread.sleep(100);System.out.println(Thread.currentThread().getName()+" 售出第 "+ ticketNum--+" 张票");}else{break;}}catch(InterruptedException e){ e.printStackTrace();}finally{// 释放锁:必须放在finally块中 lock.unlock();}}}publicstaticvoidmain(String[] args){SafeTicketDemo3 task =newSafeTicketDemo3();newThread(task,"窗口1").start();newThread(task,"窗口2").start();newThread(task,"窗口3").start();}}

✅ 核心结论:synchronizedLock 的对比

特性synchronizedLock
锁获取释放自动获取和释放手动调用 lock()/unlock()
灵活性高(支持公平锁/非公平锁)
可中断性不支持支持
条件变量不支持支持(Condition)

1.6 线程池:高效管理多线程

1.6.1 为什么需要线程池

频繁创建和销毁线程会消耗大量系统资源,线程池可以复用线程,降低系统开销。线程池的核心优势:

  1. 降低资源消耗:复用已创建的线程,减少创建和销毁线程的开销。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 便于线程管理:可以控制最大并发数,避免线程过多导致资源耗尽。

1.6.2 线程池的核心参数(ThreadPoolExecutor)

Java 中线程池的核心实现类是 ThreadPoolExecutor,其构造方法包含 7 个核心参数:

publicThreadPoolExecutor(int corePoolSize,// 核心线程数int maximumPoolSize,// 最大线程数long keepAliveTime,// 非核心线程空闲存活时间TimeUnit unit,// 时间单位BlockingQueue<Runnable> workQueue,// 任务队列ThreadFactory threadFactory,// 线程工厂RejectedExecutionHandler handler // 拒绝策略)
参数说明
  1. 核心线程数:线程池长期保持的线程数量,即使空闲也不会销毁。
  2. 最大线程数:线程池允许创建的最大线程数。
  3. 空闲存活时间:非核心线程空闲超过该时间会被销毁。
  4. 任务队列:存放等待执行的任务,当核心线程都在忙时,任务会进入队列。
  5. 拒绝策略:当任务队列满且线程数达到最大时,如何处理新任务。

1.6.3 线程池的使用方式

方式1:通过 Executors 工具类创建(快速使用)

Executors 提供了几种常用的线程池创建方法:

  • newFixedThreadPool(int nThreads):固定大小的线程池。
  • newCachedThreadPool():缓存线程池,线程数可动态调整。
  • newSingleThreadExecutor():单线程池,保证任务顺序执行。
代码实操:固定大小线程池
importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;/** * 固定大小线程池的使用 */publicclassThreadPoolDemo{publicstaticvoidmain(String[] args){// 1. 创建固定大小的线程池,核心线程数=最大线程数=3ExecutorService executorService =Executors.newFixedThreadPool(3);// 2. 提交10个任务for(int i =1; i <=10; i++){int taskId = i; executorService.submit(()->{System.out.println("任务"+ taskId +" 由线程 "+Thread.currentThread().getName()+" 执行");try{Thread.sleep(500);}catch(InterruptedException e){ e.printStackTrace();}});}// 3. 关闭线程池 executorService.shutdown();}}

输出结果(3个线程复用执行10个任务)

任务1 由线程 pool-1-thread-1 执行 任务2 由线程 pool-1-thread-2 执行 任务3 由线程 pool-1-thread-3 执行 任务4 由线程 pool-1-thread-1 执行 任务5 由线程 pool-1-thread-2 执行 任务6 由线程 pool-1-thread-3 执行 任务7 由线程 pool-1-thread-1 执行 任务8 由线程 pool-1-thread-2 执行 任务9 由线程 pool-1-thread-3 执行 任务10 由线程 pool-1-thread-1 执行 
方式2:手动创建 ThreadPoolExecutor(推荐,更可控)
importjava.util.concurrent.*;/** * 手动创建线程池(推荐方式) */publicclassCustomThreadPoolDemo{publicstaticvoidmain(String[] args){// 1. 定义线程池参数int corePoolSize =2;int maximumPoolSize =5;long keepAliveTime =60;TimeUnit unit =TimeUnit.SECONDS;// 任务队列:容量为3的阻塞队列BlockingQueue<Runnable> workQueue =newArrayBlockingQueue<>(3);// 线程工厂:默认线程工厂ThreadFactory threadFactory =Executors.defaultThreadFactory();// 拒绝策略:丢弃任务并抛出异常RejectedExecutionHandler handler =newThreadPoolExecutor.AbortPolicy();// 2. 创建线程池ThreadPoolExecutor executor =newThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler );// 3. 提交任务for(int i =1; i <=10; i++){int taskId = i;try{ executor.submit(()->{System.out.println("任务"+ taskId +" 由线程 "+Thread.currentThread().getName()+" 执行");Thread.sleep(1000);});}catch(Exception e){System.out.println("任务"+ taskId +" 被拒绝:"+ e.getMessage());}}// 4. 关闭线程池 executor.shutdown();}}

输出结果(当任务数超过 最大线程数+队列容量=8 时,新任务被拒绝)

任务1 由线程 pool-1-thread-1 执行 任务2 由线程 pool-1-thread-2 执行 任务3 由线程 pool-1-thread-3 执行 任务4 由线程 pool-1-thread-4 执行 任务5 由线程 pool-1-thread-5 执行 任务6 由线程 pool-1-thread-1 执行 任务7 由线程 pool-1-thread-2 执行 任务8 由线程 pool-1-thread-3 执行 任务9 被拒绝:Task java.util.concurrent.FutureTask@6d311334 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0] 任务10 被拒绝:Task java.util.concurrent.FutureTask@3d075dc0 rejected from java.util.concurrent.ThreadPoolExecutor@682a0b20[Running, pool size = 5, active threads = 5, queued tasks = 3, completed tasks = 0] 

⚠️ 注意事项:实际开发中推荐手动创建线程池,避免使用 Executors 工具类的默认方法,防止资源耗尽。

1.7 实战案例:多线程实现文件批量下载

1.7.1 需求分析

💡 实现一个多线程文件下载工具,支持从指定 URL 列表批量下载文件,要求:

  1. 使用线程池管理下载线程,控制并发数。
  2. 记录每个文件的下载状态(成功/失败)。
  3. 支持断点续传(可选)。

1.7.2 代码实现

importjava.io.*;importjava.net.HttpURLConnection;importjava.net.URL;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.*;/** * 多线程文件批量下载工具 */publicclassMultiThreadFileDownloader{// 线程池:核心线程数2,最大线程数4privatestaticfinalExecutorServiceEXECUTOR=newThreadPoolExecutor(2,4,60,TimeUnit.SECONDS,newArrayBlockingQueue<>(2),Executors.defaultThreadFactory(),newThreadPoolExecutor.CallerRunsPolicy());// 下载状态列表privatestaticfinalList<String>DOWNLOAD_STATUS=newArrayList<>();/** * 下载单个文件 * @param fileUrl 文件URL * @param savePath 保存路径 */privatestaticvoiddownloadFile(String fileUrl,String savePath){HttpURLConnection connection =null;InputStream in =null;OutputStream out =null;try{URL url =newURL(fileUrl); connection =(HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(5000);// 获取文件名String fileName = fileUrl.substring(fileUrl.lastIndexOf("/")+1);File saveFile =newFile(savePath +File.separator + fileName);// 读取文件流并写入本地 in = connection.getInputStream(); out =newFileOutputStream(saveFile);byte[] buffer =newbyte[1024*8];int len;while((len = in.read(buffer))!=-1){ out.write(buffer,0, len);}String successMsg ="成功:"+ fileName;synchronized(DOWNLOAD_STATUS){DOWNLOAD_STATUS.add(successMsg);}System.out.println(successMsg);}catch(Exception e){String fileName = fileUrl.substring(fileUrl.lastIndexOf("/")+1);String failMsg ="失败:"+ fileName +",原因:"+ e.getMessage();synchronized(DOWNLOAD_STATUS){DOWNLOAD_STATUS.add(failMsg);}System.out.println(failMsg);}finally{// 关闭资源try{if(out !=null) out.close();if(in !=null) in.close();if(connection !=null) connection.disconnect();}catch(IOException e){ e.printStackTrace();}}}/** * 批量下载文件 * @param urlList URL列表 * @param savePath 保存路径 */publicstaticvoidbatchDownload(List<String> urlList,String savePath){// 检查保存路径是否存在File saveDir =newFile(savePath);if(!saveDir.exists()){ saveDir.mkdirs();}// 提交下载任务for(String url : urlList){EXECUTOR.submit(()->downloadFile(url, savePath));}// 关闭线程池EXECUTOR.shutdown();try{// 等待所有任务完成EXECUTOR.awaitTermination(1,TimeUnit.HOURS);}catch(InterruptedException e){ e.printStackTrace();}// 打印下载结果System.out.println("\n===== 下载完成 ======");for(String status :DOWNLOAD_STATUS){System.out.println(status);}}publicstaticvoidmain(String[] args){// 测试URL列表(替换为实际可下载的URL)List<String> urlList =newArrayList<>(); urlList.add("https://example.com/file1.txt"); urlList.add("https://example.com/file2.jpg"); urlList.add("https://example.com/file3.pdf"); urlList.add("https://example.com/file4.zip"); urlList.add("https://example.com/file5.doc");// 批量下载到D盘download目录batchDownload(urlList,"D:\\download");}}

1.7.3 案例总结

✅ 这个多线程下载工具综合运用了线程池、同步机制、IO流等知识,核心亮点:

  1. 使用线程池控制并发数,避免线程过多导致系统资源耗尽。
  2. 使用 synchronized 保证下载状态列表的线程安全。
  3. 采用 CallerRunsPolicy 拒绝策略,当任务过多时,由调用线程执行,避免任务丢失。
  4. 完善的资源关闭和异常处理,保证程序健壮性。

1.8 本章总结

  1. 线程是进程的执行单元,多线程可以提升程序并发效率,充分利用 CPU 资源。
  2. 线程有三种创建方式:继承 Thread、实现 Runnable、实现 Callable,推荐使用后两种。
  3. 线程的生命周期包含六种状态,核心是掌握状态之间的转换条件。
  4. 并发安全问题的根源是多个线程操作共享资源,解决方案有 synchronized 同步代码块/方法、Lock 锁。
  5. 线程池可以复用线程,降低系统开销,实际开发中推荐手动创建 ThreadPoolExecutor。
  6. 多线程编程的核心是线程同步资源管理,要注意避免死锁、内存可见性等问题。

Read more

【2025最新】Python量化数据接口指南:baostock 免费获取分钟级K线教程

baostock 是一个对Python量化爱好者非常友好的免费开源证券数据平台,尤其适合获取A股历史行情数据。我为你准备了这份2025年更新的baostock使用指南,希望能帮助你高效地获取数据。 1. 认识baostock Baostock(证券宝)是一个免费、开源的证券数据平台。它通过Python API提供大量准确、完整的证券历史行情数据、上市公司财务数据等,能满足量化交易投资者、数量金融爱好者、计量经济从业者的数据需求。 它的数据返回格式为pandas DataFrame类型,这对于使用pandas/NumPy/Matplotlib进行数据分析和可视化非常友好。 2. 数据范围与时间 baostock的数据覆盖范围主要包括: 数据类型 包含内容 时间范围 备注                 股票数据 日、周、月K线数据 1990-12-19至今 5、15、30、60分钟K线数据 1999-07-26至今 指数数据 综合指数,规模指数,一级行业指数,二级行业指数,策略指数,成长指数,价值指数,主题指数,基金指数,

By Ne0inhk
Python中的“==“与“is“:深入解析与Vibe Coding时代的优化实践

Python中的“==“与“is“:深入解析与Vibe Coding时代的优化实践

🌟 Python中的"=="与"is":深入解析与Vibe Coding时代的优化实践 * 1. 🧐 `==`与`is`的本质区别 * 2. 🕵️‍♂️ `is`判断对象身份 - 数组与常量池案例 * 案例1:列表对象的身份 * 案例2:小整数常量池 * 案例3:字符串驻留 * 3. 🔍 `==`与`__eq__`魔法函数 * 4. 🔎 类型判断的正确姿势:使用`is` * 5. 🚀 Vibe Coding时代的提示词优化 * 场景1:解释概念 * 场景2:代码生成 * 场景3:调试帮助 * 📊 对比总结表 * 💡 实际应用建议 * 🌈 结语 在Python的奇妙世界中,==和is这两个看似简单的操作符常常让初学者感到困惑。它们如同双胞胎,外表相似却性格迥异。本文将带你深入探索它们的区别,并通过生动的案例和图表展示它们的应用场景,

By Ne0inhk
Python 绘制动态跳动爱心|情人节专属浪漫代码,新手零基础也能上手

Python 绘制动态跳动爱心|情人节专属浪漫代码,新手零基础也能上手

马上就是情人节,程序员的浪漫从一行行代码开始!今天分享一款纯 Python 内置库实现的动态跳动爱心,无需复杂第三方依赖,黑色背景搭配粒子化爱心,自带自然的跳动节奏和柔和光晕,既适合送给心仪的人制造惊喜,也能作为 Python 基础练手案例。本文全程保姆级文本解析 + 代码注释双保障,从环境搭建到代码逻辑逐字拆解,纯新手也能跟着一步步实现,轻松拿捏编程浪漫~ 这是最近粉丝私信求表白代码的聊天记录 —— 情人节 / 过年想给心仪的人制造浪漫,用代码做一份专属爱心礼物再合适不过,安排! 一、效果预览 运行代码后会直接弹出640×480的独立图形窗口,黑色背景搭配粒子化粉色爱心,实现沉浸式浪漫视觉效果,核心效果如下: 1. 爱心以自然的周期性节奏跳动,完成“收缩-扩张-收缩”的循环,流畅无卡顿; 2. 爱心由大量细腻粒子构成,轮廓清晰、内部填充饱满,边缘带有轻微粒子扩散效果; 3. 爱心外围附带动态光晕,光晕的大小、粒子数量随爱心跳动节奏同步变化,氛围感拉满; 4. 全程动态渲染,对电脑性能无要求,低配设备也能流畅运行,关闭窗口即可停止程序。

By Ne0inhk

Anaconda环境变量PYTHONPATH设置:导入自定义PyTorch模块

Anaconda环境变量PYTHONPATH设置:导入自定义PyTorch模块 在深度学习项目开发中,一个看似微小的路径问题常常让开发者陷入“明明代码没错,却无法运行”的窘境。比如你在Jupyter Notebook里写好了模型结构、数据加载器和训练脚本,结果一执行就弹出ModuleNotFoundError: No module named 'models'——这种报错几乎每个PyTorch使用者都曾经历过。 问题的根源往往不在于代码逻辑,而在于Python解释器找不到你的自定义模块。尤其是在使用Anaconda虚拟环境或容器化镜像(如PyTorch-CUDA-v2.8)时,如果没有正确配置PYTHONPATH,即使文件结构清晰、类定义完整,也无法被正常导入。 这背后其实是一个典型的工程实践问题:如何让开发环境“理解”你组织代码的方式?答案就是合理利用PYTHONPATH这一机制,在不修改源码的前提下,打通模块搜索路径。 假设我们有一个标准的项目结构: /my_project ├── models/ │ └── custom_net.py ├── utils/ │ └──

By Ne0inhk