1. 多线程
进程是程序的一次执行过程,是系统运行的基本单位。
线程是比进程更小的执行单位,一个进程可以包含多个线程
并行:单位时间多个处理器同时处理任务
并发:单个处理器处理多个任务,按照时间片轮流处理
即使是单核的处理器也支持多线程,处理器会给每个线程分配CPU时间片,线程根据拿到的时间片去执行任务,因此多线程会经常进行线程切换,当切换到下一个线程时,当前线程会保存当前任务的执行状态,等待再次拿到时间片再继续执行任务
-
多线程的优缺点
- 优点:当一个线程进入阻塞或者等待状态时,CPU可以先去执行其他线程,提高了CPU的效率
- 缺点:
- 频繁进行线程切换,上下文切换,影响执行速度
- 死锁问题
-
死锁如何产生,如何避免
- 产生:两个或两个以上的线程互相竞争对方的资源,而且同时不释放自己持有的资源时,发生死锁,导致所有线程同时阻塞
- 条件:
- 互斥条件:一个资源在同一时刻只能由一个线程持有
- 请求与保持状态:一个线程在请求获取资源时发生阻塞,同时它持有的资源不释放
- 循环等待条件:发生死锁时,所有线程一致阻塞
- 不剥夺条件:线程已获得的资源在未使用完时不能被其他线程剥夺,只能由自己使用完后释放
- 避免:破环死锁产生的条件
- 互斥条件无法被破坏,因为锁的作用就是使线程互斥
- 破坏请求与保持条件:一次性请求所有的资源
- 破坏循环等待条件:按顺序来申请资源
- 破坏不剥夺条件:在申请不到资源时,释放自己持有的资源
-
线程的生命周期
- 初始:线程被创建,还没有调用start()
- 可运行:调用start()后,等待cpu调度
- 运行:执行run()方法,就绪和运行两种状态
- 阻塞:一般是被动的,在竞争资源时得不到资源,被动挂起在内存,等待资源被释放将其唤醒。
- 等待:进入该状态的线程需要等待其他线程的动作(通知或中断)
- 超时等待
- 终止:执行完毕
-
线程中断:线程在运行过程中被其他线程打断
- interrupt():给目标线程发送中断信号
- interrupted():判断目标线程是否被中断
-
创建线程的方式:实现接口比较好,开销小
- 继承Thread类,重写run()方法,创建子类实例对象,调用start()方法
- 实现Runnable接口,重写run()方法,创建接口实现类的实例对象,将实例对象作为参数创建Thread对象,thread对象调用start()方法
- 实现Callable接口
- 创建Callable接口的实现类,重写call()方法
- 使用实现类的实例化对象,创建FutureTask对象
- 使用FutureTask对象作为参数创建Thread对象
- 调用start()方法
- 线程池
-
Runnable接口和Callable接口
- 相同点:都是接口,都需要调用Thread.start()启动线程
- 不同点:Callable是call()方法,有返回值。Runnable是run()方法,没有返回值
-
start()方法和run()方法
- run()方法定义执行的逻辑,start()方法启动线程
- run()方法可以多次被执行,start()方法只能调用一次
-
线程的其他方法:wait()、sleep()、notify()、notifyAll()、join()、yield()
- wait():使一个线程处于阻塞状态,并释放所持有的对象的锁
- sleep():使当前线程进入指定的毫秒数的休眠,暂停执行,需要处理InterruptedException
- notify():唤醒一个处于等待状态的线程,不能确切唤醒某一个,由JVM决定唤醒,与优先级无关
- notifyAll():唤醒所有处于等待状态的线程,让它们竞争,获得锁的线程进入就绪状态
- join():在一个线程中调用另一个线程的join()方法,会使当前线程挂起,知道执行join()方法的线程结束。比如B线程中调用A线程的join()方法,B线程进入阻塞状态,知道A线程结束
- yield():提醒调度器,线程愿意放弃当前的CPU资源,使线程从运行状态切换到就绪状态
-
sleep()方法和yield()方法
- sleep()方法会使当前线程暂停指定的时间,没有消耗cpu时间片
- sleep()方法使线程进入阻塞状态,yield()对cpu进行提示,使得上下文进行切换,线程进入就绪状态
- sleep()一定会完成指定的休眠时间,yield()不一定
- sleep()需要抛出InterruptException
-
sleep()方法和wait()方法
- sleep()是Thread类的方法,wait()是Object类的方法
- 都能使线程进入阻塞状态
-
线程通信方式
- volatile:读时要求线程去主内存读取最新的变量,写时将工作内存修改后的变量刷新到主内存
- synchronized:读时加锁实现只有一个线程去主内存读取变量,写时释放锁后将修改的变量刷新到主内存
- wait()和notify():线程A调用wait()进入等待,线程B调用notify()将线程A唤醒
- join():线程A调用join()阻塞线程B,线程B要等待线程A执行完才能继续执行
-
为什么wait()、notify()、notifyAll()是在Object类中
- 因为只有同一个锁上等待的线程才能被notify()和notifyAll()唤醒,而锁可以是任何对象,因此都属于Object类可以保证wait()线程一定能被notify()唤醒
-
在Java中如何保证线程安全:原子性、可见性、有序性。volatile不能保证原子性,所以volatile不是线程安全的
原子性:一个操作要么全部执行成功,要么执行失败。线程切换导致的原子性问题
内存可见性:一个线程对共享变量的修改,另一个线程能立刻看到。缓存导致的可见性问题
指令有序性:指代码按照先后书写的顺序执行,但是实际编译时,可能会出现指令重排现象,编译结果跟原先代码的书写顺序不一致
-
如何解决:
- 原子性:atom原子类、lock、synchronized
- 原子类:会以原子方式将当前值加 1,并返回更新后的值。
- lock和synchronized保证只有一个线程对变量进行操作
- 可见性:synchronized、lock、volatile
- volatile读时需要从主内存读取最新的变量,写时将最新修改的变量刷新到主内存
- synchronized读时加锁实现只有一个线程去主内存读取变量,写时释放锁后将修改的变量刷新到主内存
- 有序性:synchronized、lock、volatile
- synchronized和lock保证了只有一个线程去执行代码指令
- volatile通过内存屏障保证有序性
- 原子性:atom原子类、lock、synchronized
happens-before原则:先行发生原则。操作A在操作B之前执行,则操作A中对共享变量的修改要对执行操作B的线程可见,且操作A不会在操作B之后执行。
为什么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就失效了
-
synchronized关键字
synchronized和volatile:synchronized可以保证原子性、可见性、有序性。volatile不能保证原子性
-
实现的是什么锁
- 悲观锁:每次访问共享资源都会上锁
- 非公平锁:线程获取锁的顺序不一定是按照线程阻塞的顺序
- 可重入锁:已经获取过锁的线程可再次获取锁
- 排他锁:锁只能被一个线程持有,其他线程被阻塞
-
使用方式
- 修饰普通同步方法:作用在方法所属的类实例对象,new两个实例对象,无法实现锁
- 修饰静态方法:作用在方法所属的类,new两个类实例对象,能实现锁
- 修饰代码块:锁粒度小
-
底层原理:
- 其底层有一个monitor,当有多个线程进来获取锁时,先通过CAS方式将monitor的owner变更为当前线程,成功则count+1。当线程执行完同步代码块后,释放锁,count-1,当前count为0时,表示锁可以被获取
-
锁升级
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
-
volatile关键字
volatile是一个轻量级的synchronized,一般作用于变量
特性:内存可见性、指令有序性、不能保证原子性
如何实现内存可见性:
读:要求线程去主内存读取最新的变量
写:将最新修改的变量刷新到主内存-
如何实现指令有序性:插入内存屏障
- 写操作前:确保之前的写都已经刷新到主内存
- 写操作后:禁止与后面的volatile操作重排
- 读操作前:禁止与后面的读重排
- 读操作后:禁止与后面的写重排
-
volatile与synchronized的区别
- 作用域:volatile作用于变量,synchronized作用于方法和代码块
- 线程安全:volatile不是线程安全(不能保证原子性),synchronized是线程安全
- 线程阻塞:volatile不会阻塞线程,synchronized会阻塞线程
-
CAS:compareAndSwap比较并替换
- CAS是一种无锁机制,有三个操作数:内存值、预期值、新值。当且仅当内存值等于预期值时,才会将内存值替换为新值,否则进行自旋。也就是说当多个线程共同操作一个共享变量时,只有一个线程可以对变量进行更新,其他线程会操作失败,然后进行再次尝试,这就是自旋
- CAS的自旋操作:是CPU级别的操作,原子性操作,速度快,但是CPU占用高,因此可以设置一个自旋上限
- ABA问题:指的是预期值本来是A,后来变成了B,最后又变成A,此时线程进来更新,发现预期值是A,就把新值更新了
- 解决ABA问题:原子类的AtomicStampedReference,在变量前加一个版本号,这样就变成了1A2B3A
- 用途:synchronized的锁升级过程用到CAS,CouncurrentHashMap的put方法用到了CAS,原子类的自增方法用到CAS
- Unsafe类调用cas
AQS:抽象队列同步器,使用一个volatile的int类型的成员变量state来同步状态,通过CAS修改同步状态的值,当线程尝试获取锁时,如果此时state为0,则线程可以获取锁,state修改为1。其他线程就要等待state变为0才能获取
-
ThreadLocal
底层原理:每个线程都有一个ThreadLocalMap,key为ThreadLocal,value为object对象
-
java引用类型
- 强引用:gc时不会被回收
- 软引用:发生内存溢出时被回收
- 弱引用:下一次gc时被回收
- 虚引用:随时会被回收
-
内存泄漏问题
- 因为作为key的threadlocal是弱引用,而value值object对象是强引用,key是弱引用在下一次gc时被回收了,而value是强引用不会被gc回收,但是key没了,找不到value,value就永远不会被回收
解决内存泄漏:在调用set()、get()、remove()这些方法时,会清理掉key为null的记录,所以使用完之后调用remove()
- Reentrantlock和synchronized
- 可重入:synchronized和reentrantlock都是可重入的
- synchronized通过AQS实现
- reentrantlock在每次获得锁的时候,检查当前维护的线程id和当前正在请求的线程id是否一致,如果一致,计数器+1,表示锁被当前线程获取了多次
- 实现方式
- synchronized是通过JVM
- reentrantlock是通过jdk
- synchronized只能实现非公平锁
- synchronized只能通过随机唤醒wait线程,reentrantlock可以绑定多个condition实现精确唤醒
- 如果不是需要使用ReentrantLock的高级功能,优先使用synchronized,因为synchronized是基于JVM实现的,不需要手动释放锁,而ReentrantLock要手动释放锁,否则会造成死锁
- 可重入:synchronized和reentrantlock都是可重入的
- 公平锁和非公平锁
- new ReentrantLock();默认是非公平锁,可以通过new ReentrantLock(boolean fair)指定创建公平锁
- 公平锁指的是只有阻塞队列头部的线程才能获取锁,而非公平锁只要线程通过AQS的CAS成功获取锁就能
乐观锁:CAS,不加锁执行,进行重试,直到成功
悲观锁:synchronized和reentrantlock,先加锁再操作
- 线程池
优点:线程可复用,减少重复创建线程和销毁线程
缺点:大量创建线程会导致OOM
-
创建线程池方式:
- Executor工厂:
- newSingleThreadExecutor():创建单线程的线程池
- newFixedThreadPool():创建固定数量的线程池
- newCachedThreadPool():创建可缓存的线程池
- newScheduleThreadPool():创建固定数量的定时线程池的线程池
- new ThreadPoolExecutor()
- Executor工厂:
-
重要参数
- corePoolSize:核心线程数,定义了最小可同时工作的线程数量
- maximumPoolSize:线程池中允许存在的最大工作线程数量
- workQueue:存放任务的阻塞队列,新来的任务会先判断当前运行的线程数是否已达到核心线程数,如果达到,任务会先放在阻塞队列
- keepAliveTime:当线程池中数量大于核心线程数时,如果没有新的任务提交,核心线程数量外的线程等待keepAliveTime后销毁
- unit:keepAliveTime的时间单位
- threadFactory:创建新线程的线程工厂
- handler:线程池任务数量超过maximumPoolSize的拒绝策略
-
拒绝策略:
- abortPolicy:抛出异常拒绝任务
- callerRunPolicy:由提交该任务的线程处理
- discardPolicy:丢弃新任务
- discardOldestPolicy:丢弃最早的未处理任务
-
使用Executor创建线程池有哪些坑
- CachedThreadPool()和ScheduledThreadPool()的maximumPoolSize是Integer.MAX_VALUE,可能创建大量线程导致OOM
- FixedThreadPool()和SingleThreadPool()的workQueue的长度为Integer.MAX_VALUE,可能堆积大量的等待任务导致OOM
execute():提交没有返回值的任务
submit():提交有返回值的任务
2. JVM
- JVM内存结构:程序计数器、虚拟机栈、本地方法栈、堆、方法区
-
堆:是JVM中最大的一块内存空间,存放new创建的对象,gc回收主要回收的就是堆中的对象,堆又分为新生代和老年代
- 新生代:存放新创建的对象和短期存活的对象。新生代分为eden区、fromSurvivor区、toSurvivor区。对象优先分配到eden区,当eden区空间满了之后,执行MirrorGc,回收新生代的空间,如果对象经过多次gc(默认15次)后依旧存活,则移动到老年代空间
- 老年代:存放长期存活的对象和大对象,如果新创建的对象是大对象,直接分配到老年代。当老年代空间满了之后,会执行FullGc,回收堆空间,如果FullGc后空间依旧不足,抛出OOM
-
栈:java虚拟机栈和本地方法栈
- java虚拟机栈:执行java方法,期间会创建栈帧,存放局部变量表,局部变量表上存放着基本数据类型和引用数据类型的引用指针。
- 本地方法栈:执行System方法,
方法区:存放被加载的类信息、常量、静态变量、编译后的代码
程序计数器:是当前线程所执行的字节码的行号指示器。多线程情况下,线程通过获得CPU时间片来执行任务,当线程时间片用完发生中断时,线程在字节码执行的位置会被记录,等待下一次获得时间片时,再从这个位置继续执行。
-
- 哪些是线程共享的,哪些是线程私有的
- 共享:堆、方法区。因为这两个区域存放着对象、常量和静态遍历
- 私有:程序计数器、栈
- 哪些区域可能发生OOM:堆、栈、方法区
-
堆
- 老年代存在大对象。比如大的数组
- 存在未被回收的对象,内存泄漏
- 解决:可通过jmap查看堆内存使用情况,如果是存在大对象,可以通过调整-Xmx和-Xms调整堆内存大小
栈:大量创建线程
解决:-Xss调整每个线程的大小-
方法区
- 常量池存放了大量的String
- 创建了大量的类,存放了大量的类信息
- 解决:-XX:MetaSpaceSize调整元空间大小
-
- java引用类型
- 强引用:gc时不会被回收,new创建的对象是强引用
- 软引用:发生内存溢出时被回收
- 弱引用:下一次gc时被回收
- 虚引用:随时会被回收
- Gc标记方法
-
引用计数法:每个对象都有一个引用计数器,有引用计数+1,释放引用计数-1,0表示对象可以被回收
- 优点:实时计算
- 缺点:实时计数,开销大
- 存在循环引用问题,如果两个对象相互引用,它们的计数都不为0,都无法被回收
-
可达性分析:从GCroots开始往下,当一个对象在Gcroots中没有任何一条引用链与之相连,表名该对象可以被回收
- 可作为gcroots的对象:java虚拟机栈的引用对象、本地方法栈的引用对象、方法区的静态变量、方法区的常量
-
- GC算法
-
标记清除法
- 过程:先对需要回收的对象进行标记,标记完成后同一清除
- 问题:效率低,清除过程会产生空间碎片
-
标记复制法
- 过程:先将内存分为大小相等的两块空间,先使用其中一块,当这一块空间满了之后,对这块空间需要回收的对象进行标记、回收,将存活的对象移动到另一块空间
- 问题:运行高效,但空间浪费
-
标记整理法
- 过程:先对需要存活的对象进行标记,标记完成后将这些对象移到内存空间的一边,回收这块空间以外的区域
- 优点:不会产生空间碎片
- 缺点:用在老年代,需要移动的对象大,效率低
-
分代收集法
- 新生代因为存活的对象少,使用标记复制法
- 老年代因为有大对象和长期存活的对象,使用标记清除法或者标记整理法
-
- GC收集器
-
CMS:并发标记清除
- 过程:初始标记(STW)、并发标记(STW)、重新标记、并发清除
- 初始标记和并发标记阶段会STW
- 优点:并发标记、清除,效率高,停顿短
- 缺点:基于标记清除法,会产生空间碎片
-
G1:并行并发
- 过程:初始标记(STW)、并发标记(可达性分析)、最终标记(STW)、筛选回收
- 初始标记和最终标记会STW
- 优点:基于标记整理,不会产生空间碎片
-
- 为什么会发生STW
- 在进行gc时,需要移动对象(比如标记复制和标记整理法,需要将对象在空间进行移动,会导致对象引用发生更新。为了保证引用更新的正确性,在进行gc时要先暂停其他线程
- 在进行gc时,如果一边产生回收对象一边进行标记,效率低
- JVM调优参数
- -Xms:堆大小
- -Xmx:最大堆大小
- -Xmn:新生代大小
- -Xss:线程大小
- -XX:MetaSpaceSize:元空间大小
- JVM内存分配原则和空间担保机制
- 内存分配原则:
- 新对象优先进入新生代
- 大对象直接进入老年代
- 新生代中经历多次gc的对象进入老年代
- 空间担保机制:在新生代进行mirrorGc之前,老年代先检测是否有足够的空间存放新生代的所有对象,如果不够,老年代先进行FullGc
- 内存分配原则:
- 类加载过程:加载、连接、初始化
- 加载:将字节码文件加载到jvm,将类的静态变量、静态方法、
常量和编译后的代码存入方法区 - 连接:
- 验证:检查字节码文件符合Java的虚拟机规范,确保加载后不会发生错误
- 准备:在方法区中的静态变量分配内存空间并设置初值为0
- 解析:将常量的符号引用为直接引用
- 初始化:为静态变量赋值
- 加载:将字节码文件加载到jvm,将类的静态变量、静态方法、
- 类加载器
- bootstrapClassLoader:启动类加载器,加载jre的rt.jar
- extensionClassLoader:拓展类加载器,加载lib的ext文件
- applicationClassLoader:应用类加载器,加载环境变量classpath和java.class.path的类
- 双亲委派机制
- 流程:当某个类加载器接到加载任务时,先检查该类是否已经被加载,如果是,返回class对象,否则将加载任务交给父类加载器进行加载,当父类加载器无法加载类时,才会由子类加载器进行加载
- 优点:避免类重复加载
- 破坏双亲委派:重写classloader类的loadclass()方法