当前位置: 首页>后端>正文

2.28TODO

1. 多线程

  1. 进程是程序的一次执行过程,是系统运行的基本单位。

  2. 线程是比进程更小的执行单位,一个进程可以包含多个线程

  3. 并行:单位时间多个处理器同时处理任务

  4. 并发:单个处理器处理多个任务,按照时间片轮流处理

  5. 即使是单核的处理器也支持多线程,处理器会给每个线程分配CPU时间片,线程根据拿到的时间片去执行任务,因此多线程会经常进行线程切换,当切换到下一个线程时,当前线程会保存当前任务的执行状态,等待再次拿到时间片再继续执行任务

  6. 多线程的优缺点

    1. 优点:当一个线程进入阻塞或者等待状态时,CPU可以先去执行其他线程,提高了CPU的效率
    2. 缺点:
      1. 频繁进行线程切换,上下文切换,影响执行速度
      2. 死锁问题
  7. 死锁如何产生,如何避免

    1. 产生:两个或两个以上的线程互相竞争对方的资源,而且同时不释放自己持有的资源时,发生死锁,导致所有线程同时阻塞
    2. 条件:
      1. 互斥条件:一个资源在同一时刻只能由一个线程持有
      2. 请求与保持状态:一个线程在请求获取资源时发生阻塞,同时它持有的资源不释放
      3. 循环等待条件:发生死锁时,所有线程一致阻塞
      4. 不剥夺条件:线程已获得的资源在未使用完时不能被其他线程剥夺,只能由自己使用完后释放
    3. 避免:破环死锁产生的条件
      1. 互斥条件无法被破坏,因为锁的作用就是使线程互斥
      2. 破坏请求与保持条件:一次性请求所有的资源
      3. 破坏循环等待条件:按顺序来申请资源
      4. 破坏不剥夺条件:在申请不到资源时,释放自己持有的资源
  8. 线程的生命周期

    1. 初始:线程被创建,还没有调用start()
    2. 可运行:调用start()后,等待cpu调度
    3. 运行:执行run()方法,就绪和运行两种状态
    4. 阻塞:一般是被动的,在竞争资源时得不到资源,被动挂起在内存,等待资源被释放将其唤醒。
    5. 等待:进入该状态的线程需要等待其他线程的动作(通知或中断)
    6. 超时等待
    7. 终止:执行完毕
  9. 线程中断:线程在运行过程中被其他线程打断

    1. interrupt():给目标线程发送中断信号
    2. interrupted():判断目标线程是否被中断
  10. 创建线程的方式:实现接口比较好,开销小

    1. 继承Thread类,重写run()方法,创建子类实例对象,调用start()方法
    2. 实现Runnable接口,重写run()方法,创建接口实现类的实例对象,将实例对象作为参数创建Thread对象,thread对象调用start()方法
    3. 实现Callable接口
      1. 创建Callable接口的实现类,重写call()方法
      2. 使用实现类的实例化对象,创建FutureTask对象
      3. 使用FutureTask对象作为参数创建Thread对象
      4. 调用start()方法
    4. 线程池
  11. Runnable接口和Callable接口

    1. 相同点:都是接口,都需要调用Thread.start()启动线程
    2. 不同点:Callable是call()方法,有返回值。Runnable是run()方法,没有返回值
  12. start()方法和run()方法

    1. run()方法定义执行的逻辑,start()方法启动线程
    2. run()方法可以多次被执行,start()方法只能调用一次
  13. 线程的其他方法:wait()、sleep()、notify()、notifyAll()、join()、yield()

    1. wait():使一个线程处于阻塞状态,并释放所持有的对象的锁
    2. sleep():使当前线程进入指定的毫秒数的休眠,暂停执行,需要处理InterruptedException
    3. notify():唤醒一个处于等待状态的线程,不能确切唤醒某一个,由JVM决定唤醒,与优先级无关
    4. notifyAll():唤醒所有处于等待状态的线程,让它们竞争,获得锁的线程进入就绪状态
    5. join():在一个线程中调用另一个线程的join()方法,会使当前线程挂起,知道执行join()方法的线程结束。比如B线程中调用A线程的join()方法,B线程进入阻塞状态,知道A线程结束
    6. yield():提醒调度器,线程愿意放弃当前的CPU资源,使线程从运行状态切换到就绪状态
  14. sleep()方法和yield()方法

    1. sleep()方法会使当前线程暂停指定的时间,没有消耗cpu时间片
    2. sleep()方法使线程进入阻塞状态,yield()对cpu进行提示,使得上下文进行切换,线程进入就绪状态
    3. sleep()一定会完成指定的休眠时间,yield()不一定
    4. sleep()需要抛出InterruptException
  15. sleep()方法和wait()方法

    1. sleep()是Thread类的方法,wait()是Object类的方法
    2. 都能使线程进入阻塞状态
  16. 线程通信方式

    1. volatile:读时要求线程去主内存读取最新的变量,写时将工作内存修改后的变量刷新到主内存
    2. synchronized:读时加锁实现只有一个线程去主内存读取变量,写时释放锁后将修改的变量刷新到主内存
    3. wait()和notify():线程A调用wait()进入等待,线程B调用notify()将线程A唤醒
    4. join():线程A调用join()阻塞线程B,线程B要等待线程A执行完才能继续执行
  17. 为什么wait()、notify()、notifyAll()是在Object类中

    1. 因为只有同一个锁上等待的线程才能被notify()和notifyAll()唤醒,而锁可以是任何对象,因此都属于Object类可以保证wait()线程一定能被notify()唤醒
  18. 在Java中如何保证线程安全:原子性、可见性、有序性。volatile不能保证原子性,所以volatile不是线程安全的

    1. 原子性:一个操作要么全部执行成功,要么执行失败。线程切换导致的原子性问题

    2. 内存可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存导致的可见性问题

    3. 指令有序性:指代码按照先后书写的顺序执行,但是实际编译时,可能会出现指令重排现象,编译结果跟原先代码的书写顺序不一致

    4. 如何解决:

      1. 原子性:atom原子类、lock、synchronized
        1. 原子类:会以原子方式将当前值加 1,并返回更新后的值。
        2. lock和synchronized保证只有一个线程对变量进行操作
      2. 可见性:synchronized、lock、volatile
        1. volatile读时需要从主内存读取最新的变量,写时将最新修改的变量刷新到主内存
        2. synchronized读时加锁实现只有一个线程去主内存读取变量,写时释放锁后将修改的变量刷新到主内存
      3. 有序性:synchronized、lock、volatile
        1. synchronized和lock保证了只有一个线程去执行代码指令
        2. volatile通过内存屏障保证有序性
    5. happens-before原则:先行发生原则。操作A在操作B之前执行,则操作A中对共享变量的修改要对执行操作B的线程可见,且操作A不会在操作B之后执行。

    6. 为什么volatile不能保证原子性
      对于复合操作,比如a++,这个操作有三步:
      1、拿到a
      2、执行a+1
      3、将a+1的结果赋值给a
      假设现在a=1,线程A和线程B同时执行a++,线程A先拿到a=1,开始执行a+1。而此时线程B已经完成,将a=2刷新回主内存。那么此时线程A拿到的a=1就失效了

  19. synchronized关键字

    1. synchronized和volatile:synchronized可以保证原子性、可见性、有序性。volatile不能保证原子性

    2. 实现的是什么锁

      1. 悲观锁:每次访问共享资源都会上锁
      2. 非公平锁:线程获取锁的顺序不一定是按照线程阻塞的顺序
      3. 可重入锁:已经获取过锁的线程可再次获取锁
      4. 排他锁:锁只能被一个线程持有,其他线程被阻塞
    3. 使用方式

      1. 修饰普通同步方法:作用在方法所属的类实例对象,new两个实例对象,无法实现锁
      2. 修饰静态方法:作用在方法所属的类,new两个类实例对象,能实现锁
      3. 修饰代码块:锁粒度小
    4. 底层原理:

      1. 其底层有一个monitor,当有多个线程进来获取锁时,先通过CAS方式将monitor的owner变更为当前线程,成功则count+1。当线程执行完同步代码块后,释放锁,count-1,当前count为0时,表示锁可以被获取
    5. 锁升级

      1. 无锁
      2. 偏向锁
      3. 轻量级锁
      4. 重量级锁
  1. volatile关键字

    1. volatile是一个轻量级的synchronized,一般作用于变量

    2. 特性:内存可见性、指令有序性、不能保证原子性

    3. 如何实现内存可见性:
      读:要求线程去主内存读取最新的变量
      写:将最新修改的变量刷新到主内存

    4. 如何实现指令有序性:插入内存屏障

      1. 写操作前:确保之前的写都已经刷新到主内存
      2. 写操作后:禁止与后面的volatile操作重排
      3. 读操作前:禁止与后面的读重排
      4. 读操作后:禁止与后面的写重排
    5. volatile与synchronized的区别

      1. 作用域:volatile作用于变量,synchronized作用于方法和代码块
      2. 线程安全:volatile不是线程安全(不能保证原子性),synchronized是线程安全
      3. 线程阻塞:volatile不会阻塞线程,synchronized会阻塞线程
  2. CAS:compareAndSwap比较并替换

    1. CAS是一种无锁机制,有三个操作数:内存值、预期值、新值。当且仅当内存值等于预期值时,才会将内存值替换为新值,否则进行自旋。也就是说当多个线程共同操作一个共享变量时,只有一个线程可以对变量进行更新,其他线程会操作失败,然后进行再次尝试,这就是自旋
    2. CAS的自旋操作:是CPU级别的操作,原子性操作,速度快,但是CPU占用高,因此可以设置一个自旋上限
    3. ABA问题:指的是预期值本来是A,后来变成了B,最后又变成A,此时线程进来更新,发现预期值是A,就把新值更新了
    4. 解决ABA问题:原子类的AtomicStampedReference,在变量前加一个版本号,这样就变成了1A2B3A
    5. 用途:synchronized的锁升级过程用到CAS,CouncurrentHashMap的put方法用到了CAS,原子类的自增方法用到CAS
    6. Unsafe类调用cas
  3. AQS:抽象队列同步器,使用一个volatile的int类型的成员变量state来同步状态,通过CAS修改同步状态的值,当线程尝试获取锁时,如果此时state为0,则线程可以获取锁,state修改为1。其他线程就要等待state变为0才能获取

  4. ThreadLocal

    1. 底层原理:每个线程都有一个ThreadLocalMap,key为ThreadLocal,value为object对象

    2. java引用类型

      1. 强引用:gc时不会被回收
      2. 软引用:发生内存溢出时被回收
      3. 弱引用:下一次gc时被回收
      4. 虚引用:随时会被回收
    3. 内存泄漏问题

      1. 因为作为key的threadlocal是弱引用,而value值object对象是强引用,key是弱引用在下一次gc时被回收了,而value是强引用不会被gc回收,但是key没了,找不到value,value就永远不会被回收
    4. 解决内存泄漏:在调用set()、get()、remove()这些方法时,会清理掉key为null的记录,所以使用完之后调用remove()

  1. Reentrantlock和synchronized
    1. 可重入:synchronized和reentrantlock都是可重入的
      1. synchronized通过AQS实现
      2. reentrantlock在每次获得锁的时候,检查当前维护的线程id和当前正在请求的线程id是否一致,如果一致,计数器+1,表示锁被当前线程获取了多次
    2. 实现方式
      1. synchronized是通过JVM
      2. reentrantlock是通过jdk
    3. synchronized只能实现非公平锁
    4. synchronized只能通过随机唤醒wait线程,reentrantlock可以绑定多个condition实现精确唤醒
    5. 如果不是需要使用ReentrantLock的高级功能,优先使用synchronized,因为synchronized是基于JVM实现的,不需要手动释放锁,而ReentrantLock要手动释放锁,否则会造成死锁
  1. 公平锁和非公平锁
    1. new ReentrantLock();默认是非公平锁,可以通过new ReentrantLock(boolean fair)指定创建公平锁
    2. 公平锁指的是只有阻塞队列头部的线程才能获取锁,而非公平锁只要线程通过AQS的CAS成功获取锁就能
  1. 乐观锁:CAS,不加锁执行,进行重试,直到成功

  2. 悲观锁:synchronized和reentrantlock,先加锁再操作

  1. 线程池
    1. 优点:线程可复用,减少重复创建线程和销毁线程

    2. 缺点:大量创建线程会导致OOM

    3. 创建线程池方式:

      1. Executor工厂:
        1. newSingleThreadExecutor():创建单线程的线程池
        2. newFixedThreadPool():创建固定数量的线程池
        3. newCachedThreadPool():创建可缓存的线程池
        4. newScheduleThreadPool():创建固定数量的定时线程池的线程池
      2. new ThreadPoolExecutor()
    4. 重要参数

      1. corePoolSize:核心线程数,定义了最小可同时工作的线程数量
      2. maximumPoolSize:线程池中允许存在的最大工作线程数量
      3. workQueue:存放任务的阻塞队列,新来的任务会先判断当前运行的线程数是否已达到核心线程数,如果达到,任务会先放在阻塞队列
      4. keepAliveTime:当线程池中数量大于核心线程数时,如果没有新的任务提交,核心线程数量外的线程等待keepAliveTime后销毁
      5. unit:keepAliveTime的时间单位
      6. threadFactory:创建新线程的线程工厂
      7. handler:线程池任务数量超过maximumPoolSize的拒绝策略
    5. 拒绝策略:

      1. abortPolicy:抛出异常拒绝任务
      2. callerRunPolicy:由提交该任务的线程处理
      3. discardPolicy:丢弃新任务
      4. discardOldestPolicy:丢弃最早的未处理任务
    6. 使用Executor创建线程池有哪些坑

      1. CachedThreadPool()和ScheduledThreadPool()的maximumPoolSize是Integer.MAX_VALUE,可能创建大量线程导致OOM
      2. FixedThreadPool()和SingleThreadPool()的workQueue的长度为Integer.MAX_VALUE,可能堆积大量的等待任务导致OOM
    7. execute():提交没有返回值的任务

    8. submit():提交有返回值的任务

