在 Lock 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于,synchronized 关键字来解决。但是 synchronized 在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock的出现可以解决synchronized 在某些场景中的短板,它比 synchronized 更加灵活。
Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现 Lock 接口的类有很多,以下为几个常见的锁实现
void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
void lockInterruptibly() // 和lock()方法相似, 但阻塞的线程可中断,抛出java.lang.InterruptedException 异常
boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回 true
boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
void unlock() // 释放锁
表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。synchronized 和 ReentrantLock 都是可重入锁。
比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。
public class ReentrantDemo{
public synchronized void demo(){
System.out.println("begin:demo");
demo2();
}
public void demo2(){
System.out.println("begin:demo1");
synchronized (this){
}
}
public static void main(String[] args) {
ReentrantDemo rd=new ReentrantDemo();
new Thread(rd::demo).start();
}
}
**ReentrantLock 的使用案例 **
public class AtomicDemo {
private static int count=0;
static Lock lock=new ReentrantLock();
public static void inc(){
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->{AtomicDemo.inc();}).start();
}
Thread.sleep(3000);
System.out.println("result:"+count);
}
}
我们以前理解的锁,基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进
行访问,而读写所在同一时刻可以允许多个线程访问,但是在写线程访问时,所有
的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁;
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读
多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.
public class LockDemo {
static Map cacheMap=new HashMap<>();
static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();
static Lock read=rwl.readLock();
static Lock write=rwl.writeLock();
public static final Object get(String key) {
System.out.println("开始读取数据");
read.lock(); //读锁
try {
return cacheMap.get(key);
}finally {
read.unlock();
}
}
public static final Object put(String key,Object value){
write.lock();
System.out.println("开始写数据");
try{
return cacheMap.put(key,value);
}finally {
write.unlock();
}
}
}
在这个案例中,通过 hashmap 来模拟了一个内存缓存,然后使用读写所来保证这
个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时
候,读锁不会被阻塞,因为读操作不会影响执行结果。
在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线
程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升
读操作的并发性,也保证每次写操作对所有的读写操作的可见性
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它
是一个同步工具也是 Lock 用来实现线程同步的核心组件。
从使用层面来说,AQS 的功能分为两种:独占和共享
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构
都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任
意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线
程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以
后,会从队列中唤醒一个阻塞的节点(线程)
node的组成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TIGDqb8d-1583998874943)(0DF876C1E05D400A8A25B92BB97D17E2)]
head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,
如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下
这个过程也是涉及到两个变化
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,我们
前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备
业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能
Sync 有两个具体的实现类,分别是:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
CAS 的实现原理
通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的
state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返
回 false.
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,
以及涉及到 state 这个属性的意义。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入
锁的实现来说,表示一个同步状态。它有两个含义的表示
AQS.acquire
acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此
时继续 acquire(1)操作
➢ 大家思考一下,acquire 方法中的 1 的参数是用来做什么呢?
这个方法的主要逻辑是
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
NonfairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
ReentrantLock.nofairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前执行的线程
int c = getState();//获得 state 的值
if (c == 0) {//表示无锁状态
if (compareAndSetState(0, acquires)) {//cas 替换 state 的值,cas 成功表示获取锁成功
setExclusiveOwnerThread(current);//保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁
return true;
}
} 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.addWaiter
当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成
Node.
入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状
态。意味着重入锁用到了 AQS 的独占锁功能
//假设有三个线程同时进来(并发的方法)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 Node
Node pred = tail; //tail 是 AQS 中表示同比队列队尾的属性,默认 是 null
if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
if (compareAndSetTail(pred, node)) {//通过 cas 把 node加入到 AQS 队列,也就是设置为 tail
pred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 node
return node;
}
}
enq(node);//tail=null,把 node 添加到同步队列
return node;
}
enq
enq 就是通过自旋操作把当前节点加入到队列中
//假设有三个线程同时进来(并发的方法)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
图解分析
假设 3 个线程来争抢锁,那么截止到 enq 方法运行结束之后,或者调用 addwaiter方法结束后,AQS 中的链表结构图
AQS.acquireQueued
通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给
acquireQueued 方法,去竞争锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取当前节点的 prev 节点
if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁
setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB获得执行权限
p.next = null; //把原 head 节点从链表中移除
failed = false;
return interrupted;
}//ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 false
//如果线程调用了parkAndCheckInterrupt,则会阻塞在这里等待unpark唤醒
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true; //并且返回当前线程在等待过程中有没有中断过。
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire
如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会
失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法
Node 有 5 中状态,分别是:CANCELLED(1),SIGNAL(-1)、CONDITION(- 2)、PROPAGATE(-3)、默认状态(0) - = - CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取
消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状
态后的结点将不会再变化
这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是
否应该被挂起。
//这个方法的意思:判断是否需要挂起,在获取锁失败之后
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//前置节点的 waitStatus
if (ws == Node.SIGNAL)//如果前置节点为 SIGNAL,意味着只需要等待其他前置节点的线程被释放,
return true;//返回 true,意味着可以直接放心的挂起了
if (ws > 0) {//ws 大于 0,意味着 prev 节点取消了排 队,直接移除这个节点就行
do {
node.prev = pred = pred.prev;//相当于: pred=pred.prev; node.prev=pred;
} while (pred.waitStatus > 0); //这里采用循环,从双向列表中移除 CANCELLED 的节点
pred.next = node;
} else {//利用 cas 设置 prev 节点的状态为 SIGNAL(-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt
使用 LockSupport.park 挂起当前线程编程 WATING 状态
Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是
thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识
true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味
着在 acquire 方法中会执行 selfInterrupt()。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一
个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求
的
static void selfInterrupt() {
Thread.currentThread().interrupt();
}