常见的面试题
锁策略:
实现一把锁的时候,针对这个锁进行的一些设定。任何涉及到锁的地方,都可能和锁策略有关。如果以后工作中真的需要实现一把锁,锁策略肯定要理解,但是普通程序员只要能知道概念,给面试官描述清楚即可。切记不能死记硬背要穿插自己的理解。
问题一:什么是乐观锁什么是悲观锁
乐观锁:预测该场景中不会出现锁冲突,后续做的工作少
悲观锁:预测该场景中会出现锁冲突,后续做的工作多
锁冲突:两个线程同时获取一把锁,一个线程获取成功,另一个线程阻塞等待。
锁冲突的概率大还是小对后续工作是有影响的。比如你在追一个女孩,这个女孩是校花,如果你尝试对她加锁,其它很多人也对她加锁,那么锁冲突的概率会很大。如果这个女孩是个长相普通,各个方面都很普通的女孩,那么你尝试对她加锁追她会很容易。
问题二:什么是重量级锁,什么是轻量级锁
轻量级锁:加锁开销比较小(花的时间少,占用系统资源少)
重量级锁:加锁开销比较大(花的时间多,占用系统资源多)
悲观乐观是在加锁之前对锁冲突概率的预测,决定工作的多少,重量轻量是在加锁之后,考虑实际锁的开销。因为乐观锁加锁开销可能比较小,针对同一个具体的锁,可能叫他乐观锁,也可能叫轻量级锁。
问题三:什么是自旋锁,什么是挂起等待锁
自旋锁:是轻量级锁的一种典型实现,在用户态下,通过自旋的方式(while循环),实现类似于加锁的效果。
挂起等待锁:是重量级锁的一种典型实现,通过内核态,借助系统提供的锁机制,当出现锁冲突的时候,会牵扯到内核对于线程的调度,使线程出现挂起(阻塞等待)。
自旋锁相当于追女孩子时,发现追的女孩子有男朋友了(锁被占用了),任然每天给这个女孩子发早安晚安,这样就可以在她分手的第一时间发现,并且抓住机会。这种锁会消耗一定的cpu资源,但是可以最快拿到锁。
挂起等待锁,如果发现女孩子有男朋友了,自己去忙自己的事情,等听说分手了后再去联络这个女生。这种方式消耗的cpu比较少,无法保证第一时间获取锁。
问题四.读写锁和互斥锁
读写锁:把读操作加锁和写操作加锁分开了
如果两个线程一个读加锁另一个也是读加锁那么不会产生锁竞争
如果两个线程一个读加锁另一个写加锁会产生锁竞争
如果两个线程一个写加锁另一个也是写加锁也会产生锁竞争
问题五.公平锁和不公平锁
公平锁遵守先来后到的锁,非公平锁,看起来是概率相等的,但是实际上是不公平的(每个线程阻塞的时间不一样),要想实现公平锁,就需要用一些额外的数据结构(比如统计每个线程阻塞的时间)。
问题六.可重入锁不可重入锁
不可重入锁:一个线程针对同一把锁连续加锁2次,如果产生死锁为不可重入锁
可重入锁:一个线程对同一把锁连续加锁2次,如果没有产生死锁为可重入锁。
猜想上述代码:synchronized对this加锁2次,this上的锁在increase方法执行完后才能释放。要想代码继续往下执行,就需要把第二次锁获取到,也就是让第一次锁释放,然后继续对this加锁.要想第一次锁释放,又需要保证代码继续执行,这样会产生死锁。但真的产生死锁了吗?
class Rea{
public int count=0;
public synchronized void increase(){
synchronized (this){
count++;
}
}
}
public class Test13 {
public static void main(String[] args) {
Rea rea = new Rea();
Thread thread = new Thread(()->{
for(int i =0;i<100;i++){
rea.increase();
}
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(rea.count);
}
}
其实并没有产生死锁因为synchronized是可重入锁,重复加锁2次并不会产生阻塞(文章后面讲),如果是不可重入锁去加锁2次那么会产生死锁。
这里的关键在于如果是一个不重入锁,这个锁不会保存是哪个线程对它加的锁,只要它当前处于加锁状态后收到了加锁这样的请求,就会拒绝当前加锁,而不管当下的线程是哪个,就会产生死锁。
可重入锁,则是会让这个锁保存,是哪个线程加上的锁,后续收到加锁请求之后,就会先对比一下,看看加锁的线程是不是当前持有自己这把锁的线程,这个时候可以灵活判定了。
问题七.可重入锁是怎么加锁解锁的
synchronized(this){
synchronized(this){
synchronized(this){
}
}
}
上述代码加了3次锁,第一次加锁后同时synchronized内部会创建一个计数器count,count自增1,碰到第二个synchroniezd不会重复的再去对同一个对象加锁,只是让count再去加1。同理第三次也不会加锁也让count自增,等执行完一个synchronized代码块会让count–,并不会立刻解锁,当count减为0时会立马解锁。其实synchronized只是加了一次锁同时也解锁了一次。
问题八.谈谈你对死锁的理解
死锁的三中典型情况:
1.一个线程一把锁,但是不可重入锁,该线程对这个锁连续加锁2次
2.两个线程2把锁,这2个线程分别获取到一把锁,然后再去尝试获取对方的锁
3.N个线程M把锁
第一点上述已经解锁过。
第二点:比如弟弟和妹妹吃饺子的时候,弟弟喜欢蘸醋,妹妹喜欢喜欢蘸酱油,弟弟和妹妹分别到自己碗里放了醋和酱油,弟弟想要妹妹的酱油,妹妹想要哥哥的醋,2个互不相让都想要对方先给自己,那么会产生死锁。
public class Test14 {
public static Object locker1 = new Object();
public static Object locker2 = new Object();
public static void main(String[] args) {
Thread thread = new Thread(()->{
synchronized (locker1){
System.out.println("abc");
synchronized (locker2){
System.out.println("bcd");
}
}
});
Thread thread1 = new Thread(()->{
synchronized (locker2){
System.out.println("fgh");
synchronized (locker1){
System.out.println("thf");
}
}
});
thread.start();
thread1.start();
}
}
执行线程1时对locker1加锁成功,同时也对locker2加锁,线程二尝试对locker1加锁的时候要线程释放locker2的锁,locker2解锁也要locke1解锁,这样就产生了死锁。要想解决这种问题,给锁进行编号,让编号从小到大依次加锁。
public class Test14 {
public static Object locker1 = new Object();
public static Object locker2 = new Object();
public static void main(String[] args) {
Thread thread = new Thread(()->{
synchronized (locker1){
System.out.println("abc");
synchronized (locker2){
System.out.println("bcd");
}
}
});
Thread thread1 = new Thread(()->{
synchronized (locker1){
System.out.println("fgh");
synchronized (locker2){
System.out.println("thf");
}
}
});
thread.start();
thread1.start();
}
}
问题九:如何避免死锁
1.互斥使用:一个线程获取到一把锁后,别的线程不能获取这把锁
2.不可抢占:锁只能被持有者主动释放,而不能被其他线程抢走
3.请求和保持:一个线程尝试获取多把锁,在获取第二把锁的过程中,会保持打一把锁的获取状态
4.循环等待:t1尝试获取locker2,需要t2执行完locker2,t2尝试获取locker1,需要t1执行完释放locker1
问题十.Synchronized具体采用了哪些锁策略
1.synchronized既是悲观锁也是乐观锁
2.synchronized既是重量级锁也是轻量级锁
3.synchronized重量级锁部分是基于系统的互斥锁实现的,轻量级锁部分是基于自旋锁实现的
4.synchronized是非公平锁,不会遵守先来后到,会抢占式加锁
5.synchroniezed是可重入锁
6.synchronized不是读写锁
问题十一:synchronized内部的锁策略
无锁----->偏向锁-------->轻量级锁------>重量级锁
偏向锁:不是真的加锁而是做了一个标记,如果有别的线程来竞争锁了才会真的加锁,如果没有竞争自始至终都不会加锁。
轻量级锁:如果这把我把锁占据了,另一个线程会按照自旋的方式反复的查看是不是解锁了。此时锁操作比较消耗cpu。
重量级锁:随着竞争的线程越来越多,从轻量级锁变为重量级锁,即使前一个线程释放锁也不一定能拿到锁,啥时候拿到不一定可能时间会很长。
问题十二:锁的粒度
关于锁的力度如果加锁操作里面包含实际要执行的代码越多,就认为锁的粒度大
for(......)
synchronized(this){
count++;
} //代码块1
}
synchronized(this)
for(.....)
count++;
}
}//代码块2
第一个代码块锁的粒度小,因为每次加锁执行的代码少,第二个代码块锁的粒度大,加锁执行的代码多。
下一篇:CAS