十九. 多线程
2026/6/12 5:22:59 网站建设 项目流程

多线程

  • 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销
  • 能满足程序员编写高效率的程序来达到充分利用 CPU的目的
  • 特性:随机性,谁抢到谁执行,至于执行多长时间,CPU说的算

进程 && 线程

  • 一个进程中可以并发多个线程每条线程并行执行不同的任务
  • 一个进程包括由操作系统分配的内存空间包含一个或多个线程
  • 一个线程指的是进程中一个单一顺序的控制流
  • 一个线程不能独立的存在,它必须是进程的一部分
  • 一个进程一直运行,直到所有的非守护线程都结束运行后才能结束
  • 进程退出时,该进程所产生的线程都会被强制退出并清除

进程

  • 正在进行中的程序实例
  • 特征:
    • 每个进程独立占用系统资源
    • 通过任务管理器可查看所有运行中的进程
  • 每个进程执行都有执行顺序,此顺序是一个执行路径或者叫一个控制单元
  • 扩展:
    • JVM 启动时会有一个进程(java.exe),该进程中至少一个线程负责Java程序的执行
    • 而这个线程运行的代码存在于main方法中,该线程称之为主线程
    • 其实,JVM启动不止一个线程,还有负责垃圾回收的线程

线程

  • 就是进程中的一个独立的控制单元
  • 线程在控制着进程的执行
  • 一个进程中至少有一个线程

进程 VS 线程

  • 地址空间:
    • 线程:进程内的一个执行单元;共享进程的地址空间
    • 进程:至少有一个线程;有自己独立的地址空间
  • 进程是资源分配和拥有的单位,同一进程内的线程共享进程的资源
  • 线程是处理器调度的基本单位,但进程不是
  • 二者均可并发执行

线程的生命周期

  • 线程是一个动态执行的过程,有一个从产生到死亡的过程
  • 新建状态(NEW)
    • 使用new Thread类/其子类建立一个线程对象后,该线程对象就处于新建状态
    • 保持这个状态直到程序 start() 这个线程
  • 就绪状态(RUNNABLE)
    • 调用start()之后,该线程就进入就绪状态
    • 此状态的线程处于就绪队列中,要等待JVM里线程调度器的调度
  • 运行状态
    • 就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态
    • 处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态
  • 阻塞状态(BLOCKED)
    • 一个线程执行了sleep(睡眠)、suspend(挂起) 等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态
    • 睡眠时间已到获得设备资源后可以重新进入就绪状态
    • 分三种:
      • 等待阻塞:运行状态中的线程执行wait()方法,使线程进入到等待阻塞状态
      • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)
      • 其他阻塞:通过调用线程的sleep()join()发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时、join() 等待线程终止或超时、或者 I/O 处理完毕,线程重新转入 就绪状态
  • 死亡状态(TERMINATED)
    • 一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到 终止状态

线程的优先级

  • 每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序
  • 优先级是一个整数,其取值范围是 1(Thread.MIN_PRIORITY) - 10(Thread.MAX_PRIORITY)
  • 默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)
  • 具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源
  • 但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台

创建一个线程

通过实现 Runnable 接口

  • 定义类实现 Runnable 接口
  • 覆盖 Runnable 接口中的 run()
    • 将线程要运行的代码存放在该 run() 中
  • 通过 Thread 类建立线程对象
  • 将 Runnable 接口的子类对象作为实参传递给Thread类的构造函数
    • why?
    • 因为,自定义的 run() 所属的对象是 Runnable 接口的子类对象
    • 所以,要让线程去执行指定对象的 run(),就必须明确该 run() 所属对象
  • 调用 Thread 类的 start() 开启线程并调用 Runnable 接口子类的 run()
  • 扩展:避免了单继承的局限性
