java多线程面试题(一)

常见的面试题

锁策略:

实现一把锁的时候,针对这个锁进行的一些设定。任何涉及到锁的地方,都可能和锁策略有关。如果以后工作中真的需要实现一把锁,锁策略肯定要理解,但是普通程序员只要能知道概念,给面试官描述清楚即可。切记不能死记硬背要穿插自己的理解。
问题一:什么是乐观锁什么是悲观锁

乐观锁:预测该场景中不会出现锁冲突,后续做的工作少
悲观锁:预测该场景中会出现锁冲突,后续做的工作多
锁冲突:两个线程同时获取一把锁,一个线程获取成功,另一个线程阻塞等待。

锁冲突的概率大还是小对后续工作是有影响的。比如你在追一个女孩,这个女孩是校花,如果你尝试对她加锁,其它很多人也对她加锁,那么锁冲突的概率会很大。如果这个女孩是个长相普通,各个方面都很普通的女孩,那么你尝试对她加锁追她会很容易。
问题二:什么是重量级锁,什么是轻量级锁

轻量级锁:加锁开销比较小(花的时间少,占用系统资源少)
重量级锁:加锁开销比较大(花的时间多,占用系统资源多)
悲观乐观是在加锁之前对锁冲突概率的预测,决定工作的多少,重量轻量是在加锁之后,考虑实际锁的开销。因为乐观锁加锁开销可能比较小,针对同一个具体的锁,可能叫他乐观锁,也可能叫轻量级锁。

问题三:什么是自旋锁,什么是挂起等待锁

自旋锁:是轻量级锁的一种典型实现,在用户态下,通过自旋的方式(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

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值