Java并发学习笔记(八):AQS(AbstractQueuedSynchronizer)、ReentrantLock 原理、读写锁使用和原理

J.U.C

一、AQS 原理

1、概述

AQS 全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架

特点:

  • 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
    • getState - 获取 state 状态
    • setState - 设置 state 状态
    • compareAndSetState - cas 机制设置 state 状态
    • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
  • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet

原理:

  • AQS通过一个int同步状态码,和一个(先进先出)队列来控制多个线程访问资源
  • 支持独占和共享两种模式获取同步状态码
  • 当线程获取同步状态失败会被加入到同步队列中
  • 当线程释放同步状态,会唤醒后继节点来获取同步状态
  • 共享模式下的节点获取到同步状态或者释放同步状态时,不仅会唤醒后继节点,还会向后传播,唤醒所有同步节点
  • 使用volatile关键字保证状态码在线程间的可见性,CAS操作保证修改状态码过程的原子性。

子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

获取锁的方式:

// 如果获取锁失败 
if (!tryAcquire(arg)) {    
    // 入队, 可以选择阻塞当前线程  
    park unpark 
}

释放锁的方式:

// 如果释放锁成功 
if (tryRelease(arg)) {    
	// 让阻塞线程恢复运行 
}
2、使用AQS实现不可重入锁

自定义同步器

/**
 * 自定义锁(不可重入锁)
 */
final class MyLock implements Lock {

    //独占锁,同步器类
    class MySync extends AbstractQueuedSynchronizer{

        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0,1)) {
                //加锁成功,并设置owner为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
           setExclusiveOwnerThread(null);
           setState(0); //state属性是volatile的,它具有写屏障可以保证它之前的操作对其他线程可见
           return true;
        }

        //是否持有独占
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        protected Condition newCondition(){
            return new ConditionObject();
        }
    }

    private MySync sync = new MySync();

    //加锁(不成功会进入等待队列等待)
    @Override
    public void lock() {
        sync.acquire(1);

    }

    //加锁,可打断
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    //尝试加锁(一次)
    @Override
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //有时限的尝试加锁
    @Override
    public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(time));
    }

    //解锁
    @Override
    public void unlock() {
        sync.release(1);
    }

    //新建环境变量
    @NotNull
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

测试:

    public static void main(String[] args) {
        MyLock lock = new MyLock();

        new Thread(()->{
            lock.lock();
            try {
                TheadPrint.print("lock....");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                TheadPrint.print("unlock....");
                lock.unlock();
            }
        }, "t1").start();

        new Thread(()->{
            lock.lock();
            try {
                TheadPrint.print("lock....");
            } finally {
                TheadPrint.print("unlock....");
                lock.unlock();
            }
        }, "t2").start();
    }

结果如下所示,可以看到我们成功是使用MyLock对t1和t2加了,t2在t1解锁之后才继续执行。

14:26:26:201 Thread[t1,5,main]: lock....
14:26:28:203 Thread[t1,5,main]: unlock....
14:26:28:204 Thread[t2,5,main]: lock....
14:26:28:205 Thread[t2,5,main]: unlock....

同时我们实现的是不可重入锁,也就是同一个线程如果重复加锁也会发生阻塞,测试代码如下:

    public static void main(String[] args) {
        MyLock lock = new MyLock();

        new Thread(()->{
            lock.lock();
            TheadPrint.print("lock....");
            lock.lock();
            TheadPrint.print("relock....");

            TheadPrint.print("unlock....");
            lock.unlock();
        }, "t1").start();
    }

结果如下所示,可以看到relock并没有被打印出来,也就是第二次lock.lock();并没加锁成功,而是同样被阻塞了。下面的内容就是看看ReentrantLock 如何实现可重入锁。

14:32:12:728 Thread[t1,5,main]: lock....

二、ReentrantLock 原理

在这里插入图片描述

1、非公平锁实现原理

加锁解锁流程

先从构造器开始看,默认为非公平锁实现

public ReentrantLock() {    
	sync = new NonfairSync(); 
}

NonfairSync 继承自 AQS
没有竞争时,Thread-0可以直接设置exclusiveOwnerThread为自己

在这里插入图片描述

第一个竞争出现时

在这里插入图片描述

Thread-1 执行了

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  • CAS 尝试将 state 由 0 改为 1,结果失败
  • 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
  • 接下来进入 addWaiter 逻辑,构造 Node 队列
    • 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    • Node 的创建是懒惰的
    • 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

在这里插入图片描述

当前线程进入 acquireQueued 逻辑

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
  • 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  • 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false

在这里插入图片描述

  • shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
  • 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
  • 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)

