reentrantLock、readWriteLock、AQS

Lock接口使用

  1. void lock() 获取锁(不死不休)方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。
  2. bollean tryLock() 获取锁(浅尝辄止,尝试获取锁)
  3. boolean tryLock(long time,TimeUnit unit)throws InterruptedExcepiton 获取锁(过时不候,在指定时间内尝试获取锁)
  4. void lockInterruptibly() throws InterruptedExcepion; 获取锁(任人摆布),允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
  5. void unlock(); 释放锁,

结论: 1. lock()最常用;lockInterruptibly()方法一般更昂贵,只有真的需要相应中断时,才是用。

Condition 和Lock一起使用

调用condition.signal/await(会释放锁)前必须获取到该锁才能执行,和synchronization方法体内调用wait/notify一样。如下面代码:

private  static Lock lock=new ReentrantLock();
private static Condition condition=lock.newCondition();
public static void main(String[] args) throws  InterruptedException{
    Thread th=new Thread(new Runnable() {
        @Override
        public void run() {
            lock.lock();
            System.out.println("condition.await()");
            try{
                condition.await();
                System.out.println("here i am ...");
            }catch (InterruptedException e){
            e.printStackTrace();
            }finally {
              lock.unlock();
            }
        }
    });
    th.start();
    Thread.sleep(2000L);
    //lock.lock(); 如果把改行注释掉后执行下面的condition.signal()会报错,因为主线程没有获取到锁。
    condition.signal();///todo 调用condition.signal前必须获取到该锁才能执行,和synchronization方法体内调用wait一样
    lock.unlock();
}

ReentrantLock

加解锁属性变化-count、ower

在这里插入图片描述

ReentrantLock 原理-加锁流程

ReentrantLock 包含如下四属性:

  1. owner:锁的持有者,初始值为null;
  2. count:锁的重入次数,初始值为0;
  3. waiters:等待队列,抢锁失败进入等待队列

抢锁流程如下:

  1. 如下第一张图 t1、t2、t3、t4线程通过cas进行抢锁
  2. 如下第二张图 假设t2抢到了锁,此时owner=t2,count=1,如果t2接着再次抢锁,会通过owner值判断出自己则将count加1(重入操作)
  3. 如下图三其他线程再抢锁判断owner此时不为空并不是自己,(t1、t3、t4)进入waiters等待队列,线程处于waiting状态。持有锁的线程释放锁后waiters队列头部被唤醒(判断当前线程是否处于队列头部,如果是会被唤醒再次抢锁),默认锁是非公平的被唤醒的线程和外部加入的线程共同抢锁没有任何优势。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

Synchronized vs Lock

Synchronized

-优点:

  1. 使用简单,语义清晰,需要哪里点哪里。
  2. 由JVM提供,提供了多种优化方案(锁粗化、锁消除、锁偏向、轻量级锁)
  3. 锁的释放由虚拟机来完成,不用人工干预,也降低了死锁的可能性

-缺点

无法实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁等。

Lock

优点:

  1. 所有synchronized的缺点
  2. 可以实现更多功能,让synchronized缺点更多

缺点: 需要手动释放锁,新手使用不当会造成死锁。

结论: synchronized是卡片机、lock是单反。

ReadWriteLock

概念

维护一对关联锁,一个只用于读操作,一个只用于写操作;读锁可以由多个读线程同时持有,写锁是排他的。同一时间,两把锁不能被不同线程持有。

使用场景

适用于读取操作多于写入操作的场景,改进互斥锁的性能,比如:集合的并发线程安全性改造、缓存组件。

锁降级

指的是写锁降级为读锁持有写锁的同时,再获取读锁随后释放写锁的过程,此时线程持有的锁是读锁。写锁是线程独占,读锁是共享,所以写->读是降级。(读->写,是不能实现的)

读写锁示例

class TeacherInfoCache {
    static volatile boolean cacheValid;//缓存是否可用标识
    static final ReadWriteLock rwl = new ReentrantReadWriteLock();

