JUC-ReentrantLock,ReentrantReadWriteLock,StampedLock

1. 概述

前面介绍过了synchronized关键字作用的锁升级过程
无锁->偏向锁->轻量锁->重锁
下面再介绍实现Lock接口的锁的升级过程
无锁->独占锁(ReentrantLock,Synchronized)->读写锁(ReentranReadWriteLock)->邮戳锁(StampedLock)
并准备了一些问题,检验一下自己对知识的掌握程度。

  1. 你知道Java里面有哪些锁?
  2. 你说你用过读写锁,锁饥饿问题是什么?有没有比读写锁更快的锁?
  3. StampedLock知道吗?(邮戳锁/票据锁)
  4. ReentrantReadWriteLock有锁降级机制,你知道吗?

2. ReentrantLock

ReentantLock是可重入的独占锁。默认是非公平锁。
可重入:当一个线程持有锁后,在内部可以继续获取锁。
独占:是一种悲观锁,当一个线程持有锁的时候,其他线程会阻塞。
公平和非公平:在公平的机制下,线程会依次排队,放到等待队列中。排队获取锁。在非公平的机制下,新来的线程通过CAS获取锁,获取不到,才会进入等待队列。

2.1 ReentrantLock使用代码演示

public static void main(String[] args)
    {
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
                lock.lock();
                try
                {
                    System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");
                }finally {
                    lock.unlock();
                }

            }finally {
                // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                lock.unlock();// 正常情况,加锁几次就要解锁几次
            }
        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
            }finally {
                lock.unlock();
            }
        },"t2").start();
    }

