Java并发编程之锁

2023-09-10 八股文
1.你了解乐观锁和悲观锁吗?
  1. 悲观锁:每次读数据都会上锁,如synchronized
  2. 乐观锁:只有在更新时判断版本号是否正确
  3. 悲观锁适合写多,乐观锁适合读多吐量比悲观锁高
2.什么是可重入锁/不可重入锁?
  1. 可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
  2. 不可重入锁:当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞

例如我们使用mounthA,如果使用可重入锁mounthB就能获取到,使用不可重入锁mounthB就会阻塞。

private void mounthA(){
    // TODO 获取锁
    mounthB();
}

private void mounthB(){
    // TODO 获取锁
    //其他操作
}
3.什么是公平锁/非公平锁?有什么特点?使用场景?
  • 公平锁:多个线程顺序排队获取锁,保证每个线程都有机会获取资源,吞吐量可能会降低;因为处理第一个线程其他的都会阻塞。
  • 非公平锁是:多个线程同时尝试获取锁,获取不到则进入队列等待;这种方式可以减少唤醒线程的开销,提高吞吐量,但可能导致某些线程长时间无法获得锁,产生饥饿现象。
4.什么是排他锁和共享锁?
  • 共享锁:不允许写入,只允许查看,可被多个线程持有
  • 排他锁:又称互斥锁/写锁/独占锁,每次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁
5.说一下JVM中的偏向锁、轻量级锁和重量级锁
  1. 添加线程睡眠会从无锁变为匿名偏向锁
  2. 使用synchronized锁住对象,会从匿名偏向锁升级为偏向锁
  3. 如果多个线程同时竞争(CAS方式)synchronized修饰的代码块会升级为轻量级锁
  4. 如果CAS自旋时间过长仍获取不到资源,会升级为重量级锁
6.synchronized升级过程中有几次自旋?
  1. 获取已被其他线程持有的轻量级锁,会CAS自旋等待其释放
  2. 获取已被其他线程持有的重量级锁,会CAS自旋等待其释放(JDK8默认情况下不会开启重量级锁自旋)
7.synchronized的锁优化是怎样的?
  1. 自旋锁:用于快速接手刚释放的锁,可以避免普通线程挂起等待,提高效率;大多情况下锁的持有者很快就会释放锁,因此采用传统的线程挂起等待锁释放后恢复的方法效率较低。
  2. 锁消除:对于一些局部的同步代码块,jvm确定其不会被其他线程访问掉,会在JIT编译时取消同步。
  3. 锁粗化:如果在一段代码中连续的对同一个对象反复加锁解锁,编译时会释放放款加锁范围。

锁消除如下所示,hello对象只存于f()内,不会被其他线程访问到:

java public void f() { Object hello = new Object(); synchronized(hello) { System.out.println(hello); } }

锁粗化如下所示:

for(int i=0;i<100000;i++){  
    synchronized(this){  
        do();  
}
// 会被粗化成
synchronized(this){  
    for(int i=0;i<100000;i++)
        do();
}
7.解释一下什么是死锁?你能举一个死锁的例子吗?

多线程在互相竞争资源时都没有办法获取到资源出现的一种状况,若无外力阻止将永久阻塞下去,生产环境如果出现死锁非常难以排查,因为他不是必然出现的,死锁需要满足四个条件才能触发。

例如如下代码,在锁A申请锁B,锁B申请锁A,在微观角度如果同时执行就会发送死锁,因为methodA执行后由于methodB也执行了,导致methodA内获取不到LOCK_B。

public class Test2 {

    private static String LOCK_A = "lock_a";
    private static String LOCK_B = "lock_b";

    public void methodA() {
        synchronized (LOCK_A) {
            System.out.println("我是A方法中获得了锁A " + Thread.currentThread().getName());
            synchronized (LOCK_B) {
                System.out.println("我是A方法中获得了锁B " + Thread.currentThread().getName());
            }
        }
    }

    public void methodB() {
        synchronized (LOCK_B) {
            System.out.println("我是B方法中获得了锁B " + Thread.currentThread().getName());
            synchronized (LOCK_A) {
                System.out.println("我是B方法中获得了锁A " + Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("主线程运行开始运行:" + Thread.currentThread().getName());
        Test2 test2 = new Test2();
        // 使用for循环的原因是,死锁并不一定会触发
        for (int i = 0; i < 20; i++) {
            new Thread(test2::methodA).start();
            new Thread(test2::methodB).start();
        }
        System.out.println("主线程运行结束:" + Thread.currentThread().getName());
    }
}
上次更新: 5 个月前