ReentrantLock可重入锁

1,可重入锁

    顾名思义就是支持重进入的锁,它表示该所能够支持一个线程对一个资源的重复加锁。此外,该锁还支持获取锁的时候的公平与非公平选择。

    之前构造的独占锁Mutex,代码如下所示:

public class Mutex implements Lock{
	//静态内部类,自定义同步器
	private static class Sync extends AbstractQueuedSynchronizer{
	    //是否处于可用状态
		@Override
		protected boolean isHeldExclusively() {
		return getState()==1;
		}

		//当状态为0的时候获取锁
		@Override
		protected boolean tryAcquire(int arg) {
		if(compareAndSetState(0, 1)){
			setExclusiveOwnerThread(Thread.currentThread());
			return true;
		}
		return false;
		}

		
		//释放锁,将状态设置为0
		@Override
		protected boolean tryRelease(int arg) {
			if(getState()==0) throw new IllegalMonitorStateException();
			setExclusiveOwnerThread(null);
			setState(0);
			return true;
		}
		
		
		//返回一个condition,每个condition都包含一个condition队列
		Condition newCondition(){
			return new ConditionObject();
		}
		
	}
	
	private final Sync sync=new Sync();
	
	@Override
	public void lock() {
		// TODO Auto-generated method stub
		sync.acquire(1);
	}

	@Override
	public void lockInterruptibly() throws InterruptedException {
		// TODO Auto-generated method stub
		sync.acquireInterruptibly(1);
	}

	@Override
	public boolean tryLock() {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
		// TODO Auto-generated method stub
		return sync.tryAcquireNanos(1,unit.toNanos(time));
	}

	@Override
	public void unlock() {
		// TODO Auto-generated method stub
		sync.release(1);
	}

	@Override
	public Condition newCondition() {
		// TODO Auto-generated method stub
		return sync.newCondition();
	}
       .........
}
当Mutex的lock()方法获取锁之后,如果再次调用lock()方法,则线程将会被自己阻塞,原因是Mutex在实现tryAcquire(int acquires)方法时返回了false,如下所示:

//当状态为0的时候获取锁
		@Override
		protected boolean tryAcquire(int arg) {
		if(compareAndSetState(0, 1)){
			setExclusiveOwnerThread(Thread.currentThread());
			return true;
		}
		return false;
		}
显然再次获取的时候状态不为0,于是返回false。

另外synchronized关键字隐式的支持重进入,这里补充下synchronized关键字同步加锁的原理:

        jvm源码在编译指令的时候,会对synchronized加锁的同步代码块使用monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采取哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的。

       任何对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCK状态。


另外Java的compareAndSet()方法调用简称为CAS:如果当前状态等于预期值,则以原子方式将同步状态设置为给定的更新值。此操作具有volatile读和写的内存语义,原理:

 从JVM底层源码可知,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加Lock前缀:如果是在多处理器上就加上前缀,单处理器上就省略lock前缀。

回到正题,下面看看ReentrantLock是怎么实现重进入和公平性获取锁的。


2, 锁的公平性问题

     在绝对时间上,先对锁进行获取的请求一定先被满足,那么锁是公平的,反之是不公平的。公平的锁机制往往没有非公平的
     ReentrantLock依赖Java同步器框架AbstractQueuedSynchronizer(AQS,同步器)。AQS使用一个整型的volatile变量(state)来维护同步状态,具体的ReentrantLock类图如下:

ReentrantLock分为公平锁和非公平锁

2.1 公平锁

先看公平锁的加锁lock()大致流程:
1)ReentrantLock:lock()
2)FairSync:lock()
3)AbstractQueuedSynchronizer:acquire(int arg)
4)ReenTrantLock:tryAcquire(int acquires)(FairSync类中的方法)
第四步才是真正的加锁,可看 FairSync类源码:
   
 /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        //公平锁加锁
        final void lock() {
            acquire(1);
        }

        //第四步,关键加锁实现
        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); //获取锁的开始,首先读volatile变量state
            if (c == 0) {
                if (<strong>!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)</strong>) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

然后是公平锁的解锁unlock()大致流程:
1)ReentrantLock:unlock()
2) AbstractQueuedSynchronizer:release(int arg)
3)Sync:tryRelease(int releases)
其中第三步是关键的释放锁,源代码如下:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);//释放锁的最后,写volatile变量state
            return free;
        }

