并发编程面试(上)
1.synchronied原理:是由jvm实现的一种互斥同步的一种方式,被它所修饰过的代码块,
编译后的字节码,会发现在编译前后被编译器生成了monitorenter和
monitoreixt两个字节码指令。
在虚拟机执行到monitorenter指令时,首先要尝试获取对象的锁:如果这个对象
没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器+1;当执行monitorexit
指令时将锁计数器 - 1;当计数器为0的时候,锁就被释放了。
(a)如果获取对象的锁失败了,那么当前线程就要阻塞等待,直到对象锁被另外一个线程释放。
(b)java中的sychronized 通过在对象头设置标记,达到了获取锁和释放锁的目的。
2.什么是可重入性,为什么sychronized 是可重入锁
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。例如:一个类中的同步方法调用
另一个同步方法,假如不支持同步,进入method2方法时当前线程获得锁,method2方法里面执行
method1时当前线程又要去尝试获得锁,这时如果不支持重入,就要等它释放,把自己阻塞了。
在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有锁了,就把锁的计数器
+ 1,其本质上就是通过这种方式实现了可重入性。
3.JVM对 java的原生锁做了哪些优化
一种优化是自旋锁。在线程进入阻塞状态之前先让线程进入自旋等待一段时间,可能在等待期间已经解锁了。
这时候就无须再让线程执行阻塞操作,避免了状态切换。
(偏向锁)
(轻量锁)
(重量锁)
这三种锁使得JDK得以优化sychronized的运行,当jvm检测到不同的锁竞争状况时,会自动切换到合适的锁
实现,这就是锁的升级,降级。当锁没有竞争的时候,默认是偏向锁。
轻量级锁依赖CAS操作,Mark word 来试图获取锁,如果重试成功的话,就使用普通的轻量级锁,否则,
进一步升级为重量级锁。
4.为什么synchronized是非公平的
非公平主要表现在获取锁的行为上,并非是按照申请时间前后给等待线程分配锁的。每当锁被释放后,
任何一个线程都有机会竞争到锁,这样做的目的是为了提供性能。缺点是产生线程饥饿现象。
5.锁消除
虚拟机在即时编译的时候,对一些代码上要求同步,但被检测到实际不存在共享数据竞争的锁进行消除。
锁粗化:
原则上同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复进行加锁和解锁。甚至加锁
在循环操作内,导致性能消耗。锁粗化就是增大锁的作用范围。
6.为什么sychronized是悲观锁,乐观锁的实现原理是什么?什么是cas,有什么特性
sychronized显然是一个悲观锁,因为它的并发策略是悲观的,不管是否产生竞争,任何数据操作
都必须加锁,用户态核心态转换,维护锁计数器和检查是否有被阻塞的线程等操作。
乐观锁的核心算法是CAS,它涉及到三个参数,内存值,预期值,新值。当切仅当预期值和内存值
相等时,才将内存值修改为新值。这样的处理逻辑是,首先检查内存值是否跟我读取时的一样。
如果不一样则表示此内存已经被别的线程更改过,舍弃本次操作。否则说明期间没有其他线程
对此内存操作,可以把新值设置给此内存。
CAS具有原子性。
7.乐观锁一定是好的吗
乐观锁避免了悲观锁独占对象的现象,同时也提供了并发性能。当时也有缺点:
1.乐观锁只能保证一个共享变量的原子操作。如果多一个或者多几个。互斥锁能解决。
2.长时间自旋可能导致开销大,假如CAS长时间不成功而一直自旋,会给CPU带来很大开销。
3.ABA问题。CAS的核心思想是比较内存值和预期值是否一样而判断内存值是否被改过,这个逻辑
不严谨。假如内存值是A,后来被一线程改为了B,最后又改为了A,则CAS认为此内存值并没有
发生改变,但实际上是有被其他线程改过的。这种情况对依赖过程值的情景运算结果影响很大。
解决思路是引入版本号,每次变量更新把版本号加1
9.ReentrantLock及其显示锁相关问题
1.跟sychronized相比,ReentrantLock实现原理有什么不同
sychronized通过在对象头中设置标记实现,是一种jvm原生的锁实现的方式。而ReentrantLock
以及所有基于lock接口的实现类,都是通过一个volitile修改的int变量,并保证每个线程拥有该
变量的可见性和原子性,其本质是基于所谓的AQS
2.什么是AQS
1.aqs在内部定义了一个votile int state变量,表示同步状态。当线程调用lock方法时,如果state = 0
说明没有任何线程占有,可以获得锁,state=1;如果state = 1,说明有线程正在使用共享变量。
其他线程加入同步队列等待。
2.aqs通过node内部类构成一个双休链表结构的同步队列,来完成线程获取锁的排队工作,当有
线程获取锁失败的时候,就被添加到队尾。Node类有两个常量,分别代表共享模式和独占模式
共享模式就是一个锁可以多个线程同时操作(信号量就是基于AQS的共享模式),独占模式:一个时间
只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(ReentrantLock)
3.AQS通过内部类 ConditionObject构建等待队列,当Condition 调用wait()方法后,线程将会加入
等待队列,而当Condition调用signal()方法后,线程将从等待队列移动到同步队列中
10.Synchonized和ReentrantLock的异同
ReentrantLock是lock的实现类,是一个互斥同步锁。实现sychronized没有的高级功能
1.等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。
2.带超时的获取锁尝试:在指定时间范围内获取锁,时间到了仍然无法获取则返回。
3.可以判断是否有线程在排队等待获取锁
4.响应中断请求:能够响应中断异常,中断异常将会被抛出,同时锁会被释放。
5.可以实现公平锁
11. ReentrantLock是如何实现可重入性的。
ReentrantLock内部定义了同步器sync (既实现了aqs和aos),其实就是加锁的时候通过
AQS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程
ID和当前请求的线程ID是否一样,一样就可重入。
12. ReadWriteLock和stampedLock
ReentrantLock和sychronized简单实用,但是行为上有一定局限性,要么不占,要么占。
实际应用场景中,有时候不需要大量竞争的写操作。而是以并发读操作为主。为了进一步优化并发
操作的粒度,java提供了读写锁。
读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写操作是被某个线程持有。
读锁将无法获得,而只好等待对方操作结束。这样不会读取到有争议的数据。
读写锁相对开销比较大。JDK后期引入了StamedLock,在提供类似读写锁的同时,还
支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑
是尝试着修改,然后通过volidate方法确认是否进入了写模式,如果没有进入,就避免了开销。
如果进入,则尝试获取读锁。
13.如何让java的线程彼此同步?
countDownLatch,cyclicBarrier,Semaphore,通过它们可以方便的实现多线程之间协作的功能。
CountDownLacht叫倒计数,允许一个或者多个线程等待某些操作完成。
循环栅栏典型应用场景是等待并发线程结束,主要方法是await(),每被调用一次,计数器便减1,并阻塞当前线程,当计数减至0时,阻塞解除,所有再次上面阻塞的线程开始运行。
信号量:用于控制同时访问的线程个数,来达到限制通用资源访问的目的。原理是通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可。
线程池:
1.java中的线程池是如何实现的。
所谓的线程池,其实是抽象为了一个静态内部类Worker,它基于AQS实现的,存放在线程池的HashSet<Worker> workers成员变量中,而需要执行的任务则存放在成员变量workQueue中,基本的思想就是:从workQueue中不断取出需要执行的任务,放在Workers中执行。
2.创建线程池的几个核心构造参数
corePoolSize:核心线程数
maxmumPoolSize:允许最大的线程数
keepAliveTime:超过核心线程数时闲置线程的存活时间
workQueue:执行任务前保存的工作队列
3.线程池是怎么创建的
4.线程池有哪些
5.java中线程池提交线程
execute:接受一个Runable实例,用来执行一个任务
submit:返回的是future对象,可以用isDone()来查询future是否完成。可以调用get()方法获取结果
7.ThreadLocal是怎么解决并发安全
threadLocal是java提供的一种保存线程私有信息的机制,因为在整个线程生命周期内有效。
可以很方便地在一个线程关联的不同模块之间传递消息。ThreadLocal为每一个线程维护变量
的副本,把共享数据的可见范围限制在用一个线程内,其实现原理是,在ThreadLocal类中
有一个Map,用于存储每一个线程的变量的副本。
使用TreadLocal注意 remove,它的实现基于一个所谓的ThreadLocalMap,在这个Map中,
它的key是一个弱引用。项目的回收依赖于显示的触发,否则就要等待线程结束。

浙公网安备 33010602011771号