ReentrantLock
ReentrantLock与AQS的关系
AbstractOwnableSynchronizer
:抽象类,定义了存储独占当前锁的线程
和获取
的方法。AbstractQueuedSynchronizer
:抽象类,AQS框架核心类,其内部以虚拟队列
的方式,管理线程的锁获取与锁释放,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑,目的是使开发人员可以自由定义,获取锁以及释放锁的方式。Node
:AbstractQueuedSynchronizer的内部类,用于构建虚拟队列(链表双向链表),管理需要获取锁的线程。Sync
:抽象类,是ReentrantLock的内部类
,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现。NonfairSync
:是ReentrantLock的内部类
,继承自Sync
,非公平锁的实现类。FairSync
:是ReentrantLock的内部类
,继承自Sync,公平锁的实现类。ReentrantLock
:实现了Lock接口的,其内部类有Sync
、NonfairSync
、FairSync
,在创建时可以根据fair
参数决定创建NonfairSync(默认非公平锁
)还是FairSync。
ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中Sync继承自AQS实现了解锁tryRelease()
方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()
方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。
AQS是一个抽象类,但其源码中并没一个抽象的方法,这是因为AQS只是作为一个基础组件,并不希望直接作为直接操作类对外输出,而更倾向于作为基础组件,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等。
从设计模式角度来看,AQS采用的模板模式
的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作。
为什么这么做?
这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式
与独占模式
,而这两种模式的加锁与解锁实现方式是不一样的
,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用。
也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁
、解锁
的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可。
可重入锁
ReentrantLock意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁,它是Lock接口的一个重要实现类!
package testlock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 验证ReentrantLock的可重入性
* @author chenzufeng
* @date 2021-1-29
*/
public class ReEnterLock implements Runnable {
ReentrantLock lock = new ReentrantLock();
static int i = 0;
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
lock.lock();
// 可重入
lock.lock();
try {
i++;
} finally {
// 执行两次解锁
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReEnterLock enterLock = new ReEnterLock();
Thread thread1 = new Thread(enterLock, "Thread1");
Thread thread2 = new Thread(enterLock, "Thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i); // 2000
}
}
公平锁与非公平锁
ReentrantLock里面有一个内部类Sync
,Sync继承AQS(AbstractQueuedSynchronizer)(abstract static class Sync extends AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync
和非公平锁NonfairSync
两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显式地指定使用公平锁。
公平锁与非公平锁的加锁方法的源码:
公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
}
h != t && ((s = h.next) == null || s.thread != Thread.currentThread())
双向链表中,第一个节点为虚节点,其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的。
当h != t时:
- 如果(s = h.next) == null,等待队列正在有线程进行初始化,但只是进行到了Tail指向Head,没有将Head指向Tail,此时队列中有元素,需要返回True。
- 如果(s = h.next) != null,说明此时队列中至少有一个有效节点。
- 如果此时s.thread == Thread.currentThread(),说明等待队列的第一个有效节点中的线程与当前线程相同,那么当前线程是可以获取资源的;
- 如果s.thread != Thread.currentThread(),说明等待队列的第一个有效节点线程与当前线程不同,当前线程必须加入进同步队列。
hasQueuedPredecessors是公平锁加锁时判断同步队列中是否存在有效节点的方法
- 如果返回False,说明当前线程可以争取共享资源;
- 如果返回True,说明队列中存在有效节点,当前线程必须加入到同步队列中。
公平锁与非公平锁实现原理
综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁(不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源),所以存在后申请却先获得锁的情况。
为什么有公平锁与非公平锁设计
- 恢复挂起的线程到真正锁的获取还是有时间差的,
非公平锁
能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
。 - 使用多线程很重要的考量点是线程切换的开销。如果采用
非公平锁
,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程,在此刻再次获取同步状态的几率就变得非常大
,所以就减少了线程的开销
。如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间。 - 公平锁保证了排队的公平性,非公平锁有可能导致排队的长时间在排队,也没有机会获取到锁。
tryLock(time, unit)实例
synchronized
是不占用到手不罢休的,会一直试图占用下去。与synchronized
的钻牛角尖不一样,Lock
接口还提供了一个tryLock
方法。tryLock
会在指定时间范围内试图占用。如果时间到了,还占用不成功,就不会一直等下去。
注意: 因为使用tryLock
有可能成功,有可能失败,所以后面unlock
释放锁的时候,需要判断是否占用成功了,如果没占用成功也unlock
,就会抛出异常。
package testlock;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* tryLock会在指定时间范围内试图占用。
* 如果时间到了,还占用不成功,就不会一直等下去。
* @author chenzufeng
* @date 2021-1-28
*/
public class TestTryLock {
public static String currentTime() {
return new SimpleDateFormat("HH:mm:ss").format(new Date());
}
public static void log(String message) {
System.out.printf("%s %s %s\n", currentTime(), Thread.currentThread().getName(), message);
}
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(
() -> {
// 因为使用tryLock有可能成功,有可能失败,
// 所以后面unlock释放锁的时候,需要判断是否占用成功了
boolean locked = false;
try {
log(Thread.currentThread().getName() + " 线程启动!");
log(Thread.currentThread().getName() + " 试图占有锁!");
locked = lock.tryLock(1, TimeUnit.SECONDS);
if (locked) {
log(Thread.currentThread().getName() + " 占有了锁!");
log(Thread.currentThread().getName() + " 正完成时长为5秒的业务!");
TimeUnit.SECONDS.sleep(5);
} else {
log(Thread.currentThread().getName() + " 经过1秒的努力,尚未占有锁,弃之!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// unlock释放锁的时候,需要判断是否占用成功了
if (locked) {
log(Thread.currentThread().getName() + " 释放锁!");
lock.unlock();
}
}
log(Thread.currentThread().getName() + " 线程结束!\n");
}, "Thread1"
).start();
// 先让thread1启动
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(
() -> {
boolean locked = false;
try {
System.out.println();
log(Thread.currentThread().getName() + " 线程启动!");
log(Thread.currentThread().getName() + " 试图占有锁!");
locked = lock.tryLock(1, TimeUnit.SECONDS);
if (locked) {
log(Thread.currentThread().getName() + " 占有了锁!");
log(Thread.currentThread().getName() + " 正完成时长为5秒的业务!");
TimeUnit.SECONDS.sleep(5);
} else {
log(Thread.currentThread().getName() + " 经过1秒的努力,尚未占有锁,弃之!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// unlock释放锁的时候,需要判断是否占用成功了
if (locked) {
log(Thread.currentThread().getName() + " 释放锁!");
lock.unlock();
}
}
log(Thread.currentThread().getName() + " 线程结束!\n");
}, "Thread2"
).start();
}
}
输出结果:
00:30:44 Thread1 Thread1 线程启动!
00:30:44 Thread1 Thread1 试图占有锁!
00:30:44 Thread1 Thread1 占有了锁!
00:30:44 Thread1 Thread1 正完成时长为5秒的业务!
00:30:45 Thread2 Thread2 线程启动!
00:30:45 Thread2 Thread2 试图占有锁!
00:30:46 Thread2 Thread2 经过1秒的努力,尚未占有锁,弃之!
00:30:46 Thread2 Thread2 线程结束!
00:30:49 Thread1 Thread1 释放锁!
00:30:49 Thread1 Thread1 线程结束!
ReentrantLock与synchronized
ReentrantLock | synchronized | |
---|---|---|
锁实现机制 | AQS | 监视器 |
特点 | 支持被中断获取锁、超时获取锁、尝试获取锁 | 使用不灵活 |
释放形式 | 显式调用unlock() 释放锁 |
自动释放监视器 |
锁类型 | 公平锁、非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
可重入性 | 可重入 | 可重入 |
性能 | 多线程条件下,读写操作性能更强 | 得益于优化,单线程读 操作,性能高 |
// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
使用synchronized买票
package testlock;
/**
* 使用synchronized关键字买票
* @author chenzufeng
* @date 2021-1-28
*/
public class TicketSync {
public static void main(String[] args) {
// 并发:多线程操作同一个资源类,把资源类丢入线程
Ticket ticket = new Ticket();
new Thread(
() -> {
for (int i = 0; i < 51; i++) {
ticket.saleTicket();
}
}, "Thread1"
).start();
new Thread(
() -> {
for (int i = 0; i < 51; i++) {
ticket.saleTicket();
}
}, "Thread2"
).start();
}
}
/**
* 资源类
*/
class Ticket {
private int number = 100;
public synchronized void saleTicket() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() +
" 买到了第 " + number-- + " 张票!");
}
}
}
输出结果:
Thread1 买到了第 100 张票!
Thread1 买到了第 99 张票!
Thread2 买到了第 98 张票!
Thread2 买到了第 97 张票!
...
Thread2 买到了第 75 张票!
Thread1 买到了第 74 张票!
...
Thread2 买到了第 1 张票!
Reetrantlock买票
package testlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用ReentrantLock买票
* @author chenzufeng
* @date 2021-1-28
*/
public class TicketReentrantLock {
public static void main(String[] args) {
BusTicket busTicket = new BusTicket();
new Thread(
() -> {
for (int i = 0; i < 51; i++) {
try {
busTicket.saleBusTicket();
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread1"
).start();
new Thread(
() -> {
for (int i = 0; i < 51; i++) {
try {
busTicket.saleBusTicket();
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Thread2"
).start();
}
}
class BusTicket {
private int number = 100;
/*
* Lock三部曲
* 1. new ReentrantLock(); // 建锁
* 2. lock.lock(); // 加锁
* 3. finally => lock.unlock(); // 解锁
*/
Lock lock = new ReentrantLock(); // 建锁
public void saleBusTicket() {
lock.lock(); // 加锁
try {
// 业务代码
if (number > 0) {
System.out.println(Thread.currentThread().getName() +
" 买到了第 " + number-- + " 张票!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); // 解锁
}
}
}
输出结果:
Thread1 买到了第 100 张票!
Thread2 买到了第 99 张票!
Thread2 买到了第 98 张票!
Thread1 买到了第 97 张票!
...
Thread1 买到了第 4 张票!
Thread2 买到了第 3 张票!
Thread1 买到了第 2 张票!
Thread2 买到了第 1 张票!