开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情
简历第二条:熟悉Java并发编程,对Java的各种锁机制、线程池机制、AQS等有一定的理解
java并发
问题: 什么是进程和线程
进程是程序的一次执行过程,是系统运行程序的基本单位,使用进程是动态的。系统运行一个程序实际就是一个进程从创建,运行到消亡的过程。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。
问题1:为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器的作用:主要是保证切换线程后能恢复到准确位置。例如:一个线程运行一段时间后被另外一个线程抢占了,当另外一个线程运行完成后,怎么回到之前线程状态?如果我们计数器没有私有化,那么后一个线程使用了计数器后,就无法回到之前状态了。
虚拟机栈:用于存储局部变量的表,从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈:也是存储局部变量
本地方法栈虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
所以:为了柏芝线程中的局部变量不被其他线程访问到,虚拟机栈和本地方法栈私有。
堆和方法区:所有线程共享的资源,其中堆是进程最大内存,主要用于存放新创建的对象,方法区主要用于存储已经加载的类信息,常量,静态变量等。
问题2:并发和并行区别
- 并发:两个及两个以上的作业在同一 时间段 内执行。
- 并行:两个及两个以上的作业在同一 时刻 执行。 最关键的点是:是否是 同时 执行。
问题3:为什么会使用多线程?
- 线程是轻量级进程,是程序最小执行单位,线程之间切换成本远远小于进程,多核cpu出现,意味着多个吸纳从可以同时运行,减少了线程上下文的切换开销。
- 现在是大数据时代,数据量动不动就是上千万,多线程作为并发编程的基础,可以大大提高系统整体的并发能力和性能。
问题4:线程生命周期和状态
java线程在运行时候有以下几种生命周期。
- new 初始状,线程被创建但是没有钓鱼start方法
- runnable 运行状态,线程调用了start
- blocked 阻塞状态,需要等待锁释放
- waiting 等待
- TIME_WAITING 超时等待
- TERMINATED:终止状态
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
问题5:死锁的必要条件
-
互斥条件:该资源任意一个时刻只由一个线程占用。
-
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
-
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
-
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
预防死锁
- 破坏请求和保存条件:一次把所有需要资源申请了
- 破坏不可剥夺条件:占用一不规范资源进一步申请其他资源时候,如果申请不到,就把占用资源释放
- 破坏循环等待条件:按照某熟悉申请资源
避免死锁:银行家算法 在进程提出资源申请时,先预判此次分配是否会导致操作系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待
问题6:使用线程池好处
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池创建:1.通过构造方法实现 2.通过框架实现
AQS原理
如果当前请求的资源是空闲的,那么就直接使用,并且把共享资源设置位锁定状态,如果资源被占用,就把当前线程阻塞,就需要一套阻塞等待以及幻想锁分配机制。AQS这个机制主要通过CLH队列锁(虚拟双向队列)实现。 AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)
JVM
类的加载是把类的.class文件读入内存,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构
类的加载最终是位于堆里面的Class对象,Class对象封装了类在方法区内的数据结构,程序员提供了访问方法区内的数据结构的接口。
类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
- 加载:把外部二进制文件存储在方法区,并且在java堆里面创建一个对象
- 连接:确保被加载的类的正确性(检测文件格式、元数据、字节码、符合引用等)
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把类中符号引用转换为直接引用
- 初始化:为类变量赋予正确初始值,JVM负责对类进行初始化,主要对类变量进行初始化
详情参考:关于Jvm知识看这一篇就够了 - 知乎 (zhihu.com)
jvm垃圾回收:
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则。
而分代收集理论,建立在如下三个分代假说之上,即弱分代假说、强分代假说、跨代引用假说。
依据分代假说理论,垃圾回收可以分为如下几类:
-
- 新生代收集:目标为新生代的垃圾收集。
-
- 老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。
-
- 混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
-
- 整堆收集:目标为整个堆和方法区的垃圾收集。
Java中的锁
Java中加锁有两种方式,分别是synchronized关键字和Lock接口,而Lock接口的经典实现是ReentrantLock。
另外还有ReadWriteLock接口,它的内部设计了两把锁分别用于读写,这两把锁都是Lock类型,它的经典实现是ReentrantReadWriteLock。其中,synchronized的实现依赖于对象头,Lock接口的实现则依赖于AQS。
synchronized的底层是采用Java对象头来存储锁信息的,对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。
AQS是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。
AQS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。