并发相关知识,以及对部分内容进行复习
并发面试题
一. 进程和线程
1. 进程是程序运行的最小单位
2. 一个程序的运行就是线程从创建,运行到消亡的过程。
3. 在java中启动main函数就是启动了一个JVM进程,main函数所在的线程(主线程)则是这个进程当中的一个线程
4. 线程是比进程更小的执行单位,一个进程可以有多个线程
5. 线程共享的是堆和方法区,私有的是虚拟机栈、本地方法栈、程序计数器
5.1 虚拟机栈和本地方法栈的区别:本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务
二. 协程
1. 基于线程,但比线程更加轻量级,且可以由程序员自己写程序来进行管理
2. 线程切换由操作系统调度,协程由用户调度,可以减少上下文切换
3. 相同内存中协程的stack更加轻量,可以在内存中开启更多的协程
4. 在同一个线程上,可以避免因竞争关系而使用锁
三. 进程与线程的关系,从jvm和从操作系统两个角度讲有什么不同?
1. 从JVM的角度
(1) 为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器的作用:首先字节码解释器是通过改变程序计数器来依次读取指令,从而实现代码流程控制的。其次在多线程的环境下,程序计数器用于记录当前线程执行的位置
为什么程序计数器不可以共享:因为每个线程执行的位置都不一样,如果共享了程序计数器,那么多线程发生上下文切换之后,可能无法回到正确的位置
虚拟机栈作用:保存方法执行时的信息,例如局部变量表,操作数栈,常量池引用等?
本地方法栈:hotspot虚拟机中两栈合二为一?两者区别在于虚拟机栈为jvm执行java方法服务,本地方法栈则是为native方法服务。(native方法就是java调用非java代码的接口)
为什么两栈不可以共享:需要确保其他线程无法访问本线程的局部变量
(2) 其他
2. 从操作系统的角度
四. 并发和并行
1. 并发和并行的两大关键字是:同时,多任务;
2. 并行的关键是具有同时处理多任务的能力,而并发的关键是具有处理多个任务的能力,但不一定需要同时。
五. 为什么需要多线程
多核时代是为了提高cpu效率,满足
六. 多线程的问题(自己扩展一下)
1. 内存泄漏
(1) 概念:已动态分配的堆内存在使用完毕之后无法释放,导致一直占据该内存单元,直到程序结束。
(2) 主要出现在ThreadLocal中
2. 死锁
死锁是多个线程因竞争资源而造成的僵局(互相持有对方等待的资源)
产生死锁的四个必要条件(有一个不成立,就不会死锁)
(1) 互斥条件:一个资源每次只能被一个进程使用
(2) 不剥夺条件:某资源只能有获得它的进程主动释放,不能被其他进程剥夺
(3) 请求和保持条件:进程持有资源,但又希望获得已经被其他进程占有的资源
(4) 循环等待条件:存在进程资源头尾相接的循环等待链p0-p1-p2-p0
3. 线程不安全
例如:多个线程池程先后更改数据,造成脏数据
七. 线程的生命周期和状态
1. 线程在生命中期中不是处于固定某状态的,而是会在执行期间不停的切换
2. New 初始状态,线程已经被构建,但没有调用start()方法,没开始
3. Runnable 运行状态,java将操作系统中就绪(调用了start方法)和运行笼统称为运行中
4. Blocked 阻塞状态(不消耗系统资源!!),表示线程阻塞于锁
5. Waiting 等待状态,需要等待其他线程特定的行为
6. Time_Waiting 超时等待,它不同于waiting的地方在于可以在指定的时间自行返回
7. Terminated 终止,表示当前线程已经执行完毕
八. 上下文切换
1. 前提:一个cpu核心在任何时刻只能被一个线程使用
2. 目的:让线程有效的执行(先进先出并不高效)
3. 策略:时间片轮转,当一个线程时间片用完之后就会处于就绪状态,而cpu资源就会分配给其他的线程(这时候会完成一次上下文切换)
4. 切换的过程:先在cpu寄存器和程序计数器中保存正在执行的指令位置(保存地址为系统内核),然后加载新任务的上下文并执行新的任务
九. 死锁
1. 概念:两个/多个线程同时持有对方需要的锁,因此发生了阻塞
2. 死锁四大条件:
(1) 互斥:(资源的持有是互斥的)一个资源一次只能被一个线程同时占有
(2) 不可剥夺条件:除非持有资源的线程主动释放,不然其他的线程无法获取这个资源
(3) 请求与保持条件:(因为请求而阻塞,但又保持着原来的资源)一个线程因请求资源而阻塞,但又不释放已经持有的资源
(4) 循环等待:多个线程形成了环状的资源等待链
3. 如何避免死锁
(1) 互斥就没法破坏了,这是天然的性质?
(2) 破坏不剥夺条件:如因请求资源而阻塞,可以主动释放已占有的资源
(3) 破坏请求与保持:一次性申请所有的资源
(4) 破坏循环与等待:申请资源顺序,释放资源反序
十. Sleep和wait
(1) 两者作用是一样的:用于暂停线程的执行
(2) 最主要的区别是sleep()释放了锁,但是wait()没有
(3) Wait()被调用后,需要使用notify()或者notifyAll()唤醒,sleep执行完成后,会自动的苏醒
十一. 为什么不可以直接调用run()方法,而需要调用start()方法
Start()方法会启动线程,并进入就绪状态,当分配到时间片就可以运行,然后start()会自动调用run()方法的内容。
但是如果直接执行run()方法,那它会被当成main线程下的一个普通的方法来执行
十二. Synchronized
1. Synchronized关键字的一些了解
(1) 作用:解决多个线程访问资源的同步性,保证被它修饰的方法、代码块在任意时刻只有一个线程执行
Synchronized在java早起版本中属于重量级锁,原因是它依赖操作系统的锁,java的线程是映射到操作系统原生线程上的,因此如果需要挂起或者唤醒线程,需要操作系统从用户态到内核态之间的切换,成本比较高。
(2)(后来发生了什么改进呢?),在java6后面jvm对synchronized引入了大量的优化
2. Synchronized可以修饰实例方法(给对象实例加锁)、静态方法、以及代码块(给class类加锁)
2.1 同步代码块实现使用的是monitorenter和monitorexit指令;但synchronized修饰的方法并没有这两个指令,取而代之的是acc_synchronized。两者本质都是对monitor的获取
3. Synchronized 关键字的实际应用:双重检验锁实现单例模式
Public class Singleton{
Private volatile static Sigleton uniqueInstance;
Private Singleton(){
}
Public static Singleton getUniqueInstance(){
If(uniqueInstance == null){
Synchronized(Singleton.class){
If(uniqueInstance == null){
uniqueInstance = new Singleton();}
}
Return uniqueInstance;
}
}
4. 什么是构造方法:一种特殊的方法,与类同名,对象的创建就是通过构造方法来完成的,当类实例化一个对象时,就会自动的调用构造方法,构造方法和其他方法一样可以重载
十三. Java6以上的版本对synchronized的优化
1. 前提知识:java中的每个对象都可以作为锁
2. Hotspot虚拟机中对象在内存分为:对象头,示例数据和填充,对象头包含了MarkWord和类型指针,
3. 多线程下synchronized的加锁就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作.
4. Synchronized锁的分类
https://www.cnblogs.com/wuqinglong/p/9945618.html,其实不是很了解
锁可以升级,但是不可降级
(1) 无锁
(2) 偏向锁
(3) 轻量级锁
(4) 重量级锁
5. 偏向锁
针对线程而言,获得锁后就不会有解锁等操作了,目的是节约开销,如果出现竞争,就升级
6. 轻量级锁:有两个线程竞争锁,就膨胀
7. 重量级锁:上面膨胀而来的,如果一个线程看到已经是重量级锁了,就会直接挂起
十四. Synchronized和ReentrantLock的区别
1. 共同点:两者都是可重入锁(自己可以再次获取自己的内部锁)。
2. Synchronized是依赖JVM实现的,R。。。是依赖JDK实现的(API层面)
3. R比s多了一些功能
(1) 等待可中断:正在等待的线程可以选择放弃(排队做核酸,不排了)
(2) R可以指定是公平锁还是非公平锁。公平意味着先等待的线程先获取
(3) 选择性通知:依靠Condition实现
十五. Volatile
1. Cpu缓存的问题:多线程下会出现内存缓存不一致的现象
2. JMM:java内存模型,当前内存模型下,线程可以把变量保存在本地内存,而不是在主存直接读写,这就可能咋成一个线程修改了主存的值,另一个线程还在使用寄存器中变量的拷贝。而解决这个问题的办法简单粗暴,就是使用volatile,它告诉jvm这个变量不稳定,每次都要从主存中读取。
3. 有序性:由于java在编译器中的优化,代码执行的顺序未必就是编写代码的顺序,
4. 综上所述,volatile的作用:防止jvm指令重排,保证变量的可见性
十六. 并发编程三个重要特性
1. 原子性
2. 可见性:一个变量对共享变量进行修改后,另外的线程立即可以看到修改后的最新值
3. 有序性:禁止指令进行重新排序优化
十七. Synchronized和volatile的区别
1. 它们是互补的存在
2. Volatile是轻量级实现,所以性能好于synchronized,但volatile只能用于变量,sync可以用于代码块、方法(v更轻量,性能更好,s应用场景更为广泛)
3. V只能保证可见性,不能保证原子性,s两者都可以保证
4. V解决的是变量在多个线程之间的可见性,s则是多个线程之间访问资源的同步性
十八. ThreadLocal
1. 实现每个线程拥有本地变量(两个人去收集宝物,用一个袋子会有争议,每人一个袋子就没有了),因此在并发模式下变量是安全的
2. 创建一个threadlocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本
3. 原理
(1) ThreadLocalMap可以理解为定制化的HashMap,当前线程调用threadloacl的set、get方法实质上调用的就是ThreadLocalMap中的get,set方法
(2) 最终的变量存放在当前线程的ThreadLocalMap中,而不是存在ThreadLocal上,每个thread都具备一个ThreadLocalMap,它以ThreadLocal为key,Object对象为value
4. 内存泄漏问题
(1) ThreadLocalMap中key是ThreadLocal的弱引用,但是value是强引用,就可能出现在gc的时候key被回收value没有回收,这样一来map中就会出现key为null的entry。
(2) 因为gc没有回收掉value,如果不做任何措施,就会产生内存泄漏,ThreadLocal已经考虑了这种情况,调用set(),get(),remove(),会清理掉key为null的,但是使用完thread之后最好手动调用remove()。
4.1 弱引用就是可有可无的,gc发现弱引用会直接回收内存,但是因为gc优先级很低,所以不一定会很快发现弱引用的对象
十九. 线程池
1. 使用线程池是因为创建和销毁线程都开销较大,以及为并发考虑(增加速度)
2. Runnable和Callable
(1) 最主要的区别是runnable不会返回结果或者抛出异常,但callable会,具体使用哪个得具体分析,因为runnable可以让代码更简洁
(2) Executors可以实现runnable和callable的互相转化
3. Execut()和submit()
(1) execute()用于不需要返回值的任务,无法判断任务是否被线程池执行成功
(2) submit()则用于需要返回值的任务,线程池会返回future类型的对象,通过这个对象判断任务是否成功,也可以通过get()获得返回值,它会阻塞当前线程,直到任务完成
4. 线程池的创建:ThreadPoolExecutor
(1) 好处:明确线程池的运行规则
(2) 实现方式:构造方法和Executors(Executor框架的工具类)
(3) ThreadPoolExecutor 分析
3个最重要的参数
A. corePoolSize:核心线程数,定义了最小可以同时运行的线程数量
B. maximumPoolSize:队列中存放任务达到队列容量时,当前可以同时运行的线程数量变为最大线程数(啥意思)
C. workQueue,新任务来之前先判断,当前运行的线程数量是否达到核心线程数,达到的话,新任务会放在队列
其他常见参数
A. keepAliveTime:线程池的线程数量大于corePoolSize,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是等待超过keepAliveTime再销毁
B. unit:keepAliveTime的事件单位
C. threadFactory:exector创建新线程用的
D. handler:饱和策略
(4) 单独介绍饱和策略
1. 定义:同时运行的线程数量达到最大线程数量且队列满了,就会运用到饱和策略
2. AbortPolicy,通过抛出异常来拒绝新的任务
3. CallerRunsPolicy:在程序可以承受延迟,并要求每个任务都被执行的时候选择。它调用执行自己的线程运行任务,也就是调用execute方法被拒绝的任务
4. discardPolicy:扔掉任务
5. discardOldPloicy:丢弃最早未处理的任务请求。
(5) 先提交任务,然后判断核心线程满了吗,没有就创建,有就把任务放到等待队列,然后如果等待队列也满了,那就看线程池是不是满了,如果没有就创建,满了就按照策略来处理
二十. Atomic原子类
1. 指的是一个操作是不可中断的,这个操作一旦开始就不会被其他线程所干扰
2. 原理:简单分析AtomicInteger类
(1) 利用CAS+volatile和native方法保证原子操作,避免syn的高开销
(2) Cas的原理:拿期望的值和原本值比较,如果相同,则更新为新的值。objectFieldOffset()用来获取原本值,得到valueOffset,value是volatile变量,在内存中可见,因此jvm保证任何时刻任何线程都可以拿到该变量的最新值
二十一. AQS
1. AbstractQueueSynchronizer,在juc的locks包下面
2. 作用是构造锁和同步器的框架
3. 原理:如果被请求的共享资源空闲,则请求资源的线程为有效的工作线程,且锁定共享资源。如果共享资源被占用,就需要一套等待线程阻塞以及锁的分配机制,它由CLH队列锁实现
4. 对资源的共享方式
(1) 定义两种资源共享方式
独占:公平锁(先到先得)、非公平锁(抢)
共享:多个线程可以同时执行
(2) 底层实现:模板方法模式
1. 继承AbstractQueuedSynchronized 并重写指定的方法
2. AQS组合在自定义同步组件的实现中
(3) 组件总结
A. Semahpore 信号量:允许多个线程同时访问
B. CountDownLatch 倒计时器,让某个线程等待直到倒计时结束
C. CycleicBarrier :和上述CountDownLatch类似,但功能更加复杂
(4) CountDownLatch用法
A. 作用是允许线程阻塞在一个地方,应用场景是使用多线程读取多个文件
B. 如果要改进,ComplietableFuture

浙公网安备 33010602011771号