在这里插入图片描述

再次有多个线程经历上述过程竞争失败,变成这个样子

在这里插入图片描述

Thread-0 释放锁,进入 tryRelease 流程,如果成功

  • 设置 exclusiveOwnerThread 为 null
  • state = 0
    在这里插入图片描述
    当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程

找到队列中离 head 近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1

回到 Thread-1 的 acquireQueued 流程

在这里插入图片描述

如果加锁成功(没有竞争),会设置

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收

如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了

在这里插入图片描述

如果不巧又被 Thread-4 占了先

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
2、可重入原理

通过对state的++和–来控制可重入

static final class NonfairSync extends Sync {    
		// ...
	
		// Sync 继承过来的方法, 方便阅读, 放在此处 
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
             // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 
            else if (current == getExclusiveOwnerThread()) {
            	// state++ 
                int nextc = c + acquires;
                if (nextc < 0) // overflow // overflow 
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }	
        
         // Sync 继承过来的方法, 方便阅读, 放在此处 
        protected final boolean tryRelease(int releases) {
            // state-- 
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 支持锁重入, 只有 state 减为 0, 才释放成功 
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
}
3、可打断原理

不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了

// Sync 继承自 AQS 
static final class NonfairSync extends Sync {
    // ... 
    
    private final boolean parkAndCheckInterrupt() {
         // 如果打断标记已经是 true, 则 park 会失效 	
        LockSupport.park(this);
         // interrupted 会清除打断标记
        return Thread.interrupted();
    }

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (; ; ) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null;
                    failed = false;
                    // 还是需要获得锁后, 才能返回打断状态 
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt()) {
                     // 如果是因为 interrupt 被唤醒, 返回打断状态为 true 
                    interrupted = true;
                }
            }
        } finally {
            if (failed) cancelAcquire(node);
        }
    }

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
             // 如果打断状态为 true 
            selfInterrupt();
        }
    }

    static void selfInterrupt() {
         // 重新产生一次中断 
        Thread.currentThread().interrupt();
    }
}

可打断模式

static final class NonfairSync extends Sync {
    public final void acquireInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted()) 
            throw new InterruptedException();
        // 如果没有获得到锁, 进入 ㈠ 
        if (!tryAcquire(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()) {
                    // 在 park 过程中如果被 interrupt 会进入此                    
                    // 这时候抛出异常, 而不会再次进入 for (;;) 
                    throw new InterruptedException();
                }
            }
        } finally {
            if (failed) cancelAcquire(node);
        }
    }
}
4、公平锁实现原理
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    // AQS 继承过来的方法, 方便阅读, 放在此处 
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
            selfInterrupt();
        }
    }

     // 与非公平锁主要区别在于 tryAcquire 方法的实现 
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
            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;
    }
 // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处 
    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
         // h != t 时表示队列中有 Node 
        return h != t && (
            // (s = h.next) == null 表示队列中还有没有老二 
            (s = h.next) == null 
            // 或者队列中老二线程不是此线程 
            || s.thread != Thread.currentThread()
        );
    }
}
5、条件变量实现原理

每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject

await 流程

