目录
4.3.2 原子包 java.util.concurrent.atomic(锁自旋)
一、介绍
ReentrantLock是从jdk1.5定义的可重入的独享锁,其拥有比较灵活的使用方式,提供了公平锁和非公平锁功能,内部是基于乐观锁思想实现的争抢策略;主要依托AQS提供的模板方法,实现其具有自身特色的锁功能;深入学习ReentrantLock内部实现,有助于提升编码能力,提升编程思想,可谓说是益处多多。
二、对比synchronized
功能描述 | ReentrantLock | Synchronized |
可重入锁 | 支持 | 支持 |
公平锁 | 支持 | 不支持 |
非公平锁 | 支持 | 支持 |
获取、释放锁顺序 | 无序 | 有序 |
获取锁方式 | 获取锁 尝试获取非阻塞 获取可被中断的锁 尝试可超时的锁的 | 获取锁 |
同一个线程,获取、释放锁顺序不同:
- synchronized获取、释放锁是有顺序的,获取节点A的锁,然后获取节点B的锁,先释放节点B的锁,再释放节点A的锁。
- ReentrantLock获取、释放锁是无序的,获取节点A的锁,然后获取节点B的锁,可不分先后分别释放节点A、B的锁。
获取锁方式:
ReentrantLock相比于synchronized增加了获取锁的方式,优化线程资源占用,通俗讲,在实际引用场景中,被锁的程序段需要一定的执行时间,而每个线程都需要排队执行此段代码,造成线程占用。举个极端例子,“A段代码”的执行时间为1s,在1s内有500次请求,即开启了500个线程,比如我们的容器Tomcat最大只开启了500个线程,那么“其他方法”将没有线程可执行,需要等待“A段代码”的线程释放,此场景我们需要平衡“A段代码”的线程使用,因此增加了其他获取锁的方式:
lockInterruptibly():获取锁,除非当前线程被中断。
tryLock():当此段代码未被锁时,才会获取锁。不会等待获取锁,不会有队列保持公平,如果一定要划分公平与非公平,那么属于非公平锁。
tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲且当前线程未中断,则获取锁。
三、示例代码
ReentrantLock应用场景
1 同步执行
和synchronized差不多,如果已经有线程占用,则进行排队等待
适用于资源的争抢,比如文件操作,同步消息的发送
@Slf4j
public class Reentrant01 {
//公平锁 默认非公平锁
private ReentrantLock lock = new ReentrantLock(true);
public void run() {
try {
// 阻塞获取锁
lock.lock();
// 模拟操作
log.info(Thread.currentThread().getName() + " get lock");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Reentrant02 lock = new Reentrant02();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lock.run();
}, "t" + i).start();
}
}
}
当前线程释放后 其他线程才能获取到锁
2 尝试执行
如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
@Slf4j
public class Reentrant02 {
private ReentrantLock lock = new ReentrantLock();
public void run() {
if (lock.tryLock()) {
try {
// 模拟操作
log.info(Thread.currentThread().getName() + " get lock");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
} else {
log.info(Thread.currentThread().getName() + " abort");
}
}
public static void main(String[] args) {
Reentrant02 reentrant02 = new Reentrant02();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
reentrant02.run();
}, "t" + i).start();
}
}
}
执行结果:
17:22:21.134 [t2] INFO test.Reentrant02 - t2 abort
17:22:21.134 [t4] INFO test.Reentrant02 - t4 abort
17:22:21.134 [t7] INFO test.Reentrant02 - t7 abort
17:22:21.134 [t9] INFO test.Reentrant02 - t9 abort
17:22:21.134 [t1] INFO test.Reentrant02 - t1 abort
17:22:21.134 [t8] INFO test.Reentrant02 - t8 abort
17:22:21.134 [t3] INFO test.Reentrant02 - t3 abort
17:22:21.134 [t6] INFO test.Reentrant02 - t6 abort
17:22:21.134 [t0] INFO test.Reentrant02 - t0 get lock
17:22:21.134 [t5] INFO test.Reentrant02 - t5 abort
具体场景:
• 用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
• 用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。
3 尝试等待执行
获取锁如果被占用等待一段时间
@Slf4j
public class Reentrant03 {
private ReentrantLock lock = new ReentrantLock();
public void run() {
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
// 模拟操作
try {
log.info(Thread.currentThread().getName() + " do work");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
log.info(Thread.currentThread().getName() + " abort");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Reentrant03 lock = new Reentrant03();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lock.run();
}, "t" + i).start();
}
}
}
执行结果:
17:30:07.992 [t1] INFO test.Reentrant03 - t1 do work
17:30:10.988 [t5] INFO test.Reentrant03 - t5 abort
17:30:10.988 [t0] INFO test.Reentrant03 - t0 abort
17:30:10.990 [t6] INFO test.Reentrant03 - t6 abort
17:30:10.992 [t2] INFO test.Reentrant03 - t2 abort
17:30:10.992 [t3] INFO test.Reentrant03 - t3 abort
17:30:10.992 [t4] INFO test.Reentrant03 - t4 abort
17:30:10.992 [t7] INFO test.Reentrant03 - t7 abort
17:30:10.992 [t9] INFO test.Reentrant03 - t9 abort
17:30:10.992 [t8] INFO test.Reentrant03 - t8 abort
4 可中断执行
当正在进行的操作发生中断时,释放锁,不影响其他线程锁的获取
@Slf4j
public class Reentrant04 {
private ReentrantLock lock = new ReentrantLock();
public void run() {
try {
lock.lockInterruptibly();
// 模拟操作
try {
// t3线程报中断
if ("t3".equals(Thread.currentThread().getName())) {
throw new InterruptedException();
}
log.info(Thread.currentThread().getName() + " do work");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Reentrant04 reentrant01 = new Reentrant04();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
reentrant01.run();
}, "t" + i).start();
}
}
}
运行结果:
16:09:48.180 [t5] INFO test.Reentrant04 - t5 do work
16:09:49.185 [t1] INFO test.Reentrant04 - t1 do work
16:09:50.185 [t6] INFO test.Reentrant04 - t6 do work
16:09:51.186 [t7] INFO test.Reentrant04 - t7 do work
16:09:52.186 [t2] INFO test.Reentrant04 - t2 do work
java.lang.InterruptedException
at test.Reentrant04.run(Reentrant04.java:19)
at test.Reentrant04.lambda$main$0(Reentrant04.java:37)
at java.lang.Thread.run(Thread.java:748)
16:09:53.187 [t4] INFO test.Reentrant04 - t4 do work
16:09:54.188 [t8] INFO test.Reentrant04 - t8 do work
16:09:55.188 [t9] INFO test.Reentrant04 - t9 do work
16:09:56.189 [t0] INFO test.Reentrant04 - t0 do work
t3线程产生中断,释放锁后其他线程一样可以获取锁执行不影响锁的获取和释放
5 可重入
可以对同一资源多次加锁解锁,lock和unlock配对使用,一层一层加锁后需要一层一层释放
@Slf4j
public class Reentrant05 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
log.info(Thread.currentThread().getName() + "=====外层=====");
lock.lock();
try {
log.info(Thread.currentThread().getName() + "=====内层=====");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
log.info(Thread.currentThread().getName() + "=== 调用");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t2").start();
}
}
运行结果:
15:55:22.361 [t2] INFO test.Reentrant05 - t2=== 调用
15:55:22.365 [t1] INFO test.Reentrant05 - t1=====外层=====
15:55:22.365 [t1] INFO test.Reentrant05 - t1=====内层=====
如果t1线程中lock()次数>unlock()次数,t2线程不执行;如果lock()次数<unlock()次数,t1线程抛异常
四、源码解读
4.1 ReentrantLock及Lock
4.1.1 ReentrantLock类关系
-
ReentrantLock类里面有一个属性类是Sync(同步器)
-
NonfairSync和FairSync继承了Sync类,这是一个抽象的静态类
-
Sync继承了AbstractQueuedSynchronizer(抽象队列同步器)。
-
ReentrantLock有两个构造函数,一个无参,一个有参。
/**
ReentrantLock继承了Lock锁
*/
public class ReentrantLock implements Lock, java.io.Serializable {
/** Synchronizer providing all implementation mechanics 同步器提供所有实现机制*/
private final Sync sync;
/** Sync同步器是此锁的同步控制基础。派生为下面的公平和非公平版本。使用AQS状态表示锁上的持有数 */
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
/**
* Sync object for non-fair locks 非公平锁
*/
static final class NonfairSync extends Sync {
...
}
/**
* Sync object for fair locks 公平锁
*/
static final class FairSync extends Sync {
...
}
/**ReentrantLock默认使用非公平锁*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**通过指定boolean类型的参数可以设置使用公平锁或者非公平锁*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
4.1.2 ReentrantLock父类Lock以及方法
-
ReentrantLock的父类Lock是一个接口
-
获取锁和释放锁都是显式的,通过代码写出来的,而synchronized是隐式的
-
Lock接口里面包含了如下几个方法:
public interface Lock {
void lock();//获取锁(如获取不到,则等待,不能中断)
void lockInterruptibly() throws InterruptedException;//去获取锁(如获取不到一直等待,除非中断)
boolean tryLock(); //尝试获取锁(获取到返回true,获取不到返回false,不会等待)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //尝试获取锁,获取不到,则等待指定的时间
void unlock(); //释放锁
}
1、lock()
-
获取锁的方法
-
如果锁不可用,那么当前线程将被禁用以实现线程调度,并处于休眠状态,直到获得锁。(如果当前锁已经被其他锁获取,则等待)
-
不会自动释放锁,必须显式的手动释放锁,否则可能因为某些异常的发生,直接跳过unlock,则导致其他线程都不能获取锁(synchronized遇见异常自动释放锁),try代码块中写操作资源,处理业务,可能加一个catch捕获一下异常,但是一定要有finally代码块中去释放锁
-
lock()体现在公平锁和非公平锁中
非公平锁NonfairSync.lock()
首先判断当前state值是否是0,如果是0,修改为1,表示获取到锁;
获取到锁之后,将当前线程设置为独占线程。
如果state值不为0,不做任何操作,表示没有获取锁成功,跳到else,去执行acquire(1)。
final void lock() {
//采取CAS原理进行比较并更新,判断当前state值是否是0,如果是0,修改为1,并且表示获取到锁
if (compareAndSetState(0, 1))
//将当前线程设置为独占线程,exclusiveOwnerThread是AQS里的一个属性,独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
acquire(1)
尝试获取锁失败,并且成功加入到等待队列中挂起,则进行自中断
-
tryAcquire(arg):尝试获取锁
-
addWaiter(Node.EXCLUSIVE):当前线程加入AQS双向链表队列
-
acquireQueued():将加入到队列中的线程挂起
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)
非公平锁的尝试获取锁,不会判断自己是不是队头的节点(hasQueuedPredecessors),直接通过state值判断当前锁占用状态,直接会申请锁。
/**
* 非公平锁的tryAcquire()
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前state值
int c = getState();
//如果当前state值为0,表示当前锁没有线程占用,可以获取锁
if (c == 0) {
//再进行判断state是否为0,如果是,修改成1,并将当前线程设置为独占线程;如果不是,不做操作
if (compareAndSetState(0, acquires)) {
//设置当前线程为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果state不为0,表示当前锁被占用,判断占用锁的线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
//如果是当前线程,将state值加1并更新,表示重入锁的次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//更新state值
return true;
}
//如果以上两点都不满足,返回false,表示获取锁失败
return false;
}
addWaiter(Node mode)
将当前线程加入AQS双向链表队列
/**
* 将新节点和当前线程关联并入队列
* @param mode 节点的模式(独占/共享)
* @return node 新节点
*/
private Node addWaiter(Node mode) {
//初始化节点,设置关联线程和模式
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点应用
Node pred = tail;
//判断尾节点是否为空,不为空的话表示已经初始化过队列了,将新节点设置为尾节点
if (pred != null) {
//pred是初始化好的尾节点,node.prev是新节点的前继节点,将之前的尾节点引用赋值给新节点的前继节点,表示,新节点放到了现在尾节点的位置
node.prev = pred;
//设置新的节点尾尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果尾节点为空,表示还没有初始化过队列,需要进行初始化head并入队新节点
enq(node);
return node;
}
enq(final Node node)
节点为空的时候,初始化队列并加入新节点
/**
* 将节点插入队列,没有初始化时要进行初始化
*/
private Node enq(final Node node) {
//自旋的过程
for (;;) {
//将尾节点赋值给t
Node t = tail;
//如果t为空,表示当前没有初始化过队列,必须进行初始化
if (t == null) {
//将该节点设置为头节点,也是尾节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果不为空,证明有尾节点,队列初始化过了,将该节点设置为尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued(final Node node,int arg)
写入队列后,需要挂起当前线程
如果当前节点的前一个节点为头节点并且已经获取锁,将当前节点设为头节点并且获取锁成功;这时候就不需要挂起;否则获取锁失败之后是否需要将当前线程挂起
Node:上述通过addWaiter(Node mode)加入到等待队列的节点
/**
* 以独占不可中断模式获取已经在队列中的线程。用于条件等待方法和acquire方法
*/
final boolean acquireQueued(final Node node, int arg) {
//failed表示获取锁失败,默认是true,表示默认获取锁失败
boolean failed = true;
try {
//是否中断,默认为false,表示不中断
boolean interrupted = false;
//自旋
for (;;) {
//返回前一个节点,如果为空的话抛出NullPointerException,不为空的时候返回。node为当前这个节点,相当于获取上一个节点引用
final Node p = node.predecessor();
//如果上一个节点为头节点并且尝试获取锁成功
if (p == head && tryAcquire(arg)) {
setHead(node); //将当前节点设置为头节点
p.next = null; // 之前的那个头节点设置为null,等待被垃圾回收
failed = false; //成功获取锁
return interrupted; //返回默认的是否中断,false
}
//如果不是头节点,或者获取锁失败,则会根据上一个节点的 waitStatus 状态来处理节点
if (shouldParkAfterFailedAcquire(p, node ) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(p, node)
shouldParkAfterFailedAcquire(p, node) 返回当前线程是否需要挂起,如果需要则调用 parkAndCheckInterrupt()。
shouldParkAfterFailedAcquire源码解析见AQS
parkAndCheckInterrupt()
他是利用 LockSupport
的 part
方法来挂起当前线程的,直到被唤醒。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
公平锁FairSync.lock()
没有先判断state值,直接到acquire
final void lock() {
acquire(1);
}
/**
尝试获取锁失败并且成功挂起加入队列中的节点
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(int acquires)
公平锁的尝试获取锁,和非公平锁的尝试获取锁的区别在于是否去判断是否是队头的节点hasQueuedPredecessors(),其他的和非公平锁一样。
lock()注意事项:
公平锁和非公平锁的体现在lock()方法中:
非公平锁:先判断state的值,如果state值为0,更新为1,将当前线程设置为独有线程,获取锁成功,否则进入acquire();尝试获取锁的时候,还是先判断状态值state,如果为0,更新为1,设置为独有线程,获取锁成功,如果state不为0,再判断获取锁的线程是否是当前线程,如果是,更新state值,表示重入的次数,以上两种情况都不是,那证明获取锁失败, 返回false。
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
公平锁:不判断state值,直接进入acquire(),尝试获取锁的时候和非公平锁的区别是公平锁先判断当前线程是否在队首,如果在队首,再去判断state值,而非公平锁不判断是否是队首,直接判断state值。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
使用:
MyThread thread = ...; //线程
thread.lock();//线程获取锁
try{
//获取到了被本锁保护的资源,处理任务
//捕获异常
}finally{
thread.unlock(); //释放锁
}
2、tryLock()
-
尝试获取锁,当前锁没有被其他线程获取,则获取成功,返回true,否则返回flase
-
比lock()功能强大,根据是否可以获取锁来继续后续程序的行为
注意:tryLock()是非公平锁中的方法
非公平锁中尝试获取锁nonfairTryAcquire()方法见上述lock()方法中对非公平锁源码得解析
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
使用:只有获取到两把锁才能执行方法体,否则直接释放锁。如果只获取到了lock1,没有获取到lock2,那么也会释放lock1,从而避免了死锁的问题。
public void tryLock(Lock lock1, Lock lock2) throws InterruptedException {
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
System.out.println("获取到了两把锁,完成业务逻辑");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
} else {
Thread.sleep(new Random().nextInt(1000));
}
}
}
3、tryLock(long time,TimeUnit unit)
独占式超时获取锁
-
tryLock() 的重载方法是 tryLock(long time, TimeUnit unit),这个方法和 tryLock() 很类似,区别在于 tryLock(long time, TimeUnit unit) 方法会有一个超时时间,在拿不到锁时会等待一定的时间,如果在时间期限结束后,还获取不到锁,就会返回 false;如果一开始就获取锁或者等待期间内获取到锁,则返回 true
-
这个方法解决了 lock() 方法容易发生死锁的问题,使用 tryLock(long time, TimeUnit unit) 时,在等待了一段指定的超时时间后,线程会主动放弃这把锁的获取,避免永久等待;在等待的期间,也可以随时中断线程,这就避免了死锁的发生
-
tryLock(long timeout, TimeUnit unit)会抛出中断异常
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。tryAcquire我们已经看过,这里重点看一下doAcquireNanos。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//线程中断,抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//返回当前线程是否获取到锁或者是否在指定时间内获取同步状态
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos(arg, nanosTimeout):
在有限的时间内去竞争锁
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果指定的时间已经过去了,直接返回false,没有获取锁
if (nanosTimeout <= 0L)
return false;
//截至时间就是当前的起始时间加上指定的超时时间
final long deadline = System.nanoTime() + nanosTimeout;
//将独占模式的线程入队列生成一个新节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//自旋
for (;;) {
//获取这个节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点为头节点并且获取锁成功,将当前节点设置为头节点
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//更新超时时间,用截至的时间减去当前时间,剩下的就是目前等待超时的时间
nanosTimeout = deadline - System.nanoTime();
//如果已经超时,直接返回false
if (nanosTimeout <= 0L)
return false;
//如果超时时间未到,则需要挂起,并且超时时间要大于1秒,spinForTimeoutThreshold:1000L
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 阻塞当前线程直到超时时间到期
LockSupport.parkNanos(this, nanosTimeout);
//如果线程中断,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireNanos的流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试
4、lockInterruptibly()
-
去获取锁,如果当前锁是可以获得的,这个方法会立刻返回,如果当前锁不能获得,那么该线程开始等待,除非在等待过程中该线程被中断了,否则这个线程会一直等待。
-
永远不会超时
-
lockInterruptibly()本身会抛出一个InterruptedException异常
-
假设我们能够获取到这把锁,和之前一样,就必须要使用 try finally 来保障锁的绝对释放
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
如果线程中断了,抛出InterruptedException(),如果尝试获取锁失败,则进入doAcquireInterruptibly(arg)再次在独占可中断模式下获取
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//线程中断,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取锁失败,进入doAcquireInterruptibly再次在独占模式下获取
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
doAcquireInterruptibly(arg)
在独占可中断模式下获取
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//将独占模式的线程入队列生成一个新节点
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
//又是自旋
for (;;) {
//获取节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点为头节点并且获取锁成功,将当前节点设置为头节点,表示获取锁成功
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
//如果没有获取锁成功,需要挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
使用:lockInterruptibly()本身会抛出一个InterruptedException异常
public void lockInterruptibly() {
try {
lock.lockInterruptibly();//lockInterruptibly()本身抛出的异常,在这里捕获一下
try {
System.out.println("操作资源"); //执行体
} finally {
lock.unlock(); //释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
5、unlock()
-
用于线程解锁,释放锁
-
执行 unlock() 的时候,内部会把锁的“被持有计数器”减 1,直到减到 0 就代表当前这把锁已经完全释放了,如果减 1 后计数器不为 0,说明这把锁之前被“重入”了,那么锁并没有真正释放,仅仅是减少了持有的次数。
public void unlock() {
sync.release(1);
}
release:释放锁,如果尝试释放锁成功,头节点不为空并且头节点的状态不等于0,那么调用unparkSuccessor(h)释放锁成功,返回true;否则返回false,释放锁失败。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(arg):尝试释放锁
先获取当前state值,需要释放当前线程所获取的锁,所以需要保证当前线程是独占线程,也就是持有锁的线程,如果当前线程不是独占线程,则抛出IllegalMonitorStateException();如果state值为0,证明释放锁成功,独占线程置为空,更新state值,表示可重入的次数。
4.2 AQS
AQS(AbstractQueuedSynchronizer )是什么:构建 锁、其他同步器的基础组件;是juc中锁、同步组件实现的公共基础部分抽象的的实现。
AQS能做什么?
1.同步队列的管理和维护;
2.同步状态的管理;
3.线程的阻塞、唤醒的管理。
同步组件:CountDownLatch、Semaphore、CyclicBarrier
J.U.C中的锁:ReentrantLock、ReentrantReadWriteLock、StempedLock
上诉几个类都是基于AQS实现,可见AQS是如何的强大。不得不提一下它的作者:Doug Lea(牛人)。
AbstractQueuedSynchronizer 类内使用模板工厂设计模式,为子类提供了模板方法如下:
/**
* 尝试以独占模式获取。因为一般都是用cas方式,不是一定获取到锁。
* 此方法应查询对象的状态是否允许以独占模式获取该对象,如果允许,则获取该对象。
* 执行获取的线程始终调用此方法。如果此方法报告失败,acquire方法可能会将线程(如果尚未排队)排队,直到其他线程发出释放信号。这可以用来实现方法
*
* @param arg 此值始终是传递给acquire方法的值,或者是保存在Condition 中的值。
* @return 成功后获得
* @throws IllegalMonitorStateException if acquiring would place this
* synchronizer in an illegal state. This exception must be
* thrown in a consistent fashion for synchronization to work
* correctly.
* @throws UnsupportedOperationException 如果不支持独占模式的话,抛出异常
*/
protected boolean tryAcquire(int arg);
/**
* 尝试设置状态,以独占模式释放。
* 执行释放的线程总是调用此方法。
*
* @param arg 释放参数。此值始终是传递给释放方法的值,或者是进入条件等待时的当前状态值。
* @return {@code true} 返回 true时 此对象现在处于完全释放状态,则任何等待的线程都可能尝试获取 否则反之。
*/
protected boolean tryRelease(int arg);
/**
* 执行获取的线程始终调用此方法。
* 如果此方法报告失败,acquire方法可能会将线程(如果尚未排队)排队,直到其他线程发出释放信号。
*
* @return 失败时的负值;如果共享模式采集成功,但后续共享模式采集无法成功,则为零;
* 如果共享模式下的获取成功,则为正值,随后的共享模式获取也可能成功,在这种情况下,后续等待线程必须检查可用性。
* (对三个不同返回值的支持使此方法能够在acquired有时只执行独占操作的上下文中使用。)成功后,此对象已被获取。
*/
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
/**
* 如果同步只针对当前(调用)线程进行,则返回 true。
* 每次调用非等待的ConditionObject方法时都会调用此方法。
* (等待方法调用release。)
* @return 如果同步是以独占方式进行的 返回true
*/
protected boolean isHeldExclusively();
子类内部帮助类可以继承AQS的模板方法,实现具有自己特色功能的类方法。
AQS的成员变量:
jdk1.8版本:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 头节点
private transient volatile Node head;
// 尾节点
private transient volatile Node tail;
// 由字类定义其业务含义,便于实现自定义功能
private volatile int state;
// 自旋锁最大等待时间,单位纳秒
static final long spinForTimeoutThreshold = 1000L;
// 用于CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
// 用于构建等待队列节点
static final class Node {
}
// 与Condition协同使用
public class ConditionObject implements Condition, java.io.Serializable {
}
}
这里成员内部类Node在各个功能中起到了很关键得作用:在了解下起内部得成员类和方法:
// 共享模式得节点
static final Node SHARED = new Node();
// 独占模式得节点
static final Node EXCLUSIVE = null;
/**
* 因为超时或者中断,节点会被设置成取消状态,
* 被取消的节点不会参与到竞争中,
* 会一直是取消状态不会改变;从队列中删除
*/
static final int CANCELLED = 1;
/**
* 后继节点处于等待状态,
* 如果当前节点释放了同步状态或者被取消,
* 会通知后继节点,使其得以运行
*/
static final int SIGNAL = -1;
/**
* 节点在等待条件队列中,节点线程等待在condition上,
* 当其他线程对condition调用了signal后,
* 该节点将会从等待队列中进入同步队列中,获取同步状态
*/
static final int CONDITION = -2;
/**
* 下一次共享式同步状态获取会无条件的传播下去
*/
static final int PROPAGATE = -3;
// 默认为0 根据这个属性值,操作整个队列如何操作
volatile int waitStatus;
// 前驱节点
volatile Node prev;
// 后继节点
volatile Node next;
// 当前此节点封装得线程
volatile Thread thread;
// 下一个条件队列等待节点 ,条件对垒中,后继节点。共享模式
Node nextWaiter;
Node成员方法和构造
static final class Node {
/**
* 如果尾共享模式的等待状态,返回true。其他状态返回false
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
// 用于建立初始头部或共享标记
Node() {
}
// 等待队列使用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Used by Condition
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
/**
*
* 返回前驱节点,或者为空时抛出NPE。
* 使用时,前驱节点不能为空。
* 检测NPE可以省略,交给vm处理
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
Node本身没有做过多的功能,只作为数据模型使用。
稍微了解下AQS模板方法和成员变量,可以揣摩下其设计理念:
1.把竞争得线程及其等待状态,封装尾Node对象;
2.基于CLH实现得FIFO双向队列;
3.AQS通过其成员变量int 类型state表示同步状态;子类可以定义其含义和作用,比如:是否由线程获得锁 锁的重入次数。。。这个具体的含义由字类定义。
4.队列节点的Node线程的唤醒和阻塞:帮随同步队列的维护,使用LockSupport来实现对线程的唤醒和zuse。
除去AQS提供的模板方法,剩下的方法是为了维护队列,及队列Node状态。
4.3 CAS
4.3.1 概念及特性
CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3个参数CAS(V,E,N)。
V表示要更新的变量(内存值),E 表示预期值(旧的),N表示新值。当且仅当V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
4.3.2 原子包 java.util.concurrent.atomic(锁自旋)
JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。相对于对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现。由于一般 CPU 切换时间比 CPU 指令集操作更加长, 所以 JUC 在性能上有了很大的提升。如下代码:
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) { //CAS 自旋,一直尝试,直达成功
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
getAndIncrement 采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而 compareAndSet 利用 JNI 来完成CPU指令的操作。
4.3.3 ABA 问题
CAS会导致“ABA 问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。部分乐观锁的实现是通过版本号(version)的方式来解决 ABA 问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。
五 总结
ReentrantLock是把锁,实现的接口是Lock
分为公平非公平,默认不是公平的
有个变量state,重入次数它记得
每次加锁都加一,解锁成功要减一
变成0后表释放,其他线程可争抢
非公平其实很简单,线程来了先判断
如果可以改变量,加锁成功它真棒
加锁失败入队列,线程挂起不活跃
公平排队获取锁,这很公平要记得
还有api tryLock,立即返回不阻塞
这就是ReentrantLock小总结,点赞转发我先感谢