当前位置: 首页 > news >正文

【Java面试——并发编程——相关类和关键字——Day4】

1. ReentrantLock

1.1 ReentrantLock 是什么?

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

public class ReentrantLock implements Lock, java.io.Serializable {}

ReentrantLock 里面有一个内部类 Sync,Sync 继承 (AQSAbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync 有公平锁 FairSync 和非公平锁 NonfairSync 两个子类。

在这里插入图片描述
ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。

1.2 公平锁和非公平锁有什么区别?

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

1.3 synchronized 和 ReentrantLock 有什么区别?

1.3.1 两者都是可重入锁

可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。

JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。
在下面的代码中,method1() 和 method2()都被 synchronized 关键字修饰,method1()调用了method2()。

public class SynchronizedDemo {public synchronized void method1() {System.out.println("方法1");method2();}public synchronized void method2() {System.out.println("方法2");}
}

由于 synchronized锁是可重入的,同一个线程在调用method1() 时可以直接获得当前对象的锁,执行 method2() 的时候可以再次获取这个对象的锁,不会产生死锁问题。假如synchronized是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 method2()时获取锁失败,会出现死锁问题。

1.3.2 synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。

ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

1.3.3 ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:

  • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来指定是否是公平的。
  • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

有关Condition接口的补充:

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而Condition实例的signalAll()方法,只会唤醒注册在该Condition实例中的所有等待线程。

1.4 可中断锁和不可中断锁有什么区别?

  • 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。
  • 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。

2. ReentrantReadWriteLock

2.1 ReentrantReadWriteLock 是什么?

ReentrantReadWriteLock 实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable{
}
public interface ReadWriteLock {Lock readLock();Lock writeLock();
}

一般锁进行并发控制的规则:读读互斥、读写互斥、写写互斥。
读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)。

ReentrantReadWriteLock 其实是两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。

和 ReentrantLock 一样,ReentrantReadWriteLock 底层也是基于 AQS 实现的。

在这里插入图片描述
ReentrantReadWriteLock 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显示的指定。

2.2 ReentrantReadWriteLock 适合什么场景?

由于 ReentrantReadWriteLock 既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。因此,在读多写少的情况下,使用 ReentrantReadWriteLock 能够明显提升系统性能。

2.3 共享锁和独占锁有什么区别?

共享锁:一把锁可以被多个线程同时获得。
独占锁:一把锁只能被一个线程获得。

2.4 线程持有读锁还能获取写锁吗?

  • 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
  • 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

2.5 读锁为什么不能升级为写锁?

  • 写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
  • 另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。

3. StampedLock

3.1 StampedLock 是什么?

StampedLock 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Condition。

不同于一般的 Lock 类,StampedLock 并不是直接实现 Lock或 ReadWriteLock接口,而是基于 CLH 锁 独立实现的(AQS 也是基于这玩意)。

public class StampedLock implements java.io.Serializable {
}

StampedLock 提供了三种模式的读写控制模式:读锁、写锁和乐观读。

  • 写锁:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 ReentrantReadWriteLock 的写锁,不过这里的写锁是不可重入的。
  • 读锁 (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 ReentrantReadWriteLock 的读锁,不过这里的读锁是不可重入的。
  • 乐观读:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。

StampedLock 还支持这三种锁在一定条件下进行相互转换 。

long tryConvertToWriteLock(long stamp){}
long tryConvertToReadLock(long stamp){}
long tryConvertToOptimisticRead(long stamp){}

StampedLock 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是StampedLock不可重入的原因。

// 写锁
public long writeLock() {long s, next;  // bypass acquireWrite in fully unlocked case onlyreturn ((((s = state) & ABITS) == 0L &&U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?next : acquireWrite(false, 0L));
}
// 读锁
public long readLock() {long s = state, next;  // bypass acquireRead on common uncontended casereturn ((whead == wtail && (s & ABITS) < RFULL &&U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?next : acquireRead(false, 0L));
}
// 乐观读
public long tryOptimisticRead() {long s;return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}

3.2 StampedLock 的性能为什么更好?

相比于传统读写锁多出来的乐观读是StampedLock比 ReadWriteLock 性能更好的关键原因。StampedLock 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。

3.3 StampedLock 适合什么场景?

和 ReentrantReadWriteLock 一样,StampedLock 同样适合读多写少的业务场景,可以作为 ReentrantReadWriteLock的替代品,性能更好。

不过,需要注意的是StampedLock不可重入,不支持条件变量 Condition,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 ReentrantLock 的一些高级性能,就不太建议使用 StampedLock 了。

另外,StampedLock 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用StampedLock 之前,看看 StampedLock 官方文档中的案例。

3.4 StampedLock 的底层原理了解吗?

StampedLock 不是直接实现 Lock或 ReadWriteLock接口,而是基于 CLH 锁 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。StampedLock 通过 CLH 队列进行线程的管理,通过同步状态值 state 来表示锁的状态和类型。


http://www.mrgr.cn/news/55785.html

相关文章:

  • 使用finalshell远程ssh连接ubuntu
  • 数据治理(1)-数据规划
  • 边界AIchat:智能办公与学习的全能助手
  • Python基础语法-列表与元组
  • 【小白学机器学习19】什么是量化分析(草稿)
  • 大数据新视界 --大数据大厂之大数据在智慧城市建设中的应用:打造智能生活的基石
  • 华为OD机试 - 生成哈夫曼树(Python/JS/C/C++ 2024 D卷 100分)
  • 快快收藏!学习 Python 最常访问的10个网站
  • MyBatis-Plus中FieldFill理解与应用
  • 编程题 7-22 龟兔赛跑【PAT】
  • C++游戏开发:从基础到进阶
  • JavaWeb——Maven(6/8):依赖管理-依赖传递 (介绍、直接依赖与间接依赖、演示、排除依赖)
  • Java 分页实战详解
  • 保研推荐信模板
  • Unity地面检测、跳跃
  • 低功耗4G模组的小秘密:RSA算法示例驾到,通通闪开...
  • 一分钟运行DBT入门示例项目(Jaffle Shop)
  • 新的类Rufus应用可带来简单的Windows 11 24H2安装旁路
  • 【07】z检验
  • Redis在实践的关键点
  • autMan框架对接飞书机器人
  • Golang | Leetcode Golang题解之第500题键盘行
  • Flutter Container容器组件实战案例
  • 精选录屏软件下载工具:记录精彩每一刻
  • 基于leaflet-polygon.fillPattern的面状对象图片填充实现
  • SQL CHECK 约束:确保数据完整性的关键