一、可重入锁
可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
可重入锁,意味着线程可以进入它已经拥有的锁的同步代码块。
Java 中的 synchronized(隐式)与Lock(显式)都是可重入锁。
二、示例
1、synchronized 同步代码块
public class SyncLockDemo {
public static void main(String[] args) {
// synchronized
Object o = new Object();
new Thread(()->{
synchronized(o) {
System.out.println(Thread.currentThread().getName()+" 外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName()+" 内层");
}
}
}
},"t1").start();
}
}
2、synchronized 同步方法(递归锁)
public class SyncLockDemo {
public synchronized void add() {
add();
}
public static void main(String[] args) {
new SyncLockDemo().add();
}
}
3、Lock 可重入锁
public class SyncLockDemo {
public synchronized void add() {
add();
}
public static void main(String[] args) {
//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁
lock.unlock();
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
}
}
使用ReentrantLock的注意点:
ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样
以下代码演示,加锁和释放次数不一样导致的死锁:
public class SyncLockDemo {
public static void main(String[] args) {
//Lock演示可重入锁
Lock lock = new ReentrantLock();
//创建线程
new Thread(()->{
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 外层");
try {
//上锁
lock.lock();
System.out.println(Thread.currentThread().getName()+" 内层");
}finally {
//释放锁
//lock.unlock(); // 这里故意注释,实现加锁次数和释放次数不一样
}
}finally {
//释放做
lock.unlock();
}
},"t1").start();
//创建新线程
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
}
}
由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
使用 ReentrantLock 一定要注意两点:① 无论如何都要手动释放锁;② 加锁次数和释放次数要一样;
三、锁的简单应用
用lock来保证原子性(this.count++这段代码称为临界区)
什么是原子性,就是不可分,从头执行到尾,不能被其他线程同时执行。
可通过CAS来实现原子操作
CAS(Compare and Swap):
CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS主要通过compareAndSwapXXX()方法来实现,而这个方法的实现需要涉及底层的unsafe类
unsafe类:java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作
这里有个介绍原子操作的博客
https://my.oschina.net/xinxingegeya/blog/499223
还有对unsafe类详解的博客
http://www.cnblogs.com/mickole/articles/3757278.html
示例:
public class Counter{
private Lock lock = new Lock();
private int count = 0;
public int inc(){
lock.lock();
this.count++;
lock.unlock();
return count;
}
}
四、设计锁
1、不可重入锁
所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。
先来设计一种锁,这其实是一个不可重入锁:
public class Lock{
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
举个例子:
public class Count{
Lock lock = new Lock();
public void print(){
lock.lock();
doAdd();
lock.unlock();
}
public void doAdd(){
lock.lock();
//do something
lock.unlock();
}
}
当调用print()方法时,获得了锁,这时就无法再调用doAdd()方法,这时必须先释放锁才能调用,所以称这种锁为不可重入锁,也叫自旋锁。
2、可重入锁
可重入锁设计如下:
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = thread;
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
相对来说,可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。
第一个线程执行print()方法,得到了锁,使lockedBy等于当前线程,也就是说,执行的这个方法的线程获得了这个锁,执行add()方法时,同样要先获得锁,因不满足while循环的条件,也就是不等待,继续进行,将此时的lockedCount变量,也就是当前获得锁的数量加一,当释放了所有的锁,才执行notify()。如果在执行这个方法时,有第二个线程想要执行这个方法,因为lockedBy不等于第二个线程,导致这个线程进入了循环,也就是等待,不断执行wait()方法。只有当第一个线程释放了所有的锁,执行了notify()方法,第二个线程才得以跳出循环,继续执行。
这就是可重入锁的特点,可重入锁的概念和设计思想大体如此,Java中的可重入锁ReentrantLock设计思路也是这样。
五、Java 中常用的可重入锁
synchronized
java.util.concurrent.locks.ReentrantLock
这里记录下java中实现原子操作的类(记录至http://blog.csdn.net/huzhigenlaohu/article/details/51646455)
六、ReenTrantLock可重入锁(和synchronized的区别)总结
1、可重入性
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程没进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2、锁的实现
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
3、性能区别
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。
3、功能区别
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized
4、ReentrantLock 独有的能力
(1)ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
(2)ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
(3)ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
5、ReentrantLock 实现的原理
在网上看到相关的源码分析,本来这块应该是本文的核心,但是感觉比较复杂就不一一详解了,简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。
6、什么情况下使用 ReentrantLock
答案是,如果你需要实现ReenTrantLock的三个独有功能时。
参考博客:
https://www.cnblogs.com/dj3839/p/6580765.html
https://blog.csdn.net/z69183787/article/details/79344382
https://justinneil.blog.csdn.net/article/details/98937600
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器