备战秋招冲击大厂Java面试题系列—并发编程
Posted Java-桃子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了备战秋招冲击大厂Java面试题系列—并发编程相关的知识,希望对你有一定的参考价值。
1. 进程和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位,一个程序至少一个进程,一个进程至少一个线程。
进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程:是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
为什么会有线程?
每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。
1)线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来。
2)进程分为单线程进程和多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作。
3)线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化。
进程线程的区别:
1)地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
2)资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
3)一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
4)进程切换时,消耗的资源大,效率低。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
5)执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
6)线程是处理器调度的基本单位,进程是资源分配的最小单位。
7)两者均可并发执行。
- 优缺点:
1)线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
2)进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器迁移。 - 何时使用多进程,何时使用多线程?
1)对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
2)要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
更多Java学习资料、面试真题获得,请【点击此处】
2. 协程
协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
在有大量IO操作业务的情况下,我们采用协程替换线程,可以到达很好的效果,一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。
在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。
协程只有和异步IO结合起来才能发挥出最大的威力。
3. 线程的5种状态
4. sleep、yield、wait、join的区别
- sleep:Thread类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU(Sleep释放CPU,wait 也会释放CPU,因为cpu资源太宝贵了,只有在线程running的时候,才会获取cpu片段),提供其他线程运行的机会且不考虑优先级,但如果有同步锁,则sleep不会释放锁即其他线程无法获得同步锁,可通过调用interrupt()方法来唤醒休眠线程。
- yield:让出CPU调度,Thread类的方法,类似sleep只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会。 yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。调用yield方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用CPU了,没有任何机制保证采纳。
- wait:Object类的方法(notify()、notifyAll() 也是Object对象),必须放在循环体和同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify随机唤醒,notifyAll全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁
- join:一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。注意该方法也需要捕捉异常。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
5. 为何不推荐使用stop()和suspend()方法?
stop()方法不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。
suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用 wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
6. 7种线程同步方法
1)synchronized同步方法
2)synchronized同步代码块
3)LOCK
4)Volatile
5)Threadlocal
6)阻塞队列
7)wait与notify
7. 监视器(Monitor)内部如何线程同步?
监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码。
8. 线程的创建方式
启动线程有如下三种方式:
- 继承Thread类创建线程类
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
package com.thread;
public class FirstThreadTest extends Thread{
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run(){
for(;i<100;i++){
System.out.println(getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i = 0;i< 100;i++){
System.out.println(Thread.currentThread().getName()+" : "+i);
if(i==20) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。
- 通过Runnable接口创建线程类:使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享.
- 适合多个相同程序代码的线程去处理同一个资源(多线程内的数据共享)
- 增加程序健壮性,数据被共享时,仍然可以保持代码和数据的分离和独立
- 避免java特性中的单继承限制
- 更能体现java面向对象的设计特点
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
package com.thread;
public class RunnableThreadTest implements Runnable{
private int i;
public void run(){
for(i = 0;i <100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
public static void main(String[] args) {
for(int i = 0;i < 100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==20){
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt,"新线程1").start();
new Thread(rtt,"新线程2").start();
}
}
}
}
- 通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer>{
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++){
System.out.println(Thread.currentThread().getName()+" 的变量i的值"+i);
if(i==20) {
new Thread(ft,"有返回值的线程").start();
}
}
try{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception{
int i = 0;
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
9. cyclicbarrier和countdownlatch的区别
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待
- CountDownLatch一般用于某个线程A或多个线程,等待若干个其他线程执行完任务之后,它才执行;CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量;调用await()方法的线程会被阻塞,直到计数器减到 0 的时候,才能继续往下执行;调用了await()进行阻塞等待的线程,它们阻塞在Latch门闩/栅栏上;只有当条件满足的时候(countDown() N次,将计数减为0),它们才能同时通过这个栅栏;以此能够实现,让所有的线程站在一个起跑线上。
- CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;另外,CountDownLatch是减计数,计数减为0后不能重用;而CyclicBarrier是加计数,可置0后复用。
10. Java中提供的线程池
Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor
1)newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2)newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3)newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4)newScheduledThreadPool:适用于执行延时或者周期性任务。
11. 自定义线程池(ThreadPoolExector)
new ThreadPoolExecutor(
2,
9,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
七大参数:
1)corePoolSize(常驻核心线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2)maximumPoolSize(线程池最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3)keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4)unit(时间单位)
5)workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
6)threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
- 四种拒绝策略:
1)ThreadPoolExecutor.AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
2)ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
4)ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 - 配置线程池
1)CPU密集型任务:尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
2)IO密集型任务:可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。CPU核心线程数/1-阻塞系数(0.8~0.9)
3)混合型任务:可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
12. 线程池的状态
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
- TIDYING(整理状态):所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。
-
TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
更多Java学习资料、面试真题获得,请【点击此处】
13. 线程池中shutdown()和shutdownNow()方法的区别
shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
14. 线程池中 submit() 和 execute() 方法的区别
- execute():只能执行 Runnable 类型的任务。
- submit():可以执行 Runnable 和 Callable 类型的任务。
- submit()能获取返回值(异步)以及处理Exception。
15. ThreadLocal
https://segmentfault.com/a/1190000037728236?utm_source=tag-newest
ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
- ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:
- Synchronized是通过线程等待,牺牲时间来解决访问冲突
- ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。
- 应用场景
- 数据库连接池的实现:获取connection
- 提升性能和安全,如SimpleDateFormat
- 底层实现
1)ThreadLocal仅仅是个变量访问的入口;
2)每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用;
3)ThreadLocalMap以当前的threadLocal对象为key,以真正的存储对象为value。get()方法时通过threadLocal实例就可以找到绑定在当前线程上的副本对象。 -
每个线程都有一个属于自己的ThreadLocalMap类, 他用于关联多个以ThreadLocal对象为key, 以你的数据为value的Entry对象, 且该对象的key是一个弱引用对象,以防止内存泄漏。
16. Synchronized在静态方法和非静态方法的区别
- Synchronzied 修饰非静态方法==》对象锁(同步代码块,方法锁)
- Synchronzied 修饰静态方法==》类锁,因为是静态方法,它把整个类锁起来了;
17. Synchronized和Lock
- 实现层面不一样。synchronized 是Java关键字,JVM层面实现加锁和释放锁;Lock 是一个接口,在代码层面实现加锁和释放锁
- 是否自动释放锁。synchronized 在线程代码执行完或出现异常时自动释放锁;Lock 不会自动释放锁,需要再 finally{}代码块显式地中释放锁
- 是否一直等待。synchronized 会导致线程拿不到锁一直等待;Lock 可以设置尝试获取锁或者获取锁失败一定时间超时
- 获取锁成功是否可知。synchronized 无法得知是否获取锁成功;Lock 可以通过 tryLock 获得加锁是否成功
- 功能复杂性。synchronized 加锁可重入、不可中断、非公平;Lock 可重入、可判断、可公平和不公平、细分读写锁提高效率
18. Synchronized和Volatile
- synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量。
- synchronized 可以保证线程间的有序性(个人猜测是无法保证线程内的有序性,即线程内的代码可能被 CPU 指令重排序)、原子性和可见性;volatile 只保证了可见性和有序性,禁止指令重排序,无法保证原子性。
- synchronized 线程阻塞,volatile 线程不阻塞。
- volatile 本质是告诉jvm当前变量在寄存器中的值是不安全的需要从内存中读取;sychronized 则是锁定当前变量,只有当前线程可以访问到,该变量其他线程被阻塞。
- volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
19. copyonwrite
- 实现就是写时复制,在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。
- 适用场景:copyonwrite的机制虽然是线程安全的,但是在add操作的时候不停的拷贝是一件很费时的操作,所以使用到这个集合的时候尽量不要出现频繁的添加操作,而且在迭代的时候数据也是不及时的,数据量少还好说,数据太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的。
20. synchronized可重入的实现
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
21. Lock(锁)
1)公平锁:线程申请锁的顺序来获取锁
2)非公平锁:允许加塞
3)自旋锁:循环方式获取锁
4)独占锁:写锁
5)共享锁:读锁
6)互斥锁:读写锁
22. 非公平锁和公平锁在reetrantlock里的实现
对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
23. 死锁
- 定义:两个线程或两个以上线程因争夺资源而出现线程互相等待的现象。
- 原因:
1)循环等待条件:若干资源形成一种头尾相接的循环等待资源关系。
2)互斥条件:一个资源一次只能被一个进程访问。
3)请求保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。
4)不剥夺条件:进程已经获得的资源,在未使用完之前不能强行剥夺,而只能由该资源的占有者进程自行释放。 - 解决方法:
- 银行家算法,操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。
- 一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
- 问题:CPU达到100%(死循环创建对象也会)
- 排查:(Linux)
1)top -c查看CPU资源使用情况,定位进程
2)ps –mp 进程号 –o THREAD,tid,time定位线程
3)printf “%x\\n” 12785将线程ID转为16进制格式(英文小写)
4)jstack 进程号 | grep 线程ID –A60定位具体出错代码行
24. ReentrantLock(可重入锁)和synchorized区别
- ReentrantLock主要利用CAS+AQS队列来实现。ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH同步队列队首的线程会被唤醒,然后CAS再次尝试获取锁。
- 相同点:都可以做到同一线程,同一把锁,可重入代码块。
- 不同点
1)ReentrantLock可以实现公平锁和非公平锁,默认非公平锁。又称递归锁,线程可以进入任何一个他已经拥有的锁同步着的代码块。可以避免死锁。Synchorized为非公平锁。
2)synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
3)Synchorized竞争锁时会一直等待;reentrantLock可以尝试获取锁,并得到获取结果或者超时
4)synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
5)synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
25. CAS(compare and swap)
- 定义:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。
- 缺点:
1)循环时间长
2)只能保证一个共享变量的原子操作
3)ABA问题:(狸猫换太子)A->B->A,解决方法:时间戳原子引用(版本控制)AutomicStampedReference类
26. AQS(AbstractQueuedSynchronizer)
- 定义:是一个用于构建锁和同步容器的框架。它能降低构建锁和同步器的工作量,还可以避免处理多个位置上发生的竞争问题。在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并提高吞吐量。AQS支持独占锁(exclusive)和共享锁(share)两种模式。无论是独占锁还是共享锁,本质上都是对AQS内部的一个变量state的获取。state是一个原子的int变量,用来表示锁状态、资源数等。
- 独占锁:只能被一个线程获取到(Reentrantlock)
- 共享锁:可以被多个线程同时获取(CountDownLatch,ReadWriteLock).
-
AQS内部的数据结构与原理
AQS内部实现了两个队列,一个同步队列,一个条件队列。
同步队列的作用是:当线程获取资源失败之后,就进入同步队列的尾部保持自旋等待,不断判断自己是否是链表的头节点,如果是头节点,就不断参试获取资源,获取成功后则退出同步队列。
条件队列是为Lock实现的一个基础同步器,并且一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。
更多Java学习资料、面试真题获得,请【点击此处】
27. 运行时异常与受检异常
异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误,只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关,即使程序设计无误,仍然可能因使用的问题而引发。Java编译器要求方法必须声明抛出可能发生的受检异常,但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样,是面向对象程序设计中经常被滥用的东西,在Effective Java中对异常的使用给出了以下指导原则:
- 不要将异常处理用于正常的控制流(设计良好的API不应该强迫它的调用者为了正常的控制流而使用异常)
- 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
- 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
- 优先使用标准的异常
- 每个方法抛出的异常都要有文档
- 保持异常的原子性
- 不要在catch中忽略掉捕获到的异常
28. BIO/NIO/AIO
- BIO:同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
- NIO:NIO是一种同步非阻塞的I/O模型,NIO中的所有IO都是从 Channel(通道) 开始的。
1)从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
2)从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。 - AIO:异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
- 总结:对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
https://blog.csdn.net/weixin_43809223/article/details/100529810
29. 锁的优化
- 锁升级:锁的4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高),锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
- 锁消除:Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间
30. 系统如何提高并发性?
- 提高CPU并发计算能力
1)多进程&多线程
2)减少进程切换,使用线程,考虑进程绑定CPU
3)减少使用不必要的锁,考虑无锁编程
4)考虑进程优先级
5)关注系统负载 - 改进I/O模型
1)DMA技术
2)异步I/O
3)改进多路I/O就绪通知策略,epoll
4)Sendfile
5)内存映射
6)直接I/O
更多Java学习资料、面试真题获得,请【点击此处】
以上是关于备战秋招冲击大厂Java面试题系列—并发编程的主要内容,如果未能解决你的问题,请参考以下文章