今天学习的是可重入锁。
参考资料:https://mp.weixin.qq.com/s/GDno-X1N8zc98h9MZ8_KoA
一、什么是锁
使用Java进行多线程开发,使用锁是一个不可避免的问题
(1)锁的含义
- 在计算机科学中,锁是一种同步机制,在有许多执行线程的环境中强制对资源的访问控制。
- 锁旨在强制实施互斥排它,并发控制策略。
我的理解,锁如同字面含义的“锁”。我进了一个房间,把门锁上,不让其他人进来。在Java中,我(某一线程)在做某个事情(执行某方法)时加锁,只允许它自己执行该方法,别人不允许插队进来。
(2)为什么要使用锁
- 场景
有一个User对象,包含一下两个属性。有一个线程A进入,对该对象进行写操作。为username赋完值,还没来得及对password进行操作。此时,线程B进入,直接读这个对象,此时,它只读了半个数据。线程B认为该对象的手机号的不存在。
public class User{
private String username;
private String password;
}
- 出现原因
由于写用户名和写密码不是原子操作 - 解决方案
写操作前,加锁,不允许其他线程使用该资源。
写完数据,释放锁。
读线程加锁,不允许其他线程操作
读完数据,释放锁。
二、什么是可重入锁
- 如果一个线程中,连续两次对同一把锁进行lock(这里,我不是很清楚。意思是一个线程,多次加lock?),这个线程就会被卡死
- 实际开发中,一个线程调用多个方法,可能出现方法的嵌套。如主方法调用methodA(),methodA()调用methodB(),若是mainA()中存在lock,methodB()也存在lock,线程就会卡死。
- 可重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,不会导致线程卡死。唯一需要保证的是lock()的次数和unlock()的次数一致
三、Java中的重入锁
-
所在包
java.util.concurrent.locks的ReentrantLock

-
主要方法
void lock()
加锁,如果锁已经被占用了,就无限等待。它提供了锁最基本的功能,拿到就返回,拿不到就等待。
boolean tryLock(long timeout,TimeUnti unit) throws InterruptedException
尝试获取锁,等待timeout时间,可以响应中断(程序收到关机信号,中断就会触发,进入中断异常,线程可以做一些清理工作)。
它的好处:
- 可以不用进行无线等待,如果一段时间拿不到锁,可以直接放弃,同时释放自己已经得到的资源。避免死锁的产生。
- 可以在应用层进行自旋,自己决定尝试几次,或者放弃
- 可以响应中断,进入中断异常后,线程可以做一些清理工作,防止终止线程时出现数据写坏,数据丢失等异常情况。
public boolean trylock()
不带参数的trylock()方法更加直接,如果能获取到锁,直接返回true;获取失败,返回false。特别适合在应用层自己对锁进行管理,在应用层自旋等待。
关于自旋
自旋锁是指一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能被成功获取,直到获取到锁才会退出循环。
void unlock() 释放锁
四、重入锁的实现原理
- ReentrantLock实现了Lock接口
- 重入锁的功能由它的内部类Sync实现,根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。(后期将学习策略模式)
(1)重入锁的实现
- AbstractQueuedSynchronizer对象中存在一个变量state。重入锁的实现,就是基于这个状态变量。它(state)的值表示当前线程重复占用这个锁的次数。
0表示锁是空闲的
private volatile int state; - Sync中的方法
final void lock() {
//compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否则,说明别的线程已经使用了这个锁,那么就需要等待
acquire(1);
}
- 关于acquire()方法,它也是在AbstractQueuedSynchronizer中。若是详细学,还可以将if()中涉及的方法理解。
public final void acquire(int arg) {
//1.当tryAcquire()为true,表示获取锁成功,更新state的值。不再执行&&后的相关操作
//2.tryAcquire()为false(表示获取锁失败),执行&&后面的操作。入队等待,直到获取锁,如果在等待过程中,被中断了,那么重新把标志位设置上。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
(2)公平锁的实现
默认情况下,重入锁是不公平的
- 公平与非公平
如果有1,2,3,4四个线程,按顺序,依次请求锁。
若是非公平,则拿到锁的线程是随机的;若是公平,则按照顺序获得锁。 - 公平锁的声明
ReentrantLock fairLock = new ReentrantLock(true);
新建重入锁时,传入参数,表示它是一个公平锁;默认不传参,即是非公平锁。使用公平锁对性能有影响 - 公平锁与非公平锁实现的区别
如果,非公平锁第一次争抢失败,后面的操作和公平锁一样,都是入队等待
//非公平锁
final void lock() {
//直接抢占资源,抢不到(else)则入队等待
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//公平锁,直接入队等待
final void lock() {
acquire(1);
}
tryLock()的实现
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//直接抢资源
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程占用了锁,更新一下state,表示重复占用的次数,这是重入的关键
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;
}
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//先看看有没有在等待,没有人等我才会去抢,有人在我前面,我就不抢了。
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;
}
五、Condition
(1)含义
可以理解为重入锁的伴生对象,它提供了在重入锁的基础上,进行等待和通知的机制。
可以使用newCondition()方法生成一个Condition对象。
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
(2)使用场景
java.util.concurrent包下的ArrayBlockingQueue,它是一个队列,和普通队列一样,有入队(enqueue(E x))和出队(take())的操作。
但是,有个条件,如果队列是空的,那么take就需要等待,一直到有元素了,再返回。
(3)使用方式,结合ArrayBlockingQueue
首先,初始化,ArrayBlockingQueue中的构造方法中维护了一个Condition对象:notEmpty,它用来通知其他线程,队列是否是空着的。
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
当我们需要从队列中拿出一个元素时
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果队列长度为0,那么就在notEmpty condition上等待,一直到有元素进来为止。
//注意,await()方法,一定是要先获取Condition伴生的那个lock。
while (count == 0)
notEmpty.await();
return dequeue(); //一旦队列中有数据,count不为0,就返回出队
} finally {
lock.unlock();
}
}
当有元素入队时
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
//先拿到锁,拿到锁才能操作对应的Condition对象。
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
//入队,在这个函数中,就会有notEmpty的通知,通知相关线程,有数据准备好了。
enqueue(e);
return true;
} finally {
//释放锁
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//发出一个信号量,元素已经放好了,通知那个拿东西的人吧
notEmpty.signal();
}
整个流程图

六、重入锁的使用示例
public class Counter {
//重入锁
private final Lock lock = new ReentrantLock();
private int count;
private void incr(){
//访问count时,需要加锁。
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
private int getCount(){
//读取数据时也要加锁,才能保证数据的可见性
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
}
}
七、总结
- 对于同一个线程,重入锁允许你反复获得同一把锁,但是,申请和释放锁的次数要一致。
- 默认情况下,重入锁是非公平的,公平的重入锁性能差于非公平的。
- 重入锁的内部实现是基于CAS操作的
- 重入锁的伴生对象Condition提供了await()和singal()的功能,可用于线程间消息通信。
小记
本打算每日一篇。但是一个知识点连带的其他知识点想要搞清楚,还是有点复杂,我不想糊弄自己,还是希望多花时间理解清楚。
涉及源码,根据不同阶段自己的水平慢慢理解。
学习不停,积累不停,你会走得更远~
可重入锁&spm=1001.2101.3001.5002&articleId=114855819&d=1&t=3&u=e4a95a58c9154bcfa17e9df627b8bfcf)
149

被折叠的 条评论
为什么被折叠?