    /***
     * 实现一个使用Cache数据的方法,通过读写锁限制读取数据库的线程数量
     * > 如果缓存可用,就使用缓存
     * > 如果缓存不可用,从DB加载数据到缓存
     * >拿到数据之后,需要使用数据,这期间,数据不能发生改变
     * @param dataKey
     * @return
     */
    static Object get(String dataKey){
        Object data = null;

        //读取数据,加读锁,读锁共享锁,线程可以并发访问
        rwl.readLock().lock();
        try {
            if (cacheValid){//缓存可用则直接从缓存中获取数据
                data = Redis.data.get(dataKey);
            }else{
                //通过加锁的方式去访问DB(因为写锁是独占锁,只能一个线程持有,这就有效的避免了大量线程访问db,线程获取写锁首先得释放读锁),加写锁
                rwl.readLock().unlock();//进来的线程释放读锁

                rwl.writeLock().lock();//此时只有一个线程可以获取到读锁,其他线程阻塞到这里
                try {
                    if (!cacheValid){//获取到写(独占)锁的线程进行如下数据库操作,从数据库中获取data并放入redis,将cacheValid设置为true,然后后续线程就可以直接读取缓存中的数据,不再进入else
                        data = DataBase.queryUserInfo();
                        Redis.data.put(dataKey, data);

                        cacheValid = true;
                    }
                }finally {
                    //用到了readWrite的锁降级,释放写锁前获取读锁,然后再释放写锁,此时持有的是读锁;因为缓存不可用时(else)发现此时多了一次readLock().unlock(),44行和62行,所以使用锁降级转换为读锁
                    rwl.readLock().lock();
                    rwl.writeLock().unlock();
                }
            }
            return data;
        }finally {
            rwl.readLock().unlock();
        }
    }
}
class DataBase{
    static String queryUserInfo(){
        System.out.println("查询数据库。。。");
        return "name:Kody,age:40,gender:true,";
    }
}

class Redis{
    static Map<String, Object> data = new HashMap<>();
}

重要属性

  1. owner 标记独占锁被哪个线程占有
  2. readcount 共享锁有多少个线程持有 (注AQS中 readcount和writecount是一个32位变量 前16位表示readcount 后16位表示writecount,目前这么表示是方便理解
  3. writecount 独占锁重入的次数 (注AQS中 readcount和writecount是一个32位变量 前16位表示readcount 后16位表示writecount,目前这么表示是方便理解
  4. waiters 等待队列,抢锁失败的线程进入waiters等待被唤醒,AQS中是一个链表结构

ReadWriteLock原理

  1. 如下图一所示,t1 w、 t2 w 为获取写锁的线程,t2 r、 t3 r 、t4r 线程为获取读锁的线程 ;假设t1 w 最先发起获取写锁操作,会判断readcount是否为0,如果readCount为0,判断writeCount如果为0并且owner为空,则通过cas获取锁,获取锁成功,将writeCount=1,owner指向t1 w的引用。如果t1 w在释放锁前再次获取写锁会进行重入操作,如果再次获取的是读锁会产生锁降级。
  2. 如图二、图三 t2r获取读锁会判断writeCount是否为0,因为writeCount不为0,读锁和写锁互斥,t2 r进入waiters等待队列。紧接着t2 r t3r t4r 也向后进入waiters队列。
  3. 如图四,图五 t1w释放写锁,会唤醒waiters头部的t2r线程进行获取读锁操作。
  4. 如图五、图六 t2 r获取到读锁后,因为读锁是共享锁,t3r t4r先后被唤醒获取到读锁。
  5. 图图七、图八,t2w尝试获取写锁,判断readCount>0,进入waiters等待队列
  6. 图九、图十、图十一 、图十二 t2r 、t3r 、t4r 释放完读锁,唤醒waiters队列头部线程,t2w被唤醒进行抢写锁,t2w判断当前readCount=0,抢锁成功,owner指向t2w,writeCount=1

readWrite 总结

  1. 读锁和写锁不能被不同线程同时持有,读锁和写锁是互斥的,线程持有写锁(独占锁)可以再次获取读锁(共享锁),然后释放写锁,持有读锁(锁降级)
  2. 读锁释放成功是readcount=0时才算释放成功,释放成功后唤醒waiters队列头部的等待线程
  3. 没有抢到锁的线程会进入waiters等待队列,持有锁的线程释放成功后会唤醒队列头部的线程。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

AQS

概念

AQS:共有的逻辑放到模板当中,封装成一个工具类方便其他人去使用 (公共的逻辑实际上是(线程)排队的队列) 经常会变的代码留给用户去实现。提供了对资源占用、释放、线程的挂起有、唤醒的逻辑。预留了各种(tryAcquire 、tryRelease、tryAcquireShared、tryReleaseShared) 方法给用户实现,可以用在各种需要控制资源争用的场景中。(ReentrantLock/CountDownLatch、Samphore)

AQS中的主要方法

  1. 独享锁 acquire(获取独占锁) 、 release(释放独占锁)、tryAcquire(尝试获取独占锁)、tryRelease(尝试释放独占锁)
  2. 共享锁 acquireShared(获取共享锁) 、releaseShared(释放共享锁)、tryAcquireShared(尝试获取共享锁) 、tryReleaseShared(尝试释放共享锁)

AQS中的字段

  1. int state (记录锁的状态,是一个三十二位的整型 前16位表示readCount 后16位表示writeCount)
  2. Thread Owner 线程的持有者,写锁被持有时指向对应的线程
  3. Queue (head-node-node-tail) 等待队列,是链表结构

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值