在java里,juc一直占据着重要的一面,关于juc的面试题一直也是面试官考验的重点,现在跟着本篇文章来详细了解一下各大juc面试题吧。
一、说说synchronized的底层原理?
java虚拟机里面的同步是基于进入和退出monitor对象实现的,无论是显式同步(同步代码块)还是隐式同步都是如此,当同步方法的时候并不是由monitorenter和monitorexit指令来实现同步的,而是由方法调用指令读取运行时常量池中的表结构的ACC_SYNCHRONIZED标志来隐式实现的;
同步代码块:monitorenter插入到同步代码块开始位置,monitorexit指令插入到同步代码块结束的位置,任何对象都有一个monitor与之关联,当且一个monitor被持有了之后,他将处于锁定的状态,当线程执行到monitorenter的地方时,他将会尝试获取对象锁持有的monitor所有权,即获取对象的锁;
monitorenter:每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter时尝试获取monitor的所有权,过程如下:
1.如果monitor的进入数为0,则该线程进入monitor,然后将进入数置为1,该线程即为monitor的持有者;
2.如果线程已经占有该锁,只是重新进入,则将monitor的进入数加1;
3.如果其他线程已经占有了该锁,那么该线程就处于阻塞状态,直到monitor的进入数为0,则将再次尝试获取该锁的所有权;
monitorexit:执行monitorexit的线程必须是对象所对应的monitor的持有者,指令执行时,monitor的进入数减1,直到monitor的进入数为0时,他将不再是该对象的monitor的持有者,其他被这个monitor阻塞的线程将尝试获取该monitor的所有权;
对于方法来说,方法的同步并没有通过monitorenter和monitorexit来实现(理论上也是通过这两个指令来实现的),相比与普通方法,其常量池中多了ACC_SYNCHRONIZED标志符;
方法调用时,调用指令会先检测方法的ACC_SYNCHRONIZED访问标志有没有被设置,如果设置了,执行线程将会获取monitor,获取成功之后才可以执行方法体,方法执行完之后会释放monitor,在方法执行期间,任何其他线程将不能获取到同一个monitor对象;
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
二、HashTable的原理?
HashTable是一个线程安全的哈希表;其key和value不能为空;其数据结构是和HashMap相同的;
HashTable内部通过synchronized来实现线程安全的,由于synchronized锁住了整张表,所以效率很低于ConcurrentHashMap的;
三、lock的原理?
lock是由java的jdk实现的,而synchronized是jvm自带的,以lock的可重入锁ReentrantLock为例,它把所有继承自Lock的操作委派给了Sync来实现,该类继承了AbstractQueuedSynchronizer,Sync有两个实现NonfairSync,FailSync,是为了非公平锁和公平锁定义的,默认情况下为非公平锁,
加锁是通过将所有的线程加入到CLH队列里面,当一个线程执行完成了之后会激活自己的后继节点;
nonfairTryAcquire是lock间接调用的第一个方法,每次请求锁时都会率先调用该方法,首先会先判断c==0,如果等于0则表示当前没有线程正在竞争该锁,如果c!=0说明已经有线程正在占用该锁。如果c等于0则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock的时候该值都会-1,当该值等于0的时候则释放锁,如果CAS设置成功,则可以预计其他任何线程调用CAS都不会在成功,也就人为该线程获取了该锁,很显然,该线程并未进入等待队列,如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能;
公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值l
非公平锁指在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。
四、ConCurrentHashMap是什么?
ConcurrentHashMap是java并发包下的一个高性能的且线程安全的HashMap实现,它采用了分段锁机制,将hashmap的结构分成了多个segment,以ConcurrentLevel为16而言,ConcurrentHashmap最多支持16个线程同时指向写操作,相比较于hashtab的全局锁,性能会很高;
每个segment相当于一个哈希表,他的参数和hashmap的结构类似,比如loadFactory,threshold等等,
我们查看类他的源码之后就可以得知,当指向put方法时,一共指向两步操作,1、先找到对应的segment,然后判断segment是否初始化,若初始化则调用segment的put方法;
get方法无需加锁,其中共享变量都通过volatile修饰,通过volatile修饰过的变量保证了可见性,不会读到过期的数据;
在1.8的优化:
链表改成了红黑树,当链表中的节点超过可阈值时,会将链表转成红黑树;
segment的分段锁ReentrantLock改成了cas+synchronized
五、volatile关键字,为什么不能保证原子性?
被volicate关键字修饰的变量会禁止线程内部缓存(不同的cpu会缓存数据到CPU cache中,操作的时候会直接读缓存的数据)和重排序的,即直接修改内存,对其他线程是可见的,
volatile原理:volatile修饰变量后,对该变量进行写操作时,汇编指令中会执行一个LOCK 前缀指令,该LOCK指令可以保证1.将当前处理器缓存行的数据写入到主内存中去;2.这个写回内存的操作会使得其他处理器缓存了该内存的地址无效;
不能保证原子性:首先,java中只有对变量的赋值和读取是原子性的,其他的操作都不是原子性的,比如a++,它分为三步,先读取a的值,然后将a+1,最后将a的值刷新的主内存上;这里面包括多个原子操作,多个原子操作加起来就不能保证原子性的了,举个例子:比如2个线程同时对a=100做了a++操作,线程A读取了a的值,此时还没来得及修改就阻塞了,B线程也去读取了a的值,此时A线程还没有来得及修改,那么B线程读取的值还是100,然后B线程对a+1,再写到内存里面,之后继续执行A线程,A线程已经读取到了a的值然后对a+1,a的值还是101,在将值写入到缓存中,并刷新到主存中;所以即使volatile即使能保证被修饰的变量具有可见性,但是不能保证原子性;
屏蔽指令重排序:编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序,达到更好的效果,指令重排序在单线程先不会有问题,但是在多线程下,可能会出现问题;
六、了解CountDownLatch吗?
CountDownLatch称之为闭锁;
闭锁可以用来确保某些活动直到其他活动全部结束之后才进行;
主要包含两个方法,一个是countDown(),一个是await();
countDown方法用来给计数器减一;
await方法是用来阻塞当前线程,直到计数器为0的时候在唤醒线程继续执行;
七、ArrayList、LinkedList、Vector、HashSet的区别?
ArrayList是由数组实现的,LinkedList是由双向链表实现的;
数组可以通过下标来检索数据,所以ArrayList的查找快,但是插入数据涉及到数组元素移动等内存操作,所以插入和删除慢;
链表只能通过遍历来查询数据,所以查找慢,但是插入和删除的话不需要对数据进行移动,所以插入和删除快;
Vector和ArrayList都是动态数组实现的,但是Vercor内部通过Synchronized修饰的,是线程安全的,而ArrayList是非线程安全的;Vertor扩容时,每次扩容为原来的2倍,ArrayList扩容是每次扩容为原来的1.5倍;
HashSet底层是HashMap,因为HashMap是非线程安全的,所以HashSet也是非线程安全的;HashSet调用add方法的时候,会往内部的HashMap中插入一个key为添加的值 value为预设的值的数值;如果之前key存在则覆盖;所以HashSet是不允许重复的;
八、什么是CyclicBarrier?
CyclicBarrier称之为循环屏障;
CyclicBarrier构造方法有两个参数,一个是parties,一个是barrierAction
parties是线程的个数;
barrierAction是最后一个线程要做的事;
使用场景:可以用来多线程统计数据,最后合并计算结果的场景;
和CountDownLatch的区别?
CyclicBarrier可以使用多次,用完之后可以用reset方法重置,CountDownLatch只能使用一次;
九、讲讲你了解的CAS
CAS 的全称是COMPARE AND SWAP,指的是,当设置的旧值与预想的值一致则修改,否则不修改;
CAS可能引发的问题:ABA
ABA是指当线程A进行CAS逻辑时,在从内存获取值val到执行CAS逻辑之间,会有一个时间差,而恰巧在这个时间差里面,另一个线程B修改了val值并做了一些其他操作之后又将val值改成了原来的值,虽然线程A仍然能CAS执行成功,但是线程B在对线程A中间执行的逻辑可能会导致出现意料之外的结果;
解决ABA方式:如果出现ABA问题会对我们的业务逻辑产生影响,那么我们就必须处理ABA问题,如果不会对我们的业务的逻辑产生影响,我们可以处理也可以不处理,比如AtomicInteger,它没有其他属性可以改变的,我们只在意它实际的值,所以就算出现ABA问题,我们也无需处理;
可以通过加version的方式来处理ABA问题。类似于我们在Mysql中用到的乐观锁,当我们进行CAS时,对要修改的值预加一个版本号,在对比内存中的值时,不仅比对值是否相等,我们还要比对版本号是否一致,并且对值修改时,版本号做自增,这样就避免了值相同,当时值代表的意义不同的后果了;
以上就是本篇文章的所有内容,希望能对小伙伴们的面试有所帮助,更多java面试题可以关注我们了解详情。
推荐阅读: