聊聊高并发(十六)实现一个简单的可重入锁

可重入锁指的是如果一个线程已经获得了一个锁,那么它可以多次进入这个锁,当然前提是线程需要先获得这个锁。

可重入锁是最常使用的锁,Java的内置锁就是可重入锁,使用synchronized关键字可以启用内置锁机制,比如说一个类有两个synchronized方法A和B,在A方法中调用了B方法,如果锁不是可重入的,那么访问B时需要再次竞争锁,这样会带来死锁。

        public synchronized void A(){
		B();
	}

	public synchronized void B(){

	}

可重入锁解决了这个问题,它使用一个计数器来记录一个线程进入锁的次数,每次进入锁计数器就加1,释放锁减1,直到计数器为0时表示真正释放了锁。其他锁看到计数器不为0时就知道有其他线程已经获得了锁,就需要等待。Java的内置锁的基本原理也是这样,JDK1.5之后提供了显式锁ReentrantLock也是这个基本原理。

下面实现一个简单的可重入锁,

1. 使用一个Thread引用指向获得锁的线程

2. 使用一个计数器记录一个线程进入锁的次数,当计数器为0时表示锁是空闲的

3. 使用一个内部锁Lock来同步线程

4. 使用一个isHoldZero的条件来进行条件队列操作

5. 当获得锁的线程是自己时,只修改计数器的值,直接获得锁

6. 当获得锁的线程不是自己时,需要在holdCount !=0 这个条件谓词上等待,直到计数器归0,再次竞争锁

7. 释放锁时计数器减1,当计数器为0时,唤醒在条件队列中等待的线程

package com.zc.lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 简单的可重入锁实现,使用一个计数器记录当前线程重入锁的次数,获得锁时计数器加1,释放锁时计数器减1,当计数器等于0时表示释放了锁
 * **/
public class SimpleReentrantLock implements Lock{
    
    // 指向已经获得锁的线程
    private volatile Thread exclusiveOwnerThread;
    
    // 记录获取了同一个锁的次数
    private volatile int holdCount;
    
    private final java.util.concurrent.locks.Lock lock;
    
    // 是否是自己获得锁的条件
    private final Condition isCountZero;
    
    public SimpleReentrantLock(){
        lock = new ReentrantLock();
        isCountZero = lock.newCondition();
        holdCount = 0;
    }
    
    @Override
    public void lock() {
        lock.lock();
        try{
            // 当前线程的引用
            Thread currentThread = Thread.currentThread();
            // 如果获得锁的线程是自己,那么计数器加1,直接返回
            if(exclusiveOwnerThread == currentThread){
                holdCount ++;
                return;
            }
            
            while(holdCount != 0){
                try {
                    isCountZero.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException("Interrupted");
                }
            }
            // 将exclusiveOwnerThread设置为自己
            exclusiveOwnerThread = currentThread;
            holdCount ++;
        }finally{
            lock.unlock();
        }
    }

    @Override
    public void unlock() {
        lock.lock();
        try{
            holdCount --;
            if(holdCount == 0){
                isCountZero.signalAll();
            }
        }finally{
            lock.unlock();
        }
    }

}
时间: 2024-10-09 22:13:26

聊聊高并发(十六)实现一个简单的可重入锁的相关文章

聊聊高并发(六)实现几种自旋锁(一)

在聊聊高并发(五)理解缓存一致性协议以及对并发编程的影响 我们了解了处理器缓存一致性协议的原理,并且提到了它对并发编程的影响,"多个线程对同一个变量一直使用CAS操作,那么会有大量修改操作,从而产生大量的缓存一致性流量,因为每一次CAS操作都会发出广播通知其他处理器,从而影响程序的性能." 这一篇我们通过两种实现自旋锁的方式来看一下不同的编程方式带来的程序性能的变化. 先理解一下什么是自旋,所谓自旋就是线程在不满足某种条件的情况下,一直循环做某个动作.所以对于自旋锁来锁,当线程在没有获

聊聊高并发(十八)理解AtomicXXX.lazySet方法

看过java.util.concurrent.atomic包里面各个AtomicXXX类实现的同学应该见过lazySet方法,比如AtomicBoolean类的lazySet方法 public final void lazySet(boolean newValue) { int v = newValue ? 1 : 0; unsafe.putOrderedInt(this, valueOffset, v); } 它的底层实现调用了Unsafe的putOrderedInt方法,来看看putOrde