公平锁在释放锁的最后写volatile变量state,获取锁的时候读这个变量。由happens-before规则,释放锁的线程在写volatile变量之前课件的共享变量,在获取锁的线程读取同一个volatile变量之后将立即变得对获取锁的线程可见。

2.2 非公平锁

非公平锁的释放与公平锁的释放完全一样,这里看其获取锁的原理
1)ReentrantLock:lock()
2)NonfairSync:lock()
3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)
非公平锁加锁;
final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

第三步是关键
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
这里将以原子操作的方式更新state变量。只有失败的时候才会去和公平锁一样,acquire(int arg),最后都是调用同一个方法
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&   //这里的tryAcquire(int arg)将根据公平锁与否去调用具体的方法nonfairTryAcquire或者tryAcquire(int arg)实现
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

2.3 区别分析

上面说道了nonfairTryAcquire与tryAcquire的区别,可以看nonfairTryAcquire的源码:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (<strong>compareAndSetState(0, acquires)</strong>) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可以看到对于非公平锁,只要CAS设置同步状态成功当前线程就可获取到锁
而公平锁增加了判断hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,需要等待前驱线程获取并释放锁之后当前线程才能继续获取锁

3,锁的可重入问题

锁的重入需要解决两个问题:线程再次获取锁与锁的最终释放
对于锁的再获取,需要去识别当前占据线程是否是要获取锁的线程
释放锁的时候,先要计数锁获取的次数,释放锁的时候计数自减,当计数等于0 时候才表示锁已经释放。
以上面非公平锁为例,看其nonfairTryAcquire(int acquires)方法源码:
       c==getState()查询锁的获取状态,c==0表示没有锁获取,即申请获取锁的线程不是占据锁的线程,直接CAS设置锁状态,并设置当前线程为占据线程;
     否则表示是再获取,判断当前线程是不是已经占据的线程,若是则只需要设置状态自增,改变同步状态值则可。

释放时,可看源码:

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
n次获取需要释放n次才行,如上面代码所示,只有最后当c==0,即同步状态为0时,才是完全释放锁,这时设置占据线程为null即可,释放锁成功。
好的,针对第二个问题的线程同步问题,我们可以使用同步方法和ReentrantLock可重入锁两种方式来解决。 ### 1. 同步方法 使用同步方法可以保证在同一时刻只有一个线程能够访问被synchronized关键字修饰的方法。在本例中,我们可以将withdraw和deposit方法都声明为同步方法,如下所示: ```java public synchronized void withdraw(double amount){ if (balance >= amount) { balance -= amount; System.out.println(Thread.currentThread().getName() + " withdraws " + amount + ", balance is " + balance); } else { System.out.println(Thread.currentThread().getName() + " withdraws failed, balance is " + balance); } } public synchronized void deposit(double amount){ if (amount > 0) { balance += amount; System.out.println(Thread.currentThread().getName() + " deposits " + amount + ", balance is " + balance); } else { System.out.println(Thread.currentThread().getName() + " deposits failed, balance is " + balance); } } ``` ### 2. ReentrantLock可重入锁 ReentrantLock可重入锁是一种更加灵活的线程同步方式,它比synchronized关键字提供了更多的功能,如公平锁、可中断锁、多条件变量等。在本例中,我们可以使用ReentrantLock来保证在同一时刻只有一个线程能够访问被锁住的代码块。具体实现如下: ```java private final ReentrantLock lock = new ReentrantLock(); public void withdraw(double amount){ lock.lock(); try { if (balance >= amount) { balance -= amount; System.out.println(Thread.currentThread().getName() + " withdraws " + amount + ", balance is " + balance); } else { System.out.println(Thread.currentThread().getName() + " withdraws failed, balance is " + balance); } } finally { lock.unlock(); } } public void deposit(double amount){ lock.lock(); try { if (amount > 0) { balance += amount; System.out.println(Thread.currentThread().getName() + " deposits " + amount + ", balance is " + balance); } else { System.out.println(Thread.currentThread().getName() + " deposits failed, balance is " + balance); } } finally { lock.unlock(); } } ``` 在以上代码中,我们使用了ReentrantLocklock()和unlock()方法来手动加锁和解锁代码块。使用try-finally语句块的方式可以确保即使在代码块执行过程中出现异常,锁也能够被正确释放。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值