多线程相关记录

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、进程与线程

进程

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

线程

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

线程的创建方式

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用四种方式来创建线程。

  • 继承Thread类创建线程
    通过继承Thread类来创建并启动多线程 (extends Thread)
    1、定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
    2、创建Thread子类的实例,也就是创建了线程对象
    3、启动线程,即调用线程的start()方法

  • 实现Runnable接口创建线程
    通过实现Runnable接口创建并启动线程 (implements Runnable)
    1、定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
    2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
    3、第三步依然是通过调用线程对象的start()方法来启动线程

  • 使用Callable和Future创建线程
    Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。(implements Callable )
    1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
    2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
    3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
    4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

  • 使用线程池例如用Executor框架
    线程池:提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提高了响应的速度。
    提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。

:Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代码看起来会更加简洁。

工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))。

线程池
为什么使用线程池:
  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Executor框架

Java中线程池核心实现类是ThreadPoolExecutor
使用过程:

  • 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
  • 把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task))。
  • 如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
    1、execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
    2、submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。)
  • 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
线程池的重要参数

ThreadPoolExecutor 几 个最重要的参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit:keepAliveTime的时间单位
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。
    如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:
    1、ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
    2、ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
    3、ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
    4、ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
线程池的7种创建方式
  • Executors.newFixedThreadPool:创建一个固定大小的线程池,可以控制并发的线程数,超出的线程会在队列中等待;(允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。)
  • Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;(允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。)
  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;(允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。)
  • Executors.newSingleThreadExecutor:创建单个线程的线程池,它可以保证先进先出的执行顺序(允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。)
  • Executors.newSingleThreadScheduledExecutor:创建一个可以执行延迟任务的单个线程的线程池;
  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池;
  • ThreadPoolExecutor:手动创建线程池 的方式,它最多包含了7个参数可供设置,最少可设置5个参数。

:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy

线程的生命周期

线程的生命周期分为5个阶段,分别为:新建、就绪、运行、阻塞、死亡。

  1. 新建:使用new方法,new出来线程,此时仅仅由JAVA虚拟机为其分配内存,并初始化成员变量的值。此时仅仅是个对象。
  2. 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
    该线程进入就绪状态,JAVA虚拟机会为其创建方法调用栈和程序计数器。线程的执行是由底层平台控制, 具有一定的随机性。
  3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;(当处于就绪状态的线程获得CPU,它就会执行run()方法)
    对于一个单核cpu(或者是一个内核)来说,只能同时执行一条指令,而JVM通过快速切换线程执行指令来达到多线程的,真正处理器就能同时处理一条指令,只是这种切换速度很快,我们根本不会感知到。为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
    当一个线程开始运行后,它不可能一直持有CPU(除非该线程执行体非常短,瞬间就执行结束了)。所以,线程在执行过程中需要被中断,目的是让其它线程获得执行的CPU的机会。线程的调度细节取决于底层平台所采用的策略。
  4. 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态。原因如下:
  • 等待I/O流的输入输出
  • 等待网络资源,即网速问题
  • 调用sleep()方法,需要等sleep时间结束
  • 调用wait()方法,需要调用notify()唤醒线程
  • 其他线程执行join()方法,当前线程则会阻塞,需要等其他线程执行完。

从阻塞状态恢复到就绪状态 :自动恢复,用resume方法恢复和notify或notifyAll()
调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

  1. 死亡:线程会通过以下三种方式的一种结束,结束后就会处于死亡状态:
  • run()方法执行完成,线程正常结束;
  • 线程抛出一个未捕获的Exception或Error;
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用;

线程的状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED :阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。
上下文切换

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。

sleep() 方法和 wait() 方法

共同点 :两者都可以暂停线程的执行。

区别 :
sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成 。

通过程序计数器,将一个个指令,可以连接成一个完整的动作,从而完成整个程序的执行。
在这里插入图片描述
两个主要作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

Java 虚拟机栈

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)

  • 局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)

可能出现的两种异常

  • StackOverFlowError(栈内存溢出): 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
  • OutOfMemoryError(堆内存溢出): 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
native方法即本地方法,就是java调用非java代码的接口。native方法是这样一个java方法:该方法的实现由非java语言实现,比如C或C++。

  • 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

  • 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。

在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,**它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。**虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
在这里插入图片描述

JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

进程与线程的关系

线程 是 进程 划分成的更小的运行单位。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
在这里插入图片描述

二、并发与并行

  • 并发:并发在一台处理器上“同时”处理多个任务。
  • 并行:并行在多台处理器上同时处理多个任务。

三、为什么使用多线程

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值