聊聊高并发(二十七)解析java.util.concurrent各个组件(九) 理解ReentrantLock可重入锁

这篇讲讲ReentrantLock可重入锁,JUC里提供的可重入锁是基于AQS实现的阻塞式可重入锁.这篇 聊聊高并发(十六)实现一个简单的可重入锁 模拟了可重入锁的实现.可重入锁的特点是: 1. 是互斥锁,基于AQS的互斥模式实现,也就是说同时只有一个线程进入临界区,唤醒下一个线程时也只能释放一个等待线程 2. 可重入,通过设置了一个字段exclusiveOwnerThread来标示当前获得锁的线程.获取锁操作是,如果当前线程是已经获得锁的线程,那么获取操作成功.把当前状态作为获得锁次数的计数器

聊聊高并发(七)实现几种自旋锁(二)

在聊聊高并发(六)实现几种自旋锁(一) 这篇中实现了两种基本的自旋锁:TASLock和TTASLock,它们的问题是会进行频繁的CAS操作,引发大量的缓存一致性流量,导致锁的性能不好. 对TTASLock的一种改进是BackoffLock,它会在锁高争用的情况下对线程进行回退,减少竞争,减少缓存一致性流量.但是BackoffLock有三个主要的问题: 1. 还是有大量的缓存一致性流量,因为所有线程在同一个共享变量上旋转,每一次成功的获取锁都会产生缓存一致性流量 2. 因为回退的存在,不能及时获取

聊聊高并发(三十二)实现一个基于链表的无锁Set集合

Set表示一种没有反复元素的集合类,在JDK里面有HashSet的实现,底层是基于HashMap来实现的.这里实现一个简化版本号的Set,有下面约束: 1. 基于链表实现.链表节点依照对象的hashCode()顺序由小到大从Head到Tail排列. 2. 如果对象的hashCode()是唯一的.这个如果实际上是不成立的,这里为了简化实现做这个如果.实际情况是HashCode是基于对象地址进行的一次Hash操作.目的是把对象依据Hash散开.所以可能有多个对象地址相应到一个HashCode.也就是

聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

在前几篇将Java内存模型的那些事基本上把这个域底层的概念都解释清楚了,聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障 这篇分析了在X86平台下,volatile,synchronized, CAS操作都是基于Lock前缀的汇编指令来实现的,关于Lock指令有两个要点: 1. lock会锁总线,总线是互斥的,所以lock后面的写操作会写入缓存和内存,可以理解为在lock后面的写缓存和写内存这两个动作称为了一个原子操作.当总线被锁时,其他的CPU是无法使用总线的,也就让其他的读写都等

聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源代码分析

ThreadPoolExecutor是Executor运行框架最重要的一个实现类.提供了线程池管理和任务管理是两个最主要的能力.这篇通过分析ThreadPoolExecutor的源代码来看看怎样设计和实现一个基于生产者消费者模型的运行器. 生产者消费者模型 生产者消费者模型包括三个角色:生产者,工作队列,消费者.对于ThreadPoolExecutor来说, 1. 生产者是任务的提交者,是外部调用ThreadPoolExecutor的线程 2. 工作队列是一个堵塞队列的接口,详细的实现类能够有非

聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore

前几篇分析了一下AQS的原理和实现.这篇拿Semaphore信号量做样例看看AQS实际是怎样使用的. Semaphore表示了一种能够同一时候有多个线程进入临界区的同步器,它维护了一个状态表示可用的票据,仅仅有拿到了票据的线程尽能够进入临界区,否则就等待.直到获得释放出的票据. Semaphore经常使用在资源池中来管理资源.当状态仅仅有1个0两个值时,它退化成了一个相互排斥的同步器.类似锁. 以下来看看Semaphore的代码. 它维护了一个内部类Sync来继承AQS,定制tryXXX方法来使

聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类

这篇说说java.util.concurrent.atomic包里的类,总共12个.网上有非常多文章解析这几个类.这里挑些重点说说. 这12个类能够分为三组: 1. 普通类型的原子变量 2. 数组类型的原子变量 3. 域更新器 普通类型的原子变量的6个, 1. 当中AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference分别相应boolean, int,  long, object完毕主要的原子操作 2. AtomicMarkableRe