开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程

        // ㈠ 添加一个 Node 至等待队列 
		private Node addConditionWaiter() {
            Node t = lastWaiter;
             // 所有已取消的 Node 从队列链表删除, 见 ㈡
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
             // 创建一个关联当前线程的新 Node, 添加至队列尾部
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
在这里插入图片描述
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁

在这里插入图片描述

unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功

在这里插入图片描述

park 阻塞 Thread-0

在这里插入图片描述

signal 流程
假设 Thread-1 要来唤醒 Thread-0

在这里插入图片描述

进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node

 // 唤醒 - 将没取消的第一个节点转移至 AQS 队列 
private void doSignal(Node first) {
    do {
         // 已经是尾节点了 
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (
         // 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢ 
        !transferForSignal(first) &&
        // 队列还有节点 
             (first = firstWaiter) != null);
}

在这里插入图片描述
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1

    // 外部类方法, 方便阅读, 放在此处    
    // ㈢ 如果节点状态是取消(任务放弃执行), 返回 false 表示转移失败, 否则转移成功
final boolean transferForSignal(Node node) {
    // 如果状态已经不是 Node.CONDITION, 说明被取消了 
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 加入 AQS 队列尾部 
    Node p = enq(node);
    int ws = p.waitStatus;
    if (// 上一个节点被取消 
        ws > 0 || 
        // 上一个节点不能设置状态为 Node.SIGNAL 
        !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // unpark 取消阻塞, 让线程重新同步状态 
        LockSupport.unpark(node.thread);
    return true;
}

在这里插入图片描述
Thread-1 释放锁,进入 unlock 流程,略

三、读写锁

1、使用

当读操作频率远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发提高性能。 类似于数据库中的 select ... from ... lock in share mode

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

class DataContainer {

    private Object data;

    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    //读锁
    private ReentrantReadWriteLock.ReadLock r = rw.readLock();
    //写锁
    private ReentrantReadWriteLock.WriteLock w = rw.writeLock();

    //读操作
    public Object readData() {
        TheadPrint.print("获取读锁...");
        r.lock();
        try {
            TheadPrint.print("读操作");
            Thread.sleep(1000);
            return data;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            TheadPrint.print("释放读锁...");
            r.unlock();
        }
        return null;
    }

    //写操作
    public void writeData(Object object) {
        TheadPrint.print("获取写锁...");
        w.lock();
        try {
            TheadPrint.print("写操作");
            Thread.sleep(1000);
            data = object;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            TheadPrint.print("释放写锁...");
            w.unlock();
        }
    }
}

测试代码:

public static void main(String[] args) {
    DataContainer dc = new DataContainer();
    new Thread(()->{
        dc.readData();
    }, "t1").start();

    new Thread(()->{
        dc.readData();
    }, "t2").start();
}

从结果可以看出,读-读操作并没有相互阻塞:

18:17:22:222 Thread[t1,5,main]: 获取读锁...
18:17:22:228 Thread[t1,5,main]: 读操作
18:17:22:231 Thread[t2,5,main]: 获取读锁...
18:17:22:231 Thread[t2,5,main]: 读操作
18:17:23:229 Thread[t1,5,main]: 释放读锁...
18:17:23:232 Thread[t2,5,main]: 释放读锁...

如果让t1进行读操作,然后让t2进行写操作,也即是读-写操作,结果如下。可以看出t2等到一秒后t1执行完毕之后才只能获得到锁开始执行:

18:28:31:284 Thread[t1,5,main]: 获取读锁...
18:28:31:286 Thread[t1,5,main]: 读操作
18:28:31:292 Thread[t2,5,main]: 获取写锁...
18:28:32:286 Thread[t1,5,main]: 释放读锁...
18:28:32:286 Thread[t2,5,main]: 写操作
18:28:33:287 Thread[t2,5,main]: 释放写锁...

如果将t1和t2改成写-写操作,则和读-写操作一样,t1仍然需要t2完成之后才能进行操作:

18:29:07:663 Thread[t2,5,main]: 获取写锁...
18:29:07:669 Thread[t2,5,main]: 写操作
18:29:07:696 Thread[t1,5,main]: 获取写锁...
18:29:08:669 Thread[t2,5,main]: 释放写锁...
18:29:08:670 Thread[t1,5,main]: 写操作
18:29:09:672 Thread[t1,5,main]: 释放写锁...
2、注意事项
  • 读锁不支持条件变量
  • 重入时读锁不支持升级为写锁:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
r.lock();
try {
	//已经有读锁,不能升级,写锁永久等待
    w.lock();
    try {
    } finally {
        w.unlock();
    }
} finally {
    r.unlock();
}
  • 重入时写锁支持降级读锁:即持有写锁的情况下去获取读锁
class CachedData {
    Object data;
    data
    // 是否有效,如果失效,需要重新计算 data 
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            // 获取写锁前必须释放读锁 
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新 
                if (!cacheValid) {
                    data = ...;
                    cacheValid = true;
                } 
                // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock();
            }
        } 
        // 自己用完数据, 释放读锁 
        try {
            use(data);
        } finally {
            rwl.readLock().unlock();
        }
    }
}
3、读写锁原理

读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个

3.1 t1 设置写锁(w.lock),t2设置读锁(r.lock )

  • t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位

在这里插入图片描述

  • t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写 锁占据,那么 tryAcquireShared 返回 -1 表示失败

tryAcquireShared 返回值表示

  • -1 表示失败
  • 0 表示成功,但后继节点不会继续唤醒
  • 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
//1、 r.lock
public void lock() {
    sync.acquireShared(1);
}
//2
public final void acquireShared(int arg) {
    //尝试加读锁
    if (tryAcquireShared(arg) < 0)
        //小于零说明加锁失败,下面介绍
        doAcquireShared(arg);
}
//3、尝试加读锁
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    // 如果是其它线程持有写锁, 获取读锁失败
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
    	//低16位不为0,说明加了写锁,而且当前Owner不为自己,则不能加读锁
        return -1;
    int r = sharedCount(c);
    if (
        // 读锁不该阻塞(如果老二是写锁,读锁该阻塞), 并且 
        !readerShouldBlock() &&
        // 小于读锁计数, 并且 
        r < MAX_COUNT &&
        // 尝试增加计数成功 
        compareAndSetState(c, c + SHARED_UNIT)) {
        // ... 省略不重要的代码            
        return 1;
    }
    return fullTryAcquireShared(current);
}