2. JVM

  1. JVM内存结构:程序计数器、虚拟机栈、本地方法栈、堆、方法区
    1. 堆:是JVM中最大的一块内存空间,存放new创建的对象,gc回收主要回收的就是堆中的对象,堆又分为新生代和老年代

      1. 新生代:存放新创建的对象和短期存活的对象。新生代分为eden区、fromSurvivor区、toSurvivor区。对象优先分配到eden区,当eden区空间满了之后,执行MirrorGc,回收新生代的空间,如果对象经过多次gc(默认15次)后依旧存活,则移动到老年代空间
      2. 老年代:存放长期存活的对象和大对象,如果新创建的对象是大对象,直接分配到老年代。当老年代空间满了之后,会执行FullGc,回收堆空间,如果FullGc后空间依旧不足,抛出OOM
    2. 栈:java虚拟机栈和本地方法栈

      1. java虚拟机栈:执行java方法,期间会创建栈帧,存放局部变量表,局部变量表上存放着基本数据类型和引用数据类型的引用指针。
      2. 本地方法栈:执行System方法,
    3. 方法区:存放被加载的类信息、常量、静态变量、编译后的代码

    4. 程序计数器:是当前线程所执行的字节码的行号指示器。多线程情况下,线程通过获得CPU时间片来执行任务,当线程时间片用完发生中断时,线程在字节码执行的位置会被记录,等待下一次获得时间片时,再从这个位置继续执行。

  1. 哪些是线程共享的,哪些是线程私有的
    1. 共享:堆、方法区。因为这两个区域存放着对象、常量和静态遍历
    2. 私有:程序计数器、栈
  1. 哪些区域可能发生OOM:堆、栈、方法区
      1. 老年代存在大对象。比如大的数组
      2. 存在未被回收的对象,内存泄漏
      3. 解决:可通过jmap查看堆内存使用情况,如果是存在大对象,可以通过调整-Xmx和-Xms调整堆内存大小
    1. 栈:大量创建线程
      解决:-Xss调整每个线程的大小

    2. 方法区

      1. 常量池存放了大量的String
      2. 创建了大量的类,存放了大量的类信息
      3. 解决:-XX:MetaSpaceSize调整元空间大小
  1. java引用类型
    1. 强引用:gc时不会被回收,new创建的对象是强引用
    2. 软引用:发生内存溢出时被回收
    3. 弱引用:下一次gc时被回收
    4. 虚引用:随时会被回收
  1. Gc标记方法
    1. 引用计数法:每个对象都有一个引用计数器,有引用计数+1,释放引用计数-1,0表示对象可以被回收

      1. 优点:实时计算
      2. 缺点:实时计数,开销大
      3. 存在循环引用问题,如果两个对象相互引用,它们的计数都不为0,都无法被回收
    2. 可达性分析:从GCroots开始往下,当一个对象在Gcroots中没有任何一条引用链与之相连,表名该对象可以被回收

      1. 可作为gcroots的对象:java虚拟机栈的引用对象、本地方法栈的引用对象、方法区的静态变量、方法区的常量
  1. GC算法
    1. 标记清除法

      1. 过程:先对需要回收的对象进行标记,标记完成后同一清除
      2. 问题:效率低,清除过程会产生空间碎片
    2. 标记复制法

      1. 过程:先将内存分为大小相等的两块空间,先使用其中一块,当这一块空间满了之后,对这块空间需要回收的对象进行标记、回收,将存活的对象移动到另一块空间
      2. 问题:运行高效,但空间浪费
    3. 标记整理法

      1. 过程:先对需要存活的对象进行标记,标记完成后将这些对象移到内存空间的一边,回收这块空间以外的区域
      2. 优点:不会产生空间碎片
      3. 缺点:用在老年代,需要移动的对象大,效率低
    4. 分代收集法

      1. 新生代因为存活的对象少,使用标记复制法
      2. 老年代因为有大对象和长期存活的对象,使用标记清除法或者标记整理法
  1. GC收集器
    1. CMS:并发标记清除

      1. 过程:初始标记(STW)、并发标记(STW)、重新标记、并发清除
      2. 初始标记和并发标记阶段会STW
      3. 优点:并发标记、清除,效率高,停顿短
      4. 缺点:基于标记清除法,会产生空间碎片
    2. G1:并行并发

      1. 过程:初始标记(STW)、并发标记(可达性分析)、最终标记(STW)、筛选回收
      2. 初始标记和最终标记会STW
      3. 优点:基于标记整理,不会产生空间碎片
  1. 为什么会发生STW
    1. 在进行gc时,需要移动对象(比如标记复制和标记整理法,需要将对象在空间进行移动,会导致对象引用发生更新。为了保证引用更新的正确性,在进行gc时要先暂停其他线程
    2. 在进行gc时,如果一边产生回收对象一边进行标记,效率低
  1. JVM调优参数
    1. -Xms:堆大小
    2. -Xmx:最大堆大小
    3. -Xmn:新生代大小
    4. -Xss:线程大小
    5. -XX:MetaSpaceSize:元空间大小
  1. JVM内存分配原则和空间担保机制
    1. 内存分配原则:
      1. 新对象优先进入新生代
      2. 大对象直接进入老年代
      3. 新生代中经历多次gc的对象进入老年代
    2. 空间担保机制:在新生代进行mirrorGc之前,老年代先检测是否有足够的空间存放新生代的所有对象,如果不够,老年代先进行FullGc
  1. 类加载过程:加载、连接、初始化
    1. 加载:将字节码文件加载到jvm,将类的静态变量、静态方法、
      常量和编译后的代码存入方法区
    2. 连接:
      1. 验证:检查字节码文件符合Java的虚拟机规范,确保加载后不会发生错误
      2. 准备:在方法区中的静态变量分配内存空间并设置初值为0
      3. 解析:将常量的符号引用为直接引用
    3. 初始化:为静态变量赋值
  1. 类加载器
    1. bootstrapClassLoader:启动类加载器,加载jre的rt.jar
    2. extensionClassLoader:拓展类加载器,加载lib的ext文件
    3. applicationClassLoader:应用类加载器,加载环境变量classpath和java.class.path的类
  1. 双亲委派机制
    1. 流程:当某个类加载器接到加载任务时,先检查该类是否已经被加载,如果是,返回class对象,否则将加载任务交给父类加载器进行加载,当父类加载器无法加载类时,才会由子类加载器进行加载
    2. 优点:避免类重复加载
    3. 破坏双亲委派:重写classloader类的loadclass()方法

https://www.xamrdz.com/backend/3vs1995274.html

相关文章: