zoukankan      html  css  js  c++  java
  • 并发编程(九):读写锁,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()方法,唤醒所有该等待队列上的线程


  • 相关阅读:
    redis安装以及php扩展
    Linux下php安装Redis扩展
    正则验证邮箱
    常用方法
    PHPExcel说明
    冒泡排序
    CURL post请求
    PHP生成随机字符串
    PHP中的字符串函数
    PHP中的数组函数
  • 原文地址:https://www.cnblogs.com/kenshine/p/14520521.html
Copyright © 2011-2022 走看看