前面在学习synchronized关键字的时候我们了解了synchronized锁和它的膨胀过程,虽然Jdk1.5之后对synchronized进行了大量的优化,但是在使用synchronized过程中依然会存在很多问题。因此在Jdk1.5版本以后添加了显示锁Lock。
目录
显式锁Lock
Lock接口是对锁操作的基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助Lock创建不同的Condtion对象进行多线程间的通信操作。
Lock接口的定义的方法如下所示:
lock():尝试获取锁。如果锁已经被另一个线程持有,那么该线程会进入阻塞状态,直到获取到锁。
lockInterruptibly():尝试获取锁,进入阻塞的线程是可以被中断的,该方法可以获取中断信号。
tryLock():尝试获取锁,调用该方法获取锁无论是否成功都会立即返回,线程不会进入阻塞状态。
boolean tryLock(long time, TimeUnit unit):该方法与tryLock()方法类似,只是多了获取锁时间的限制,如果在限制的时间内没有获取到锁,则结果返回false。
unlock():释放锁,在持有锁的线程运行结束后,应该确保对锁资源的释放。
newCondition():创建一个与Lock相关的Condition对象。
显示锁Lock有很多实现:ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。开发中最常用的实现类是ReentrantLock。
所以接下来我们就开始学习ReentrantLock啦!
ReentrantLock介绍
ReentrantLock是基于AQS同步器构建的锁,只支持独占方式的获取操作,因此它实现了tryAcquire、tryRelease、isHeldExclusively。
ReentrantLock将同步状态state用于保存锁获取操作的次数,并且还维护了一个owner变量用来保存当前所有者线程的标识符。
ReentrantLock的UML图:
ReentrantLock内部类有3个:
-
抽象静态内部类Sync,其继承了AbstractQueuedSynchronizer。
-
静态内部类NonfairSync,其继承了Sync。
-
静态内部类FairSync,其继承了Sync。
ReentrantLock提供了两种锁机制:公平锁和非公平锁。公平锁通过类FairSync提供的方法实现,非公平锁通过NonfairSync提供的方法实现。
接下来我们从源码角度分析这3个内部类的作用。
ReentrantLock内部类分析
Sync是AQS同步器的实现类,其提供了抽象方法lock()由子类实现。其源码解析如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
//抽象方法,获取锁的方法lock()
abstract void lock();
/**
* 非公平锁获取方法
*/
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state同步器状态的值
int c = getState();
//如果state的值为0,说明没有其他线程持有锁
if (c == 0) {
//通过CAS的方式修改state的值,多线程环境下state的值可以提前被其他线程抢占
if (compareAndSetState(0, acquires)) {
//将线程持有标识设置为当前线程对象
setExclusiveOwnerThread(current);
//当前线程获取锁成功返回true
return true;
}
}
//如果当前线程是持有锁的线程
else if (current == getExclusiveOwnerThread()) {
//将获取的state的值+本次获取锁的个数
int nextc = c + acquires;
//如果修改后的数值小于0,则抛出异常
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//如果不小于0,则修改sate的值
setState(nextc);
//当前线程重入成功,返回true
return true;
}
//其他情况均认为当前线程获取锁失败,返回false
return false;
}
//释放锁
protected final boolean tryRelease(int releases) {
//获取state状态值,减去需要释放的锁的个数
int c = getState() - releases;
//获取当前线程,如果当前线程不是锁的持有者,则抛出异常,此步骤保证只有锁的持有者才可以释放锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//定义free变量初始化为false
boolean free = false;
//如果c为0,表示锁已经全部释放
if (c == 0) {
//修改free为true,表示锁已经全部释放成功
free = true;
//设置锁的持有标识为null
setExclusiveOwnerThread(null);
}
//将释放锁之后的值赋值给state
setState(c);
//如果c为0,则返回true,其他情况锁资源没有完全释放,均返回false
return free;
}
//判断当前线程是否持有某个Lock
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
//创建Condition对象
final ConditionObject newCondition() {
return new ConditionObject();
}
//获取锁对象的持有线程
final Thread getOwner() {
//如果state为0表示没有任何线程持有锁资源返回null,state不为0表示有线程持有锁则返回持有锁的线程
return getState() == 0 ? null : getExclusiveOwnerThread();
}
//查询当前线程在某个Lock上的数量,它与monitor计数器的作用是一样的
final int getHoldCount() {
//当前线程是锁的持有者则返回state的值,不是则返回0
return isHeldExclusively() ? getState() : 0;
}
//判断锁对象是否被线程持有
final boolean isLocked() {
return getState() != 0;
}
}
NonfairSync非公平锁内部类,提供非公平的锁竞争方法
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//实现父类Sync的抽象接口
final void lock() {
//通过CAS的方式修改state状态,期望值是0,新值是1
//修改成功,则表示获取锁成功,将当前线程设置为锁持有者
//修改失败,则调用AQS的acqiure方法,将当前线程加入同步队列
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//AQS的子类实现方法tryAcquire,其调用了父类Sync的非公平锁获取方法nonfairTryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
非公平锁加锁流程图:
FairSync公平锁内部类,提供公平的锁竞争方法
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//实现父类Sync的抽象接口
final void lock() {
//调用AQS的acqiure方法,将当前线程加入同步队列
acquire(1);
}
//AQS的子类实现方法tryAcquire,它没有调用父类Sync的方法,而是自己实现了tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//获取当前线程对象
final Thread current = Thread.currentThread();
//获取同步器状态值
int c = getState();
//如果c为0,表示锁资源没有被其他线程持有
if (c == 0) {
//判断同步队列中的当前线程节点是head的下一个节点
//如果是则通过CAS的方式修改state的值,修改成功将当前线程设置为锁的持有者
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
//当前线程获取锁成功返回true
return true;
}
}
//如果当前线程是锁的持有者,则将状态的值+此次需要获取的锁的个数将其赋值给state
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
//当前线程获取锁成功返回true
return true;
}
//其他情况锁获取失败,返回false
return false;
}
}
//AQS方法
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
//如果头节点和尾节点相等,则返回false,表示同步队列中只有一个节点是当前节点
//如果不相等,那么如果头节点的next不等于null并且next的节点线程是当前节点返回false
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
公平锁加锁流程图:
ReentrantLock用法
我们先看下ReentrantLock的构造函数,ReentrantLock提供了两个构造函数,可以指定锁获取的方式:公平和非公平。
默认情况下为非公平锁。
public class ReentrantLock implements Lock, java.io.Serializable {
/** 内部类Sync对象 */
private final Sync sync;
/**
* 无参构造,默认是非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 有参构造,可以指定公平锁和非公共锁
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
可以通过不同的构造方法创建ReentrantLock对象,从而实现不同的加锁和解锁操作。
private final ReentrantLock lock = new ReentrantLock();
public void fun() {
lock.lock();
try {
//同步代码逻辑
}finally {
lock.unlock();
}
}
熟悉了ReentrantLock用法之后,我们通过一段代码再熟悉下ReentrantLock的特点。
public class ReentrantLockTest {
private final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
ReentrantLockTest test = new ReentrantLockTest();
test.creatThread().start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//main线程不能释放其他线程加的锁
//test.lock.unlock();
test.lock.lock();
System.out.println("main线程计数器=" + test.lock.getHoldCount());
test.lock.unlock();
}
public Thread creatThread() {
return new Thread(()->{
lock.lock();
try {
System.out.println("计数器=" + lock.getHoldCount());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//加两次锁,只释放一次,会一直阻塞
// lock.lock();
} finally {
lock.unlock();
}
});
}
}
代码中注释了两行,大家可以去掉注释查看不同的运行效果。
ReentrantLock的用法比较简单,在使用的时候需注意加锁几次就要解锁几次,否则可能出现锁资源无法释放导致一直阻塞的情况。
锁的释放写在finally语句块中保证锁资源一定释放。
只有获取锁的线程才能释放锁资源,没有持有锁的线程释放锁资源抛出IllegalMonitorStateException异常。
ReentrantLock是独占锁,main线程只有在thread线程释放锁资源后才能获取到锁。
ReentrantLock总结
ReentrantLock与synchronized的区别:
-
特点:synchronized是独占可重入锁,是非公平的竞争锁方式。ReentrantLock也是独占可重入锁,但是其可以指定为公平锁,默认是非公平锁。
-
用法:synchronized可以修饰方法和代码块,不需要显示的加锁和解锁。ReentrantLock修饰代码块,在lock()和unlock()方法中间的代码都是同步代码,需要显示的加锁和解锁,将锁的控制权交给了开发人员。
-
性能:基于JVM对关键字的支持,单线程下synchronized关键字性能要优于ReentrantLock,但是多线程环境下ReentrantLock性能优于synchronized。
-
高级特性:获取synchronized锁失败的线程会一直阻塞直到获取到锁,不能中断。ReentrantLock提供了可中断获取锁的方法lockInterruptibly(),而且还提供了获取锁失败不阻塞立即返回的方法tryLock(),如果开发场景中涉及到了高级应用,那就只能选择显示锁Lock了。
公平锁与非公平锁的区别:
-
公平锁:当一个线程尝试获取锁的时候先加入同步队列,如果是下一个需要唤醒的节点则去竞争锁,锁的竞争是先到先得,保证了公平性,但是锁的竞争效率会变低。
-
非公平锁:当一个线程尝试获取锁的时候优先尝试获取锁,如果获取失败再加入同步队列,提高了锁竞争的性能,但是会出现同步队列中的线程一直获取不到锁的现象,称为饥饿现象。
我是勾勾,一直在努力的程序媛!感谢您的点赞、转发和关注!
参考文档:
《Java并发编程实战》
《Java高并发编程详解》