前言
采用synchronized进行加锁,是由jvm内部实现的 称为:内置锁。从java1.5开始,jdk api引入了新锁API 他们都继承自Lock,称为:显式锁,比如今天的主题ReentrantLock。之所以称做显式锁,主要有两点原因:1、相对于内置锁是有jvm内部实现的,显式锁是在使用java api实现的,确切的说是基于AQS实现的(对AQS的理解可以点击这里);2、使用Lock加锁,需要显式的加锁以及释放锁,相对于内置锁使用synchronized而言,要麻烦些。下面来看看ReentrantLock的基本用法:
public class ReentrantLockTest { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); lock.lock(); try{ //业务方法 }catch (Exception e){ //业务异常 }finally { lock.unlock(); } } }
从表面上看,使用显式锁比使用内置锁更繁琐,需要手动调用lock加锁和unlock解锁,如果忘记unlock 就会导致其他线程永远无法获取到锁的严重错误。既然为何还要新增显式锁呢?
简单的讲,显式锁的内置锁的补充:显式锁Lock提供了中断锁、定时锁、可轮询等实现,这些都是内置锁synchronized不具备的;显式锁可以指定为公平锁或非公平锁,而内置锁synchronized锁是非公平的。使用synchronized加锁,有些情况下很容易导致死锁,在这种情况下改用显式锁定时功能 在一段时间没有获取到锁,就放弃获取锁 就可以避免死锁。
另外与内置锁还有一个明显不同的地方是,内置锁可以用在方法上,而显式锁只能用在代码块上,也就是说强制使用更细粒度的加锁。
可以说内置锁和显式锁是互补关系:显式锁不能用在方法上,而且容易忘记释放锁;内置锁不可中断,有些情况下容易产生死锁,另外内置锁无法实现公平锁。根据这些差异在自己的实际业务场景中选择性使用即可,需要说明下的是显式锁的性能比内置锁性能稍微好些。
理论总结到处结束,下面开始以ReentrantLock锁分析下显式锁的实现原理。主要包括:排它锁、公平锁、非公平锁、中断锁、延迟锁、轮询锁、重入锁的实现原理。
ReentrantLock实现原理
对AQS的实现
首先需要说明的ReentrantLock与内置锁一样都是排它锁,从名字上开是与内置锁一样是可重入的。ReentrantLock与前两篇文章中讲的Semaphore和CountDownLatch一样都是基于AQS实现的,只是Semaphore和CountDownLatch都是共享锁的实现方法,而ReentrantLock是排它锁实现,也就是说ReentrantLock的内部类在实现AQS时是对tryAcquire、tryRelease这两个方法进行扩展的。首先来看内部类Sync对AQS的实现(Sync还有两个子类,分别对应公平和非公平锁实现):
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; //交给公平和非公平的子类去实现 abstract void lock(); //非公平的排它尝试获取锁实现 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果AQS的state为0说明获得锁,并且对state加1,其他线程获取锁时被阻塞 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //判断线程是不是重新获取锁,如果是 无需排队,对AQS的state+1处理,这就是重入锁的实现 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;//都不满足获取锁失败,进入AQS队列阻塞 } //公平的排它尝试释放锁实现 protected final boolean tryRelease(int releases) { int c = getState() - releases;//对应重入锁而言,在释放锁时对AQS的state字段减1 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {//如果AQS的状态字段已变为0,说明该锁被释放 free = true; setExclusiveOwnerThread(null); } setState(c); return free; } //判断是否是当前线程持有锁 protected final boolean isHeldExclusively() { // While we must in general read state before owner, // we don't need to do so to check if current thread is owner return getExclusiveOwnerThread() == Thread.currentThread(); } //获取条件队列 final ConditionObject newCondition() { return new ConditionObject(); } // Methods relayed from outer class final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } final boolean isLocked() { return getState() != 0; } /** * 说明ReentrantLock是可序列化的 */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } }
非公平锁
非公平锁实现:
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { //判断当前state是否为0,如果为0直接通过cas修改状态,并获取锁 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);//否则进行排队 } //调用父类的的非公平尝试获取锁 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
非公平锁的实现很简单,在lock获取锁时首先判断判断当前锁是否可以用(AQS的state状态值是否为0),如果是 直接“插队”获取锁,否则进入排队队列,并阻塞当前线程。
公平锁
公平锁实现:
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(); if (c == 0) { //AQS队列为空,或者当前线程是头节点 即可获的锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { 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; } }
公平锁的实现,跟Semaphore一样在tryAcquire方法实现中通过hasQueuedPredecessors方法判断当前线程是否是AQS队列中的头结点或者AQS队列为空,并且当前锁状态可用 可以直接获取锁,否则需要排队。
重入锁
不论是公平锁还是非公平锁的实现中,在tryAcquire方法中判断如果锁已经被占用,都会判断是否是当前线程占用,如果是 可以再次获取锁(无需排队),并对AQS的state字段加1;在释放锁时每次都减1,直到为0时,其他线程在可用。顺便提一下内置锁synchronized也是可以重入的。
//重入锁实现 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; }
排它锁
从两个tryAcquire的实现可以看出ReentrantLock的排它锁实现,根本上是通过AQS的state字段保证的,每次获取锁时都是首先判断state是否为0,并且只有1个线程能获取到锁。这个特性与内置锁synchronized相同。
关于ReentrantLock对AQS的三个内部类实现分析完毕,接下来看下ReentrantLock的核心方法,实现都很简单基本都是直接调用FairSync或者NonfairSync对AQS的实现。比如lock和unlock方法:
public void lock() { sync.lock(); } public void unlock() { sync.release(1); }
延迟锁
延迟锁,指的是在指定时间内没有获取到锁,就取消阻塞并返回获取锁失败,由调用线程自己决定后续操作,比如放弃操作或者创建轮询获取锁。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { //调用AQS带延时功能获取方法 return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
中断锁
tryLock(long timeout, TimeUnit unit)这个方法会抛出InterruptedException异常,可以用于实现中断锁,即等待的时间还未到,可以直接调用interrupt方法以中断获取锁。另外ReentrantLock还有一个方法可以实现中断锁,即:lockInterruptibly方法
public void lockInterruptibly() throws InterruptedException { //直接调用AQS的可中断获取方法 sync.acquireInterruptibly(1); }
通过调用该方法获取锁跟lock方法一样,如果获取不到会阻塞,不同的是使用这个方法获取锁是可以在外部中断的,但lock方法不行。使用灵活使用可中断锁,可以防止死锁。
Ps:中断锁是对获取锁的中断,注意与线程被中断的区别(虽然本质上都是中断线程)。不管是使用显式锁还是内置锁的线程阻塞都是可以被中断的,而中断锁是指线程在获取锁的过程中被阻塞 可以中断现在继续排队获取锁的过程。中断锁:是中断获取锁排队阻塞,可以使用ReentrantLock的tryLock(long timeout, TimeUnit unit)、lockInterruptibly()这两个方法实现,而使用内置锁synchronized 如果线程已经在排队获取锁是无法被中断的;中断线程:是中断线程执行业务方法过程中的阻塞(如sleep阻塞,BlookingQueue阻塞)。
轮询锁
tryLock(long timeout, TimeUnit unit)这个方法延迟获取锁的方法,另外ReentrantLock还有一个非延迟也不阻塞的获取锁方法tryLock(),尝试获取锁,如果没有获取到直接反回false 不阻塞线程:
public boolean tryLock() { //默认只有非公平实现 return sync.nonfairTryAcquire(1); }
ReentrantLock不直接提供轮询锁api,但可以用tryLock(long timeout, TimeUnit unit)和tryLock() 这两个方法实现。即没有获取到锁,可以使用while循环 隔一段时间再次获取,直到获取到为止,这种方式是解决死锁的常用手段。这两个方法的使用区别:tryLock(long timeout, TimeUnit unit)不用在while中sleep,而tryLock()需要自己在while中sleep一会儿,减少资源开销。以tryLock()为例 实现轮询锁:
public class ReentrantLockTest { public static void main(String[] args) throws Exception{ ReentrantLock lock = new ReentrantLock(true); while (true){ if(lock.tryLock()){//这里不阻塞 try{ System.out.println("执行业务方法"); //业务方法 return; }catch (Exception e){ //业务异常 }finally { lock.unlock(); } } //如果没有获取到睡一会儿,再取锁 Thread.sleep(1000); } } }
ReentrantLock中还有一个重要方法newCondition获取“条件队列”方法,其作用类似使用内置锁时Object的wait、notify、notifyAll。这部分内容比较多,在这里就不展开讲解,后面抽时间单独总结下。另外ReentrantLock还有一些其他辅助方法,都比较好理解,就不一一列举,在使用过程中自行查阅即可。
总结
最后再强调下显式锁Lock与内置锁是互补关系,有些场景下只能使用内置锁(比如对方法加锁);有些场景下只能使用Lock(比如 需要防止死锁、或者实现公平锁)。显式锁在java API中常用的还有读写锁ReentrantReadWriteLock,本次主要讲了重入锁ReentrantLock的实现原理和基本用法。
相关推荐
带你看看Javad的锁-ReentrantLock前言ReentrantLock简介Synchronized对比用法源码分析代码结构方法分析SyncNonfairSyncFairSync非公平锁VS公平锁什么是公平非公平ReentrantLockReentrantLock的构造函数lock加锁方法...
Java 多线程与并发(11_26)-JUC锁_ ReentrantLock详解
java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析...
主要介绍了Java多线程之内置锁(synchronized)和显式锁(ReentrantLock)的深入理解新的和用法,具有一定参考价值,需要的朋友可以了解下。
1、ReentrantLock简介 2、ReentrantLock函数列表 3、重入的实现 4、公平锁与非公平锁 5、ReentrantLock 扩展的功能 6
一、显式锁 在类中利用synchronized修饰的方法或者this代码块,均使用的是类的实例锁或者类的锁。这些锁都称为内置锁。 可以利用显式锁进行协调对象的访问。即ReentrantLock。这是一种可以提供无条件,可...
主要介绍了java并发之重入锁-ReentrantLock,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
java中的乐观锁与悲观锁,synchronized与ReentrantLock重入锁的说明与比较
java并发编程1-9,可解压,并发编程必看资料。 1 Java 并发编程实践基础 2 构建线程安全应用程序 3 使用JDK 并发包构建程序 4 使用开源软件 Amino ...7 显示锁 ReentrantLock 8 原子变量与非阻塞算法 9 Java 内存模型
在JAVA中ReentrantLock 和synchronized 都是可重入锁; 重入锁ReentrantLock 相对来说是synchronized、Object.wait()和Object.notify()方法的替代品(或者说是增强版),在JDK5.0的早期版本,重入锁的性能远远好于...
主要介绍了Java并发编程之显示锁ReentrantLock和ReadWriteLock读写锁,本文讲解了ReentrantLock概况、Lock接口、Lock使用、轮询锁的和定时锁、公平性、可中断获锁获取操作等内容,需要的朋友可以参考下
Java多线程并发的程序中使用互斥锁有synchronized和ReentrantLock两种方式,这里我们来详解Java多线程编程中互斥锁ReentrantLock类的用法:
主要介绍了Java多线程 ReentrantLock互斥锁详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。下面我们来深入了解一下它吧
4种常用Java线程锁的特点,性能比较、使用场景 线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发...
公平和非公平选择这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。public void
当然在release()方法中不仅仅只是将state- 1这么简单,- 1之后还需要进行一番处理,如果-1之后的新state = 0,则表示当前锁已经被线程释放
第六章 ReentrantLock源码解析2--释放锁unlock()最常用的方式://注意:通常情况下,这个会设置成一个类变量,比如说Segement中的段锁
Java多线程ReentrantLock1
第五章 ReentrantLock源码解析1--获得非公平锁与公平锁lock()最常用的方式://注意:通常情况下,这个会设置成一个类变量,比如说Segemen