并发编程(九):读写锁,LockSupport和Condition


学习资料

《Java并发编程的艺术》第5章 5.4~5.6


1.读写锁简介

读写锁与排他锁:

  • 排他锁:同一个时刻只能运行一个线程进行访问(不管读写),如ReentrantLock
  • 读写锁:同一时刻允许有多个读线程访问,但是有写线程访问时,所有其他线程(不管读写)都被阻塞

Java并发包提供的读写锁的实现是ReentrantReadWriteLock,支持公平性选择,重入锁以及锁降级

  • 锁降级:获取写锁—>获取读锁—>释放写锁,写锁能够降级为读锁

ReadWriteLock接口仅提供了两个与锁相关方法:

  • lock readLock():获取读锁
  • lock writeLock():获取写锁

ReentrantReadWriteLock实现提供了四个展示内部工作状态的方法:

  • getReadLockCount():返回当前读写锁被获取的次数(不等于获取读锁的线程数,有重入)
  • getReadHoldCount():返回当前线程获取读锁的个数(线程获取多个锁或重入)
  • isWriteLocked():判断写锁是否被获取
  • getWriteHoldCount():返回当前写锁被获取的次数(重入次数)

简单使用方式:

static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); 
static Lock r = rwl.readLock(); 
static Lock w = rwl.writeLock();
r.lock();r.unlock();	//读锁操作
w.lock();w.unlock();	//写锁操作

2.读写锁实现

2.1 读写状态设计

读写锁同样依赖自定义同步器来实现同步功能,读写状态就是同步器的同步状态

同步状态是一个整型(32位),需要维护多个读线程和一个写线程的状态,用高位16位表示读状态,低位16位表示写状态:

2.2 写锁获取与释放

写锁获取

写锁是一个支持重进入的排他锁:

  • 如果当前线程已经获得了写锁,则增加写状态,重入锁
  • 如果读线程已经被获取或者其他线程获取了写锁,则线程进入等待状态
  • 如果读锁没有被获取,写锁也没有被获取,则当前线程获取写锁

读读共享,读写互斥,写写互斥

只有当读线程都释放了读锁,写锁才能被当前线程获取;写锁一旦被获取,后续的所有读写线程都被阻塞。

写锁释放

每次释放均减少写状态,写状态为0则真正被释放

2.3 读锁获取与释放

读锁获取

读锁是一个支持重进入的共享锁,能被多个线程同时获取,没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取

读状态是所有线程保存的所有锁的总数,也就是说新线程获取读锁会添加读状态,同一个线程锁重入也会添加读状态。Java6之后将每个线程获取的读锁次数存放在ThreadLocal中,使得读锁获取实现变得复杂

特殊情况:

  • 其他线程获取写锁—>当前线程无法再获取读锁
  • 当前线程获取写锁—>当前线程可获取读锁,之后再释放写锁(锁降级)

读锁释放

读锁每次释放(需要保证线程安全)都会减少读状态(减少1<<16)

2.4 锁降级

写锁—>读锁

把持住(当前线程拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程

示例代码:

public void processData() {
	//获取读锁
    readLock.lock();
    if (!update) {
	// 必须先释放读锁 
        readLock.unlock();
	// 锁降级从写锁获取到开始 
        writeLock.lock();
        try {
            if (!update) {
		// 准备数据的流程(略) 
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
	// 锁降级完成,写锁降级为读锁 
    }
    try {
     // 使用数据的流程(略) 
    } finally {
        readLock.unlock();
    }
}

锁降级为了保证数据的可见性,防止写锁切换为读锁过程中被其他写锁获取到线程修改数据(此时修改的数据还未被使用)

ReentrantReadWriteLock不支持锁升级


3.LockSupport工具

LockSupport提供了最基本的线程阻塞和唤醒功能,内部调用了Unsafe类中的本地方法来实现,LockSupport是构建同步组件的基础工具

LockSupport定义了一组以park开头的方法阻塞当前线程,以及unpark(Thread thread)方法唤醒一个被阻塞的线程:park()parkNanos()parkUntil()unpark()

Java6增加了带blocker参数的park开头的方法如:park(Object blocker),blocker表示当前线程所在的阻塞对象,增加这些方法主要是为了问题排除和系统监控


4.Condition接口简介

Java提供了一组监视器方法(Object类上),主要包括wait()wait(long timeout)notify()notifyAll()方法,这些方法和Synchronized关键字配合,可以实现等待/通知模式。

Condition接口也提供了类似Object监视器的功能,和Lock配合可以实现等待/通知模式。

Object监视器方法与Condition接口对比:

Condition的简单使用方式:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();	//可创建多个condition

public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
        //相当于obj.wait(),在Condition上等待
        condition.await();
    } finally {
        lock.unlock();
    }
}

public void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
     	//相当于obj.notify()/notifyAll()
        condition.signal();
        //condition.signalAll();
    } finally {
        lock.unlock();
   }
}  

Condition的常用方法:

  • await()awaitUninterruptibly(...)awaitNanos(...)awaitUnit(...)signal()signalAll()

Condition的获取只能通过Lock.newCondition()获取,并且可以获取多个,多个等待队列,比如所有读线程在一个Condition上等待,所有写线程在另一个Condition上等待


5.Condition的实现

ConditionObject是AQS的内部类

5.1 等待队列

实际上是一个FIFO队列,复用了AQS同步队列的节点

一个Condition包含一个等待队列,Condition拥有首节点和尾结点,示意图:

Condition.await()会新增节点,通过锁来保证更新节点的线程安全的(await之前已经获取到了锁)

AQS同步器拥有一个同步队列和多个等待队列:

每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用

5.2 等待

Condition.await()会使线程进入等待队列并释放锁,同时线程变为等待状态

从队列角度看,相当于从同步队列首节点(获取了锁的节点)移动到了等待队列中:

ConditionObject.await()方法代码如下:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
                throw new InterruptedException();
    //当前线程加入等待队列
    Node node = addConditionWaiter();
    //释放同步状态(释放锁)
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    
    //节点不再同步队列中
    while (!isOnSyncQueue(node)) {
        //通过LockSupport.park()使线程等待
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) 
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

5.3 通知

Condition.signal()会唤醒在等待队列中等待时间最长的节点(首节点),并移动到同步队列中:

ConditionObject.signal()方法代码如下:

public final void signal() {
	//判断锁是否被当前线程持有
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
    	//内部通过LockSupport.inpark(node.thread)唤醒线程
        doSignal(first);
}

Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,唤醒所有该等待队列上的线程


posted @ 2021-03-11 21:05  菜鸟kenshine  阅读(240)  评论(0编辑  收藏  举报