十九. 多线程
多线程
- 多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销
- 能满足程序员编写高效率的程序来达到充分利用 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 容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待
- 为什么要定义 while 判断标记?
- wait();notify();notifyAll()