在这里插入图片描述

  • 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
private void doAcquireShared(int arg) {
    // 将当前线程关联到一个 Node 对象上, 模式为共享模式 
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // 再一次尝试获取读锁 
                int r = tryAcquireShared(arg);
                // 如果成功 
                if (r >= 0) {
                    // ㈠                       
                    // r 表示可用资源数, 在这里总是 1 允许传播                       
                    //(唤醒 AQS 中下一个 Share 节点) 
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if ( // 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL) 
                shouldParkAfterFailedAcquire(p, node) &&
                // park 当前线程 
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在这里插入图片描述

  • t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁

  • 如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
    在这里插入图片描述

3.2 t3 r.lock,t4 w.lock

在上面的基础上,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子

在这里插入图片描述

3.3 t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子

//sync.release(1) 
public final boolean release(int arg) {
	//尝试释放锁
    if (tryRelease(arg)) {
         // unpark AQS 中等待的线程 
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //调用unpark进行唤醒
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
     // 因为可重入的原因, 写锁计数为 0, 才算释放成功 
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

在这里插入图片描述

  • 接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
  • 这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一

在这里插入图片描述

  • 这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
    在这里插入图片描述
  • 在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行。这就是读锁可以共享的一部分原因
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
		//如果后面的老二,仍然是读锁,则继续唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

在这里插入图片描述

  • 这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一

在这里插入图片描述

  • 这时 t3 已经恢复运行,接下来 t3 调 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点

在这里插入图片描述

  • 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点

3.4 t2 r.unlock,t3 r.unlock

  • t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零,此时不会唤醒t4

在这里插入图片描述

  • t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
    在这里插入图片描述
  • 之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他 竞争,tryAcquire(1) 成功,修改头结点,流程结束

在这里插入图片描述

4、StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加解读锁

long stamp = lock.readLock();
lock.unlock(stamp);

加解写锁

long stamp = lock.writeLock(); 
lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead(); 
// 验戳 
if(!lock.validate(stamp)){    
	// 锁升级 
}

使用示例

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

class DataContainerStamped {
    private int data;
    private final StampedLock lock = new StampedLock();

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public int read(int readTime) {
        long stamp = lock.tryOptimisticRead();
        log.debug("optimistic read locking...{}", stamp);
        sleep(readTime);
        //首先尝试乐观读,如果stamp没有改变说明读成功,返回结果
        if (lock.validate(stamp)) {
            log.debug("read finish...{}, data:{}", stamp, data);
            return data;
        }
        //否则升级为读锁
        log.debug("updating to read lock... {}", stamp);
        try {
            stamp = lock.readLock();
            log.debug("read lock {}", stamp);
            sleep(readTime);
            log.debug("read finish...{}, data:{}", stamp, data);
            return data;
        } finally {
            log.debug("read unlock {}", stamp);
            lock.unlockRead(stamp);
        }
    }

    public void write(int newData) {
        long stamp = lock.writeLock();
        log.debug("write lock {}", stamp);
        try {
            sleep(2);
            this.data = newData;
        } finally {
            log.debug("write unlock {}", stamp);
            lock.unlockWrite(stamp);
        }
    }
}

测试 读-读 可以优化

public static void main(String[] args) {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -> {
        dataContainer.read(1);
    }, "t1").start();
    sleep(0.5);
    new Thread(() -> {
        dataContainer.read(0);
    }, "t2").start();
}

结果可以看出,t1和t2并没相互阻塞,而且都没有升级为读锁

15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256 
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256 
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1 
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1 

测试 读-写 时优化读补加读锁

public static void main(String[] args) {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -> {
        dataContainer.read(1);
    }, "t1").start();
    sleep(0.5);
    new Thread(() -> {
        dataContainer.write(100);
    }, "t2").start();
}

结果可以看出读操作被写操作阻塞,而且乐观读升级为了读锁

15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256 
15:57:00.717 c.DataContainerStamped [t2] - write lock 384 
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256 
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384 
15:57:02.719 c.DataContainerStamped [t1] - read lock 513 
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000 
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513

注意

  • StampedLock 不支持条件变量
  • StampedLock 不支持可重入

上面两个特性决定了它替代不不了ReentrantReadWriteLockReentrantLock

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值