常见的锁策略(面试八股文)

1.乐观锁vs悲观锁

        乐观锁:预测该场景中不太会出现锁冲突的情况。(后续做的工作会更少)

        悲观锁:预测该场景非常容易出现锁冲突(后续做的工作会更多)

        锁冲突:多个线程同时尝试去获得同一把锁,其中一个线程能够获取成功,其余线程阻塞等待

        乐观锁和悲观锁是在加锁之前,对锁冲突概率的预测,决定开销的多少

2.重量级锁vs轻量级锁

        重量级锁:加锁的开销是比较大的(花的时间多,占用的资源多),一个悲观锁很可能就是一个重量级锁(不绝对)

        轻量级锁:加锁的开销比较小(花的时间少,占用的资源少),一个乐观锁很可能就是一个轻量级锁(不绝对)

        .重量级和轻量级锁是在加锁之后对锁实践开销的考量

3.读写锁vs互斥锁

        互斥锁:就是普通的锁,没有为读操作和写操作分开加锁,synchronized就是互斥锁

        读写锁:把读操作加锁和写操作加锁分开了

                因为多线程同时去读同一个变量不会涉及到线程安全问题

                如果两个线程,一个线程读加锁,另一个线程也是读加锁,不会发生锁竞争(多线程并发执行的效率就会更高)

                如果两个线程,一个线程写加锁,另一个线程也是写加锁,会发生锁竞争

                如果两个线程,一个线程写加锁,另一个线程是读加锁,会发生锁竞争

        在实践开发中,读操作的频率往往比写操作的频率高,所以通过读写锁可以大大提高程序运行的效率(因为在多个线程对同一个变量进行读取操作的时候不会发生锁竞争,是并发执行的)

4.自旋锁vs挂起等待锁

        自旋锁:一种典型的轻量级锁的实现方式,线程在抢锁失败后会进入阻塞状态,要等到获得锁的线程释放锁才能去尝试获取锁,而自旋锁会在获取锁失败了以后,立即再尝试去获取锁,无限循环(一直不停的去尝试获取锁)这样一旦锁被其他线程释放,就能够第一时间获得锁。

                优点:一旦锁被释放可以第一时间获得锁(对比挂起等待锁,获得锁的速度会快很多)

                缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗cpu资源(而挂起等待的时间是不消耗cpu的)

        挂起等待锁:是重量级锁的一种典型表现,当出现锁冲突的时候,会牵扯到内核对于线程的调度,使冲突的线程出现挂起(阻塞等待)

5.公平锁vs非公平锁

        假设三个线程A, B,C.A先尝试获取锁,获取成功.然后B再尝试获取锁,获取失败,阻塞等待;然后C也尝试获取锁,C也获取失败,也阻塞等待.
        当线程A释放锁的时候,会发生啥呢?
        公平锁:遵守"先来后到".B比C先来的.当A释放锁的之后,B就能先于C获取到锁

        非公平锁:不遵守"先来后到".B和C都有可能获取到锁.

6.可重入锁和不可重入锁

        有一个线程,针对同一个对象,连续加锁了两次,如果产生了死锁就是不可重入锁,如果没有产生死锁就是可重入锁

        例子:

public synchronized void increase(){
    synchronized(this){
        count++;
    }
}

        如上述代码,调用increase方法后会在同一个线程中对同一个对象进行两次加锁,如果是不可重入锁的话,在调用increase方法对类对象进行加锁了以后,执行increase方法中的程序再对类对象进行加锁就会发生阻塞等待,一直要阻塞到第一个对类对象加锁的线程释放锁,但要执行完该线程又需要获得锁了以后才能向下执行,所以程序就卡在了第二次对类对象加锁这里,也就形成了死锁。

死锁的三种经典情况

        1.一个线程一把锁,但是是不可重入锁,该线程针对这个锁连续加锁两次,就会出现死锁

        2.两个线程,两把锁,这两把线程先分别获取到一把锁,然后再同时尝试获取对方的那把锁

通过下面的代码可以直观的看到该情况

ublic class Demo1 {
    public static void main(String[] args) {
        Object blocer1=new Object();
        Object blocer2=new Object();

        Thread t1=new Thread(()->{
            synchronized(blocer1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized(blocer2){
                    System.out.println("t1线程获取两把锁");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized(blocer2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized(blocer1){
                    System.out.println("t2线程获取两把锁");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

        3.N个线程M把锁,N个线程中每个线程都会对多把锁进行加锁,而且加的锁是有重复的,这样在莫种特殊情况下就会形成死锁

产生死锁的四个必要条件

        1.互斥使用,一个线程获取到一把锁了以后,其他线程就不能去获取这把锁(实际使用的锁,一般都是互斥的,锁的基本特征)

        2.不可抢占,锁只能是持有者主动释放,而不是被其他其他线程直接抢走(也是锁的基本特征)

        3.请求和保持,当一个线程去尝试获取多把锁,在获取第二把锁的时候会保持对第一把锁的获取状态(取决于代码结构)

        4.循环等待,t1尝试获取locker2,需要t2执行完,释放locker2,t2尝试获取locker1,需要t1执行完,释放locker1,发生了死循环(取决于代码结构)

解决死锁的有效方法

        解决死锁主要是针对循环等待这方面来进行解决

        当每个线程要获取多把锁,先针对锁进行编号,约定,每个线程如果要获取多把锁,必须先获取编号小的锁,后获取编号大的锁。

        只要所有线程加锁的顺序,都严格遵守上述顺序,就一定不会出现循环等待,就解决了死锁

synchronized采用的锁策略

        1.即是悲观锁也是乐观锁,即是重量级锁也是轻量级锁,是自适应的

        2.重量级锁部分是根据系统的互斥锁实现的,轻量级锁部分是根据自旋锁实现的

        3.非公平锁(不会遵循先来后到,锁释放后,那个线程获得锁,各凭本事)

        4.可重入锁,同一个线程可以多次获取同一把锁(内部会记录哪个线程拿了锁,记录引用计数,要是是同一个线程拿锁,计数器就加一,此时该线程要解锁的话,计数器就减一,直到1计数器为0才真正解锁)

        5.不是读写锁,是普通的互斥锁

synchronized的自适应过程

        代码写了一个synchronized之后,这里可能会产生一系列的自适应过程:无锁->偏向锁->轻量级锁->重量级锁

        一开始是无锁的,加了synchronized后就对程序加了偏向锁,偏向锁,不是真的加锁,而只是做了一个标记,如果有别的线程来竞争锁了,才会真的加锁,如果没有别的线程竞争,就自始至终都不会真的加锁

        偏向锁真的加锁以后就是轻量级锁,但是后续如果竞争这把锁的线程越来越多了(锁冲突更激烈了),从轻量级锁升级成了重量级锁

你可能感兴趣的:(面试,java,职场和发展)