1. 简述一下 Java 面向对象的基本特点,封装、继承、多态
封装:隐藏对象的属性和实现细节,仅对外公开访问接口,控制在程序中对属性的读和修改访问级别,增强安全性和简化编码,使用者不需要关注内部实现细节,只是要通过接口,一定的访问权限来使用类的成员;基本的属性的封装就是私有化成员属性,提供对应属性的 getter/setter 方法
继承:使用已存在的类构建新类的一种方式,通过 extends 关键字来实行,新类可以增加新的属性和新的功能,还能使用父类的功能,但是不是选择性的集成父类,因为Java中是单继承的。子类拥有父类非 private 的属性和方法;子类也可以拥有自己的属性和方法,即对父类的扩展;子类可以用自己的方式实现父类的方法,即对父类方法进行重写(构造器--由父及子,protected访问修饰符,向上转型),但是继承是一种强耦合关系,破坏了封装性
多态:相同的事物,调用相同的方法,参数也一样,但是表现方式却不一样。多态性的满足需要满足三个条件:继承、重写、父类引用指向子类对象
2. Java 中重写和重载的区别
重写:重写是子类对父类可访问的方法的实现过程进行重新编写,方法的名称、返回值、参数列表都不变,重写方法不能抛出新的检查异常或者比被重写方法更宽泛的异常
重载:在一个类里面,方法名称相同,而参数列表不同(Java中的重载对返回值没有要求),每一个重载的方法都必须有一个独一无二的参数列表
3. 线程的生命周期
当线程被创建并启动以后,它不是一启动就进入执行状态,也不是一直处于执行状态。它是要经过新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocking)和死亡(Dead)5中状态。当线程创建并启动之后,它不是一直占用着 CPU 资源独自运行的,CPU 高速运转在不同的线程之间切换,于是线程的状态也是在运行、阻塞之前切换
新建(new): new 关键字创建一个线程之后,该县城就处于新建状态
就绪(Runnable):当线程调用 start() 后,线程就处于就绪状态,Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
运行(Running):线程运行,正在占用 CPU 运行的一个状态
阻塞(Blocking):线程因为某种原因放弃了 CPU 的使用权,暂时停止运行,直到线程进入就绪状态,再次获得 CPU 的使用权,才能转为运行状态
- 等待阻塞(wait):调用 wati() 方法,jvm 会把线程放入等待队列中
- 同步阻塞(lock):运行的线程在获取同步锁对象时,同步所对象被其他线程占用,JVM 会将线程加入到锁池 lock pool 中
- 其他阻塞(sleep/join):运行的线程调用 sleep() / join() ,jvm 会把该线程置为阻塞状态
死亡(Dead):线程结束就是死亡状态
- 正常结束:run() 或 call() 执行完成,线程正常结束
- 异常结束:线程抛出一个未捕获的异常或者 error
- stop() : 调用 stop() 结束线程通常容易导致死锁,不推荐使用
wait() -- 使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep() -- 使一个正在运行的线程处于睡眠状态,是一个静态方法,调用该方法要处理 IngerruptedExceptiong 异常
notify() -- 唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
notifyAll() -- 唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有的线程,而是让他们竞争,只有获得锁的线程才能进入就绪状态
yield() -- 使当前线程从运行状态变为可执行状态
其中 wait()、notify()、notifyAll() 位于 Object 中,且在同步方法、同步代码快中被调用;sleep()、yield() 为 Thread 类中的方法,在当前运行的线程上调用,所以是静态方法;sleep() 不考虑线程的优先级,yield() 只会给相同优先级或更高优先级的线程以运行的机会,sleep() 调用后转入阻塞状态,yield() 转入就绪状态;sleep() 跑出中断异常,yield() 没有声明任何异常;sleep() 比 yield() 具有更好的移植性,通常不建议使用 yield() 方法来控制并发线程的执行
3. 线程池
Executors 工具类:
newFixedThreadPool(int num) -- 创建固定数量 num 线程数的线程池,核心线程数和最大线程数据相同,且使用 LinkedBlockingQueue 无界队列来作为阻塞队列,当提交任务过多,消费不及时,最终会发生 OOM
newCacheThreadPool() -- 创建一个可缓存的线程池,线程池的最大数量为 Integer.MAX_VALUE,使用 SynchronousQueue 作为阻塞队列,默认缓存 60s,但空闲线程时间超过空闲线程最大存活时间,会自动释放现场资源,但是最大线程数是 Integer.MAX_VALUE,则当提交任务且没有空闲线程时,会无限制的创建线程,加大系统开销,影响系统性能,所以使用该线程池时,一定要控制好并发任务数
newSingleThreadExecutor() -- 创建单线程的线程池,阻塞队列使用的是 LinkedBlockingQueue 无界队列,会无限制的往里面添加任务,直到内存溢出
ThreadPoolExecutor : 有 7 个参数:
- corePoolSize -- 核心线程数量
- maximumPoolSize -- 最大容纳线程数,当活动线程(核心线程+非核心线程)达到这个数量后,会依据后面的拒绝策略来处理
- keepAliveTime -- 空闲线程最大存活时间,非核心空闲线程空闲时间超过该时长会被回收掉,当设置了 allowCoreThreadTimeOut 为 true 时,同样也会作用于核心线程
- timeUnit -- 时间单位
- blickQueue -- 阻塞队列,通过线程池提交的任务,若线程数量小于核心线程数,新建线程处理;若线程数量大于等于核心线程数,则将任务加入到阻塞队列中,只要有空闲线程就从队列中获取任务执行
- threadFactory -- 线程工厂,为线程池提供创建新线程的功能
- rejectedExecutionHandler -- 拒绝策略,但线程数量超过最大容量切阻塞队列中任务已经添加满了,就会采用这里的策略进行处理,有四种拒绝策略
- abortPolicy -- 抛出 rejectedExecutionException 异常(默认)
- discardPolicy -- 丢弃不能添加的新任务,不抛异常
- callerRunsPolicy -- 重试添加当前任务,自动重复条用 execute()
- discardOldestPolicy -- 抛弃阻塞队列头部的任务,即等待最久的任务,并执行新传入的任务
线程池的优点:
降低资源消耗:重用存活线程,减少线程创建和销毁的资源消耗
提高响应速度,有效的控制并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免阻塞,当任务到达时,任务可以不需要等到线程创建就能立即执行;
提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的问题性,使用线程池可以进行统一分配、调优和监控
线程池的状态:running、shutdown、stop、tidying、terminated
4. Java 线程模型
进程、线程:在现代操作系统中,线程是处理器调度和分配的基本单位,进程则作为资源分配的基本单位。线程是进程内部的一个执行单元。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是有系统自动创建的。用户需要根据需要在应用程序中创建其他线程,多个线程并发地运行与同一个进程中。所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源,即:
1)当进程只有一个线程时,可以认为进程就是线程;
2)当进程拥有多个线程时,这些线程共享虚拟内存、全局变量等资源。这些资源在上下文切换时是不需要修改的
3)线程也拥有自己的私有数据,如栈和寄存器等,这些在上下文切换时是需要保存的
三种多线程模型:
使用用户线程实现(多对一模型):多个用户线程映射到一个内核线程,用户线程建立在用户空间的线程库上,用户线程的建立、同步、销毁和调度完全在用户态中完成,对内核透明
优点:1)线程的上下文切换都发生在用户空间,避免了模态切换,减少了性能开销;2)用户线程的创建不受内核资源的限制,可以支持更大规模的线程数量 ;
缺点:1)所有的线程都基于一个内核调度,意味着只有一个处理器可以被利用,浪费了其他处理器资源,不支持并行,在多处理器环境下这是不被接受的,如果线程因为IO操作陷入了内核态,内核态线程阻塞等待IO数据,则所有的线程都将会被阻塞;2)增加了复杂度,所有的线程操作都需要用户程序自己处理,而且在用户空间要想自己实现”阻塞之后把线程映射到其他处理器上“异常困难
使用内核线程实现(一对一模型):使用内核线程实现的方式被称为一对一实现。内核线程就是直接由操作系统支持的线程,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。其实程序一般不会直接使用内核线程,程序使用的是内核线程的一种高级接口--轻量级进程,轻量级进程就是我们通常意义上所讲的线程,轻量级进程也是属于用户线程。每个用户线程都映射到一个内核线程 ,每个线程都成为一个独立的调度单元,由内核调度器独立调度,一个线程的阻塞不会影响到其他线程,从而保障这个进程继续工作
优点:1)每一个线程都成为一个独立的调度单元,使用内核提供的线程调度功能及处理器映射,可以完成线程的切换,并将线程的任务映射到其他处理器上,充分利用多核处理器的优势,实现并行操作
缺点:1)每创建一个用户级线程都需要创建一个内核级线程与其对应,因此需要消耗一定的内核资源,而内核资源是有限的,所有能创建的线程数量也是有限的;2)模态切换频繁,各种线程操作,都需要进行系统调用,需要频繁在用户态和内核态之前切换,开销大
使用用户线程加轻量级进程混合实现(多对多模型):内核线程和用户线程的数量比为 M:N,这用模型需要内核线程调度器和用户线程调度器相互操作,本质上是多个线程被映射到了多个内核线程
优点:1)用户线程的创建、切换、析构及同步已然发生在用户空间,能创建数量更多的线程,支持更大规模的并发;2)大部分的线程上下文切换都发生在用户空间,减少了模态切换带来的开销;可以使用内核提供的线程调度功能及处理器映射,充分利用多核处理器的优势,实现并行,并降低了这个进程被完全阻塞的风险
jvm 没有限定 Java 线程需要哪种线程模型来实现,jvm 只是封装了底层操作系统的差异,而不同的操作系统可能使用不同的线程模型,所有Java的线程模型,需要针对具体的jvm实现。Hotspot虚拟机中的每一个线程都是直接映射到一个操作系统原生线程,即一对一模型
Java 线程的调度:线程调度指的是系统为线程分配处理使用权的过程,调度方式主要有两种,分别是协同式调用和抢占式调度
协同式线程调度:线程的执行时间有线程本身控制,线程把自己的工作执行完成了之后,要主动通知系统切换到另外一个线程上去
优点:实现简单,切花操作对线程自己是可知的,一般不会有什么线程同步问题
缺点:线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统进程线程切换,那么程序就会一直阻塞在那里
抢占式线程调度:每个线程将由系统分配执行时间,线程的切换不由线程本身决定
优点:可以主动让出执行时间(yield),并且线程的执行时间是系统可控的,也不会有一个线程导致整个系统阻塞的问题;
缺点:无法主动获取执行时间
Java 使用的是抢占式线程调度,虽然这种方式的线程调度是系统自己完成的,但是我们通过设置线程的优先级来让系统选择,在两个线程同时处于 ready 状态时,优先级越高的线程越容易被系统选择执行
Java 线程的生命周期:新建、就绪(start)、阻塞(获得锁,synchronized,lock)、timed_waiting、waiting、terminaled(执行结束)
volatile 和 synchronized 的区别:
volatile 是变量修饰符;synchronized 可以修饰类、方法、变量;
volatile 仅能实现变量修改可见性,防止指令重排序,不能保证原子性;synchronized 可以保证变量的修改可见性和原子性;
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞;
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化;
volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 关键字要好。但是 volatile 关键字只能用于变量; synchronized 关键字可以修饰方法以及代码块,synchronized 在 1.6 之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其他各种优化之后执行效率又了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些
synchronized 锁升级原理:
对象在内存中的布局分为 3 个部分:对象头、类型指针、填充字节,synchronized 的锁信息存储在对象头的 markwork 中,makrwork 中最后两位表示锁的标识为;
thread-id 不存在:无偏向锁,标识位为 01;存储对象 hashCode、分代年龄
thread-id 存在:有偏向锁,标识位为01;偏向线程ID、偏向时间戳、分代年龄
轻量级锁:标识位为 00;markwork中存储指向栈中锁记录指针;
重量级锁:标识位为 10
gc标记:11,为空不记录信息
偏向锁升级:线程访问对象时,会在对象头和线程栈的栈帧中设置 thread-id 来记录当前线程,偏向锁不会自动释放锁;当其他线程获取锁时,比较当前线程的 ID 和对象头中的ID是否一致,如果一致则直接获得锁;如果不一致,查看ID对应的线程是否存活,如果没有存活,所对象重置为无锁状态,其他线程获得偏向锁;如果存活,则看这个ID对应的线程的栈帧信息,判断是否继续使用该锁对象,继续使用,则撤销偏向锁,升级为轻量级锁;不再使用则重置为无锁状态,其他线程获得锁;
轻量级锁:线程获取轻量级锁的时候,会复制一份 markword 的信息到线程的栈帧中,然后使用 cas 将 markword 信息修改为当前线程存储的锁记录信息;当其他线程获取锁时,通过 cas 自旋的形式来获取锁,当然自旋是有次数限制的,不是无限制自旋,当在自旋等待的过程中,又有其他线程来获取锁,轻量级锁就会膨胀为重量级锁,重量级锁把所有的线程都阻塞了,防止 cpu 空转。
synchronized 中的各种类型的锁只能升级,没有降级,除了偏向锁可以重置为无锁状态;偏向锁适合基本没有线程竞争锁的场景;轻量级锁使用于少量线程竞争锁,且线程持有锁的时间不长,追求响应效率;重量级锁适合很多线程竞争锁,且线程持有锁的时间较长,追求吞吐量。
线程和进程区别,什么是线程和进程?
进程:一个在内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程;
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行,一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可以共享数据
区别:进程是操作系统资源分配的基本单位;而线程是 CPU 调度和执行的基本单位
什么是线程死锁?
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状体或系统产生了死锁,这个永远在互相等待的进程(线程)成为死锁进程(线程)。
形成死锁的四个必要条件:
互斥条件:线程(进程)对于所分配到的资源具有排他性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞,对已获得的资源保持不放
不剥夺条件:线程(进程)已获得的资源在未使用完之前不能被其他线程(进程)强制剥夺,只有自己使用完毕后才释放资源;
循环等待条件:当发生死锁时,所等待的线程(进程)必定会 形成一个环路(类似于死循环),造成永久阻塞
创建线程的方式:
继承 Thread 类
实现 Runnable 接口 -- 异步任务,无返回值
实现 Callable 接口 -- 异步任务,产生返回值,Future 用于获取返回值结果
使用 Executors 工具类创建线程池
synchronized 和 lock 有什么区别?
synchronized 是 Java 内置关键字,在 JVM 层面;Lock 是个 Java 类;
synchronized 可以给类、方法、代码块加锁;而 Lock 只能给代码块加锁;
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁,而 lock 需要自己手动加锁和释放锁,如果使用不当没有unlock释放锁,就会造成死锁;
通过 lock 可以知道有没有成功获取锁,而 synchronized 取法办到;
可以使锁更公平。使线程在等待锁的时候响应中断;可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间;可以在不同的范围,以不同的顺序获取和释放锁
synchronized 和 ReentrantLock 区别是什么?
相同点:两者都是可重入锁,自己可以获取自己的内部锁
不同点:synchronized 是 Java 内置关键字,在 jvm 层面;ReentrantLock 是具体的实现类;
ReentrantLock 是具体的实现类,提供了很多便捷的 api,使用起来比较灵活,需要手动的获取锁和释放锁,synchronized 是 jvm 层面实现的,需要手动加锁和释放锁,使用起来比较重;
二者的锁机制不同,ReentrantLock 底层调用的是 UnSafe 的 park() 加锁,而 synchronized 是在对象头的 markword 中;
AQS:AQS核心思想是 --- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制AQS是用CLH虚拟的双端队列来实现的,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个 volatile 修饰的 int 类型的变量 state 来表示同步状态,通过内置的先进先出队列来完成获取资源线程的排队工作,AQS通过CAS对该同步状态进行原子操作,实现对其值的修改
ReentrantReadWriteLock -- 读写锁
公平性选择:支持公平和非公平锁的获取方式,吞吐量还是非公平优于公平
重进入:读锁和写锁都支持线程重进入
锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级为读锁
ThreadLocal:
线程变量副本,ThreadLocalMap -> 弱饮用,remove()
CountDownLatch 和 CycliBarriar 区别:
CountDownLatch 与 CyclicBarrier 都是用于控制并发的工具类,都可以理解成维护的就是一个计数器
CountDownLatch 一般用于某个线程A等待若干个其他线程执行完任务之后,他才执行;而 CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
调用 CountDownLatch 的 countDown() 后,当前线程并不会阻塞,会继续向下执行;而调用 CyclicBarrier 的 await() 会阻塞当前线程,直到 CyclicBarrier 指定的线程全部到达指定点的时候才能继续向下执行;
CountDownLatch 是不能复用的,而 CyclicBarrier 是可以复用的
try cathc finially 代码块:
任何执行 try 或 catch 块中的语句之前,都会执行 finially 代码块,如果try语句里有return,那么代码的行为如下:
1.如果有返回值,就把返回值保存到局部变量中
2.执行jsr指令跳到finally语句里执行
3.执行完finally语句后,返回之前保存在局部变量表里的值
如果try,finally语句里均有return,忽略try的return,而使用finally的return.
![](https://img2020.cnblogs.com/blog/1266094/202110/1266094-20211027092649395-1133393658.png)
final 关键字:
final 关键字可以用于成员变量、本地变量、方法及类;
final 成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会编译报错;
final 修饰的变量不能被再次赋值;
本地变量必须在声明时赋值;
在匿名类中所有变量都必须是 final 变量;
final 方法不能被重写;
final 类不能被继承
没有在声明时初始化 final 变量的称为 final 变量,他们必须在构造器中初始化,或者调用 this() 初始化,不这么做的话,编译器会报错“final 变量需要进行初始化”
final 修饰的变量不会自动改变类型
抽象类和接口:
接口可以说是抽象类的一种特列,接口中的所有方法都必须是抽象的。接口中的方法定义默认为 public abstract 类型,接口中的成员变量类型默认为 public static final。另外,接口和抽象类在方法上有区别:
1. 抽象类可以有构造方法,接口中不能有构造方法;
2. 抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法,但是在 1.8 之后可以有方法的默认实现,通过 default 修饰符修饰
3. 抽象类中可以有普通成员变量,接口中没有普通成员变量
4. 抽象类中的抽象方法的访问类型可以是public、protected 和默认类型,接口中c成员变量只能是 public protected
异常类:
都是 Throwable 的子类:
1. Exception: 是程序本身可以处理的异常;
2. ERROR:是程序无法处理的错误。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,一般不需要程序处理;
3. 检查异常(编译器要求必须处置的异常):除了 ERROR、RuntimeException 及其子类以外,其他的 Exception 类及其子类都属于可查异常,这种异常的特点是 Java 编译器会检查它,也就是说,当程序中可能出现这类异常,要么用 try-catch 语句捕获它,要么用 throws 字句声明抛出它,否则编译不会通过
4. 非检查异常(编译器不要求处置的 异常):包括运行时异常(RuntimeException与其子类)和错误(Error)