2.2 ReentrantLock和Synchronized比较

  1. ReentrantLock是对象,synchronzied是关键字
  2. 两者都是独占锁。(悲观锁)
  3. ReentrantLock加锁后需要手动解锁try{//do something}finally{Lock.unlock();}。synchronized关键字超出同步块自动解锁。
  4. ReentrantLock更灵活,可以控制是否是公平锁。synchronized只能是非公平锁。

使用场景的区别:

2.2.1 synchronized

写冲突比较多,线程强冲突的场景。
自旋的概率比较大,会导致浪费CPU性能。

2.2.2 ReentrantLock

synchronized锁升级是不可逆的,进入重量级锁后性能会下降。
ReentrantReadWriteLock(注意不是ReentrantLock)可以使用读写锁,增加性能。

3. ReentrantReadWriteLock

可重入读写锁。上面的可重入锁在两个线程同时读的过程中会竞争。可重入读写锁可以允许多个线程同时读取同一个资源。只允许读读共存,读写,写写之间都是互斥的。适用于读读不互斥的场景。

3.1 具有锁降级的性质

锁降级可以理解为一种操作。具体操作为写锁持有后,在准备释放写锁的之前,当前线程继续持有读锁,然后释放写锁。

    writeLock.lock();
    //do something
    // 锁降级:释放写锁之前先只有读锁。
    readLock.lock(); // A 降级开始
    writeLock.unlock();// 注意执行完这一步,其他阻塞队列的头部的读线程才能进入。
    // 锁降级完成
    ....
    readLock.unlock();

这种方式的好处是:在耗时长的事务中,锁降级能够使让读操作更快进行执行不会被写操作给抢占,且后面的读操作不会被打断。
锁降级的代码演示:


import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockDownTest {
    private Logger logger = LoggerFactory.getLogger(LockDownTest.class);
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    /**
     *	这里仅仅是想知道锁重入的情况,是不是这个时候加入的锁会到等待队列里面排队。
     */
    public void queryData() {
        try {
            Thread.sleep(500);
            readLock.lock();
            logger.info("主线程通过可重入读锁,查询数据完成.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
    }
    public void test3() throws Exception {
        // 开始锁降级
        writeLock.lock();
        logger.info("主线程抢到写锁...");
        // 这里的休眠是为了让下面线程能在预想的情况下加入等待队列.
        Thread.sleep(500);
        // 这里就是假设等待队列里面排在前面的是读锁线程
        processReadLock(1); 
        processReadLock(2); 
        Thread.sleep(500);
        processWriteLock(4);
        Thread.sleep(500);
        processReadLock(3); 
        Thread.sleep(500);
        // 开始锁降级
        readLock.lock(); // A 降级开始
        // 锁降级完成
        writeLock.unlock();// 注意必须读锁锁住,写锁释放操作完才降级完成,其他读线程才能进入。
        logger.info("主线程释放写锁(写锁降级为读锁,允许其他读操作进入)");
        logger.info("sleep 10s 验证等待队列中的读操作是否能执行..");
        TimeUnit.SECONDS.sleep(10);// 睡3s验证其他的读操作能进行
        logger.info("sleep 10s 结束");
        queryData();// 还是主线程去获取读锁。验证可重入锁。
        readLock.unlock(); // A 降级结束
        logger.info("主线程读锁释放");
//        logger.info("过程结束..");
    }
    private void processWriteLock(int threadIndex) {
        new Thread(() -> {
            logger.info("线程" + threadIndex + " 写锁开始竞争,阻塞中.");
            writeLock.lock();
            logger.info("线程" + threadIndex + " 写锁执行中..");
            writeLock.unlock();
            logger.info("线程" + threadIndex + " 写锁释放..");
        }).start();
    }
    private void processReadLock(int threadIndex) {
        new Thread(() -> {
            logger.info("线程" + threadIndex + " 读锁开始竞争,阻塞中.");
            readLock.lock();
            logger.info("线程" + threadIndex + " 读锁执行中..");
            readLock.unlock();
            logger.info("线程" + threadIndex + " 读锁释放..");
        }).start();
    }
    public static void main(String[] args) throws Exception {
        LockDownTest readWriteLockTest = new LockDownTest();
        readWriteLockTest.test3();
    }
}

注意这是公平锁的情况,结果说明:
在这里插入图片描述

3.2 可重入读写锁缺点(引入邮戳锁)

ReentrantReadWriteLock实现了读写分离。默认是非公平锁,每个线程是随机获取锁的。可能会导致锁饥饿的问题。
使用公平锁策略一定程度上能缓解这个问题,但是公平锁是牺牲系统的吞吐量为代价的。
引入StampedLock类的乐观锁。

4. StampedLock

StampedLock邮戳锁。这种锁是一种乐观锁,允许线程在读过程中进行写操作。让读多写少的时候,写线程有机会获取写锁。减少了线程饥饿的问题。吞吐量(单位时间系统能处理的请求量)大大提高。
在读线程操作临界资源的时候,允许写操作进行资源修改,那么读取到的数据是错误的怎么办?
为了保证读线程读取数据的正确性。读取的时候是乐观读,乐观读tryOptimisticRead不能保证读取的数据是正确性的,所以将数据读取到局部变量中,再通过lock.validate(stamp)校验是否被写线程修改过,若修改过则需要上悲观锁,重新读取数据到局部变量。

4.1 代码示例

使用代码示例:

 //乐观读,读的过程中也允许获取写锁介入
    public void tryOptimisticRead()
    {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        //故意间隔4秒钟,很乐观认为读取中没有其它线程修改过number值,具体靠判断
        System.out.println("4秒前stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 0; i < 4; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"正在读取... "+i+" 秒" +
                    "后stampedLock.validate方法值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp))
        {
            System.out.println("有人修改过------有写操作");
            // 数据校验失败,升级为悲观读
            stamp = stampedLock.readLock();
            try
            {
                System.out.println("从乐观读 升级为 悲观读");
                result = number;
                System.out.println("重新悲观读后result:"+result);
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t"+" finally value: "+result);
    }

4.2 使用场景和注意事项

StampedLock适用于读多写少的高并发场景。通过乐观读很好的解决了写线程饥饿的问题。
值得注意的是:
StampedLock不是可重入锁

5. 无锁-独占锁-读写锁-邮戳锁总结

在这里插入图片描述

  1. 从无锁到独占锁:无锁状态下数据在多线程环境下不安全因此需要锁
  2. 独占锁到可重入读写锁:独占锁在「读读」的时候线程存在竞争关系,实际很多场景中是允许多个线程同时读的。
  3. 可重入读写锁到邮戳锁:可重入读写锁会导致读多写少情况下的线程饥饿问题。引入了邮戳锁,允许读的过程中进行写。但是要采取乐观读的方式,进行数据的校验。如果数据校验失败,从乐观读变为悲观读。(乐观读的过程中允许写,悲观读的过程中不允许写操作)
  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值