classRunnableDemoimplementsRunnable{privateStringthreadName;RunnableDemo(Stringname){threadName=name;System.out.println("Creating "+threadName);}@Overridepublicvoidrun(){System.out.println("Running "+threadName);try{for(inti=3;i>0;i--){System.out.println("Thread: "+threadName+", "+i);// 让线程睡眠一会Thread.sleep(50);}}catch(InterruptedExceptione){System.out.println("Thread "+threadName+" interrupted.");}System.out.println("Thread "+threadName+" exiting.");}}
RunnableDemor1=newRunnableDemo("Thread-1");newThread(r1).start();

通过继承 Thread 类本身

  • 定义类继承 Thread
  • 复写 Thread 类中的 run()
    • 目的:将自定义代码存储在 run(),让线程运行
  • 调用线程的 start()
    • 该方法两个作用:启动线程,调用run()
classThreadDemoextendsThread{privateStringthreadName;ThreadDemo(Stringname){threadName=name;System.out.println("Creating "+threadName);}@Overridepublicvoidrun(){System.out.println("Running "+threadName);try{for(inti=3;i>0;i--){System.out.println("Thread: "+threadName+", "+i);// 让线程睡眠一会Thread.sleep(50);}}catch(InterruptedExceptione){System.out.println("Thread "+threadName+" interrupted.");}System.out.println("Thread "+threadName+" exiting.");}}
ThreadDemothread=newThreadDemo("Thread-1");thread.start();

通过 Callable 和 Future 创建线程

  • 创建 Callable 接口的实现类,并实现 call() ,该 call() 将作为线程执行体,并且有返回值
  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 的返回值
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
  • 调用 FutureTask 对象的 get() 来获得子线程执行结束后的返回值
publicclassCallableFutureTestimplementsCallable<Integer>{publicstaticvoidmain(String[]args){CallableFutureTestctt=newCallableFutureTest();FutureTask<Integer>ft=newFutureTask<>(ctt);for(inti=0;i<100;i++){System.out.println(Thread.currentThread().getName()+" 的循环变量i的值:"+i);if(i==20){newThread(ft,"Callable call").start();}}try{System.out.println("子线程的返回值:"+ft.get());}catch(InterruptedExceptione){e.printStackTrace();}catch(ExecutionExceptione){e.printStackTrace();}}@OverridepublicIntegercall()throwsException{inti=0;for(;i<100;i++){System.out.println(Thread.currentThread().getName()+" "+i);}returni;}}

创建线程的三种方式的对比

  • 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable接口还可以继承其他类
  • 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用this即可获得当前线程

最佳实践

  • 优先使用实现 Runnable 接口的方式创建线程
  • 使用线程池管理线程资源
  • 避免过度同步,只在必要时使用同步机制
  • 使用volatile关键字确保变量的可见性
  • 考虑使用Java 并发包(java.util.concurrent)中的高级工具类

Thread 类

  • Java 中用于创建和管理线程的核心类
  • 该类定义了一个功能,用于存储线程要运行的代码,该存储功能就是 run()
  • 也就是说,Thread 类中的 run(),用于存储线程要运行的代码

重要方法

  • public voidstart():使该线程开始执行;Java 虚拟机调用该线程的 run 方法
    • 核心:start0(); 本地方法,Java无权调用,交给底层的C处理
    • private native void start0();
  • public voidrun():如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
  • public final voidsetName(String name):改变线程名称,使之与参数 name 相同
  • public final voidsetPriority(int priority):更改线程的优先级
  • public final void setDaemon(boolean on):将该线程标记为守护线程用户线程
  • public final voidjoin(long millisec):等待该线程终止的时间最长为 millis 毫秒
  • public voidinterrupt():中断线程
  • public final boolean isAlive():测试线程是否处于活动状态

静态方法

  • public static void yield():暂停当前正在执行的线程对象,并执行其他线程
  • public static voidsleep(long millisec):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响
    • 本线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复
    • 不会释放对象锁
  • public static boolean holdsLock(Object x):当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true
  • public static ThreadcurrentThread():返回对当前正在执行的线程对象的引用
  • public static void dumpStack():将当前线程的堆栈跟踪打印至标准错误流

ExecutorService 类

  • Java 并发编程中的一个核心接口,它属于 java.util.concurrent 包
  • 提供了一种更高级的线程管理方式,允许开发者高效地执行异步任务,而无需手动创建和管理线程
  • 主要作用:
    • 线程池管理:自动管理线程的生命周期,减少线程创建和销毁的开销
    • 任务调度:支持提交 Runnable 或 Callable 任务,并返回 Future 对象以跟踪任务执行状态
    • 资源优化:通过线程池复用线程,提高系统性能

创建

  • Java 提供了Executors 工具类来创建不同类型的线程池
  • ExecutorService newFixedThreadPool(int nThreads【, ThreadFactory threadFactory】):创建固定大小的线程池,适用于负载稳定的任务
  • ExecutorService newCachedThreadPool(【ThreadFactory threadFactory】):创建可缓存的线程池,适用于短生命周期的异步任务
  • ExecutorService newSingleThreadExecutor(【ThreadFactory threadFactory】):创建单线程的线程池,适用于顺序执行的任务
  • ScheduledExecutorService newScheduledThreadPool(int corePoolSize【, ThreadFactory threadFactory】):创建支持定时或周期性任务的线程池

核心方法

  • submit():用于提交一个任务(Runnable 或 Callable)并返回 Future 对象,以便检查任务是否完成或获取返回值
ExecutorServiceexecutor=Executors.newFixedThreadPool(2);Future<?>future=executor.submit(()->{System.out.println("Task is running");});
  • execute():仅用于提交 Runnable 任务,不返回任何结果
executor.execute(()->{System.out.println("Task executed");});
  • shutdown():优雅关闭线程池,不再接受新任务,但会等待已提交的任务完成
executor.shutdown();
  • shutdownNow():立即关闭线程池,尝试中断所有正在执行的任务,并返回未执行的任务列表
List<Runnable>notExecutedTasks=executor.shutdownNow();
  • awaitTermination():等待线程池关闭,直到所有任务完成或超时
executor.awaitTermination(10,TimeUnit.SECONDS);

线程池

  • 在实际开发中,通常使用线程池来管理线程,而不是直接创建 Thread 对象
importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;publicclassThreadPoolExample{publicstaticvoidmain(String[]args){ExecutorServiceexecutor=Executors.newFixedThreadPool(5);for(inti=0;i<10;i++){executor.execute(()->{System.out.println("线程执行任务");});}executor.shutdown();}}

提交 Callable 任务并获取结果

ExecutorServiceexecutor=Executors.newFixedThreadPool(2);Future<String>future=executor.submit(()->{Thread.sleep(1000);return"Task completed";});try{Stringresult=future.get();// 阻塞直到任务完成System.out.println(result);}catch(Exceptione){e.printStackTrace();}executor.shutdown();

最佳实践

  • 合理设置线程池大小:
    • CPU 密集型任务:线程数 = CPU 核心数 + 1
    • IO 密集型任务:线程数 = CPU 核心数 * 2
  • 避免内存泄漏:确保调用 shutdown() 或 shutdownNow() 关闭线程池
  • 处理异常:使用 try-catch 捕获任务中的异常,防止线程意外终止
  • 使用 Future 管理任务:通过 Future.get() 获取任务结果或检查任务状态

线程的主要概念

  • 有效利用多线程的关键是理解程序是并发执行而不是串行执行的
  • 通过对多线程的使用,可以编写出非常高效的程序
  • 如果创建了太多的线程,程序执行的效率实际上是降低了,而不是提升了
  • 上下文的切换开销也很重要,如果创建了太多的线程CPU 花费在上下文的切换的时间将多于执行程序的时间

线程同步

  • 当多个线程访问共享资源时,需要使用同步机制来避免数据不一致问题
  • 同步的前提:
    • 必须有2个及以上的线程
    • 必须是多个线程使用同一个锁
  • 好处:解决了多线程的安全问题
  • 坏处:多个线程需要判断锁较为消耗资源
  • 必须保证同步中只能有一个线程在运行
  • 静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象 类名.class,该对象的类型是class
  • 静态的同步方法,使用的锁是该方法所在类的字节码文件对象 类名.class

使用 synchronized 关键字

  • 同步代码块(常用)
    • 对象如同锁,持有锁的线程可以在同步中执行
    • 没有持有锁的线程即使获取CPU的执行权,也进不去,因为没有获取锁
synchronized(对象){需要被同步的代码}
  • 同步方法
    • 一个对象的一个 synchronized 方法只能由一个线程访问
    • 方法需要被对象调用,那么方法都有一个所属对象引用,就是this,所以同步方法使用的锁是this
classCounter{privateintcount=0;// 同步方法publicsynchronizedvoidincrement(){count++;}}

使用 Lock 接口(优先级高)

  • JDK1.5中提供了多线程升级解决方案
  • 将同步 synchronized 替换成显示 Lock 操作
  • 将 Object 中的wait()、notify()、notifyAll()替换成 Condition 中的await()、signal()、signalAll()
  • 该 Condition对象 可通过 Lock锁 进行获取,Lock锁 可定义 多个Condition
  • 可实现本方只唤醒对方操作
  • synchronized 会自动释放锁,而 Lock 一定要手工释放
importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;classCounter{privateintcount=0;privateLocklock=newReentrantLock();publicvoidincrement(){lock.lock();// 加锁try{count++;}finally{lock.unlock();// 释放锁}}}

优先级

  • Lock > 同步代码块 > 同步方法

线程间通信

  • 其实就是多个线程在操作同一个资源,但操作的动作不同

线程死锁

  • 同步中嵌套同步
  • 两个人都抱着对方的锁
    • 互斥、请求与保持、不剥夺条件、循环等待条件

线程控制:挂起、停止和恢复

  • wait():使当前线程放弃执行资格进入等待状态
  • notify():随机唤醒一个等待线程(通常是最早等待的)
    • 注意:不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而不是按优先级
  • notifyAll():唤醒所有等待线程
    • 注意:并不是给所有唤醒线程一个对象的锁,而是让它们竞争
  • 必须配合 synchronized 使用,否则会抛出 IllegalMonitorStateException
  • 每次 wait() 前必须 notify(),避免线程永久等待
  • 扩展:
    • wait();notify();notifyAll()
      • 都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁
      • why?这些操作线程的方法要定义在Object类中?
        • 因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁
        • 只有同一个锁上的被等待线程,可以被同一个锁上的notify 唤醒,不可以对不同锁中的线程进行唤醒
        • 也就是说,等待和唤醒必须是同一个锁
        • 可以是任意对象,所以可以被任意对象调用的方法定义在Object类中
    • 多消费者多生产者:while + notifyAll
      • 为什么要定义 while 判断标记?
        • 让被唤醒的线程再一次判断标记
      • why?notifyAll
        • 因为需要唤醒对方线程
        • 因为只用 notify 容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询