ReentrantLock锁原理

简介

ReentrantLock 提供了与synchronized相同的互斥性和内存可见性,提供了可重入的加锁语义,能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

与synchronized相比,它还为处理锁的不可用性提供了更高的灵活性,与此同时,ReentrantLock 还支持公平锁和非公平锁两种方式。

ReentrantLock底层使用了CAS+AQS队列实现

CAS(Compare and Swap)

CAS是一种无锁算法。

有3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

该操作是一个原子操作,被广泛应用在Java的底层实现中。

在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

1、CAS是CPU指令级的操作,只有一步原子操作

2、CAS避免了请求操作系统来裁定锁的问题,这一操作不需要OS操作系统参与,也就是不需要用户态和内核态的切换,效率很高

do {
  备份旧数据;
  基于旧数据构造新数据;
} while ( !CAS( 内存地址, 备份的旧数据, 新数据 ))

AQS

AQS,即AbstractQueuedSynchronizer,队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。

AQS使用一个FIFO的队列,先进先出,(也叫CLH队列,是CLH锁的一种变形),表示排队等待锁的线程。队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

CLH锁其实就是一种是基于逻辑队列非线程饥饿的一种自旋公平锁,由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁。

加锁lock()方法

第一种描述:

1、tryAcquire()

1.1检查state字段,若为0,表示锁未被占用,那么尝试占用;

1.2若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。

1.3如果以上两点都没有成功,则获取锁失败,返回false。

2、失败后,就要把线程加入到CLH队列中,addWaiter()加入AQS双向链表队列。

3、写入队列后,需要挂起当前线程,利用 LockSupport 的 part 方法来挂起当前线程的,直到被唤醒,for(;;) 自旋挂起。

第二种描述:

1 ReentrantLock先通过CAS尝试获取锁,获取到了就直接走。

1.1如果此时锁已经被占用,该线程加入AQS队列并wait()

1.2当前驱线程的锁被释放,挂在CLH队列为首的线程就会被notify(),然后继续CAS尝试获取锁,此时:

    1.2.1如果是非公平锁,如果有其他线程尝试lock(),有可能被其他刚好申请锁的线程抢占。

    1.2.2如果是公平锁,只有在CLH队列头的线程才可以获取锁,新来的线程只能插入到队尾。

解锁unlock()方法

1、释放时候,state–,通过state==0判断锁是否完全被释放。

2、成功释放锁的话,唤起一个被挂起的线程,使用LockSupport unpark来唤醒线程

可重入锁

字面意思是再进入的锁,对于 synchronized 关键字所使用的锁也是可重入的,两者关于这个的区别不大。

两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

PS:维护了state来统计这个线程的锁的计数。

公平锁和非公平锁的区别

锁的公平性是相对于获取锁的顺序而言的。

如果是一个公平锁,锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。

非公平锁只要CAS设置同步状态成功,则表示当前线程获取了锁,线程获取锁的顺序和调用lock的顺序无关,全凭运气,也就是老的线程排队使用锁,但是无法保证新线程抢占已经在排队的线程的锁。

ReentrantLock默认使用非公平锁,是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。

ReenTrantLock独有的

1、可以指定是公平锁还是非公平锁。而 synchronized 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

2、提供了一个 Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized 要么随机唤醒一个线程要么唤醒全部线程。

3、提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。

Varhandle介绍

Unsafe 是不建议开发者直接使用的,因为 Unsafe 所操作的并不属于Java标准,会容易带来一些安全性的问题。

JDK9 之后,官方推荐使用 java.lang.invoke.Varhandle 来替代 Unsafe 大部分功能,对比 Unsafe ,Varhandle 有着相似的功能,但会更加安全,并且,在并发方面也提高了不少性能。

Varhandle是对变量或参数定义的变量系列的动态强类型引用,包括静态字段,非静态字段,数组元素或堆外数据结构的组件。

在各种访问模式下都支持访问这些变量,包括简单的读/写访问,volatile 的读/写访问以及 CAS (compare-and-set)访问。简单来说 Variable 就是对这些变量进行绑定,通过 Varhandle 直接对这些变量进行操作。

总结

1、每一个ReentrantLock自身维护一个AQS队列记录申请锁的线程信息。

2、通过大量CAS保证多个线程竞争锁的时候的并发安全。

3、可重入的功能是通过维护state变量来记录重入次数实现的。

4、公平锁需要维护队列,通过AQS队列的先后顺序获取锁,缺点是会造成大量线程上下文切换。

5、非公平锁可以直接抢占,所以效率更高。

6、JDK9后实现AQS是用 Varhandle ,JDK1.8是用 Unsafe

唠嗑广场

投资没有银弹,工作也没有银弹。

想买入一家公司,然后再也不理他,一劳永逸,这是偷懒的表现,没有一劳永逸的好事。

工作也是如此,心心念念想找一家公司没有竞争,躺平,一劳永逸到退休,也是一种偷懒,只是银行岗位相对压力小而已,但铁饭碗是不可能的。

就没有铁饭碗,包括公务员也不是铁饭碗,因为人是活在预期内的,是活在未来的,就算在公务员里干着,心态也会变化,只要心态发生变化,动作就会变化。

没有什么是不可替代的,只是成本大小而已,连总统都可以换,何况你的小小工作?不可替代是不可能的,只有提高你的替换成本。

你想想,支付宝的总裁也可以换,大把的人可以新上任这个岗位,打工就是如此,只有提高你的价值,下岗了不怕,甚至可以换到更好的,就不怕。现在行情不好,换到更差的,其实也不怕的。

这和西风说的追求变化一样,追求变化,享受当下,逻辑是一样的。