前言
刷b站短视频时,经常会遇到讲解Java中各种锁的,如可重入锁,排他锁,读锁等等,讲到锁,那么必然与多线程相关,在Java中多线程的地位还是很高的,所以很容易就被这种短视频迷惑,直到自己亲自探索,才发现也就那么回事。其实在Java中正真的锁只有一把,那就是synchronized,其他所有的锁,都是通过代码的控制某一个变量,来达到类似锁的机制,那么下面就列举下我们常见到的锁
常见的锁
这些锁大多都是些概念,Java中通过ReentrantLock及其扩展便能找到对应的源码,ReentrantLock也包含了大部分的锁实现
- 乐观锁
- 悲观锁
- 自旋锁
- 非公平锁
- 公平锁
- 可重入锁(递归锁)
- 读写锁
- 独占锁(排他锁)
- 共享锁
- 同步锁Synchronized
悲观锁
悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。悲观锁的实现往往依靠数据库本身的锁功能实现。
Java实现: Synchronized 和 ReentrantLock 等独占锁(排他锁)是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁。
乐观锁
乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量
Java实现:
java.util.concurrent 包中的 Atomic 类(如 AtomicInteger、AtomicLong、AtomicReference 等)提供了基于CAS操作的原子性操作。这些类底层的cas其实依赖sun.misc.Unsafe实现。cas本质还是依赖计算机硬件
StampedLock: java.util.concurrent.locks 包中引入了 StampedLock,读写锁的改进版本,支持乐观读取
自旋锁
多线程在竞争锁时,同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?
通常有两种处理方式:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。
Java实现:太多了,juc包下的有太多cas加自旋的方式等待其他的线程释放资源。
非公平锁和公平锁
公平锁和非公平锁指的是获取线程获取锁时的顺序。公平锁指按照锁申请的顺序来获取锁,线程直接进入队列中,队列中的第一个线程才能获取锁。非公平锁指多个线程获取锁时,直接尝试获取锁,只有当线程未获取到锁时才放入队列中
Java实现:ReentrantLock的内部 提供了公平锁和非公平锁两种实现,默认使用非公平锁
可重入锁
可重入锁允许同一个线程在持有锁的情况下多次获取该锁,而不会导致死锁。同一个线程在持有锁的情况下,可以重复地进入被锁保护的代码块。
Java实现:ReentrantLock
不可重入锁
不可重入锁不允许同一个线程在持有锁的情况下再次获取该锁。如果一个线程已经持有该锁,再次获取时会导致死锁。 同一个线程不能多次获取锁。
Java实现:Java中的 synchronized 关键字就是一种不可重入锁
Object lock = new Object();
// 第一次获取锁
synchronized (lock) {
// 执行被锁保护的代码
// ...
// 第二次获取锁,会导致死锁
synchronized (lock) {
// 这里的代码不会执行到
// ...
}
}
读写锁,排他锁,共享锁
读锁就是共享锁,写锁就是排他锁
Java实现:ReentrantReadWriteLock 分别实现了读锁和写锁