Java并发编程七ReentrantReadWriteLock和StampedLock

10 篇文章 1 订阅


Java并发编程一:并发基础必知
Java并发编程二:Java中线程
Java并发编程三:volatile使用
Java并发编程四:synchronized和lock
Java并发编程五:Atomic原子类
Java并发编程六:并发队列

ReentrantReadWriteLock

是一种基于lock的读写锁,在使用ReentrantLock时,它保证当前只有一个线程获取锁,但是有时候我们实际应用中会出现读多写少的场景,读于读之间都是读取同样的数据,如果使用ReentrantLock反而效率会低下,使用ReentrantReadWriteLock会很高效,它可以实现多个读锁同时进行,但是读与写和写于写互斥,只能有一个写锁线程在进行。

public class ReentrantReadWriteLockDemo {
  static class MyDemo{
        // 实例化读写锁 默认非公平
        private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
        // 模拟共享资源
        private int number;
        public void put(int number)  {
            // 写锁加锁
            lock.writeLock().lock();
            try {
                Thread.sleep(500);
               this.number=number;
                System.out.println(Thread.currentThread().getName()+":写入了"+number);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 写锁释放锁
                lock.writeLock().unlock();
            }
        }
        public int get()  {
            // 读写 加锁
            lock.readLock().lock();
            try {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":读取了"+number);
                return number;
            }  finally {
                // 读锁解锁
                lock.readLock().unlock();
            }
        }
    }
    public static void main(String[] args) {
        MyDemo myDemo = new MyDemo();
        // 三个写线程
        for (int i = 0; i < 3; i++) {
            new Thread(()-> myDemo.put(new Random().nextInt(100)),"写锁"+i).start();
        }
        // 十个读线程
        for (int i = 0; i < 10; i++) {
           new Thread(()-> myDemo.get(),"读锁"+i).start();
        }
    }
}

其中一次输出结果为:

写锁2:写入了96
读锁0:读取了96
写锁1:写入了27
读锁4:读取了27
读锁6:读取了27
读锁5:读取了27
读锁3:读取了27
读锁7:读取了27
读锁1:读取了27
读锁2:读取了27
写锁0:写入了66
读锁9:读取了66
读锁8:读取了66

StampedLock

它是Jdk在1.8提供的一种读写锁,相比较ReentrantReadWriteLock性能更好,因为ReentrantReadWriteLock在读写之间是互斥的,使用的是一种悲观策略,在读线程特别多的情况下,会造成写线程处于饥饿状态,虽然可以在初始化的时候设置为true指定为公平,但是吞吐量又下去了,而StampedLock是提供了一种乐观策略,更好的实现读写分离,并且吞吐量不会下降。

三种锁

  • 写锁writeLock:
    是一个独占锁写锁,当一个线程获得该锁后,其他请求读锁或者写锁的线程阻塞, 获取成功后,会返回一个stamp(凭据)变量来表示该锁的版本,在释放锁时调用unlockWrite方法传递stamp参数。提供了非阻塞式获取锁tryWriteLock。
  • 悲观读锁readLock:
    是一个共享读锁,在没有线程获取写锁情况下,多个线程可以获取该锁。如果有写锁获取,那么其他线程请求读锁会被阻塞。悲观读锁会认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据进行加锁,这是在读少写多的情况下考虑的。请求该锁成功后会返回一个stamp值,在释放锁时调用unlockRead方法传递stamp参数。提供了非阻塞式获取锁方法tryWriteLock。
  • 乐观读锁tryOptimisticRead:
    相对比悲观读锁,在操作数据前并没有通过CAS设置锁的状态,如果没有线程获取写锁,则返回一个非0的stamp变量,获取该stamp后在操作数据前还需要调用validate方法来判断期间是否有线程获取了写锁,如果是返回值为0则有线程获取写锁,如果不是0则可以使用stamp变量的锁来操作数据。由于tryOptimisticRead并没有修改锁状态,所以不需要释放锁。这是读多写少的情况下考虑的,不涉及CAS操作,所以效率较高,在保证数据一致性上需要复制一份要操作的变量到方法栈中,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性得到了保证。

StampedLock支持三种锁在一定情况下进行相互转换。例如long tryConvertToWriteLock(long stamp)期望把stamp标示的锁升级为写锁,这个函数会在下面几种情况下返回一个有效的stamp(也就是晋升写锁成功):

  • 如果当前锁已经是写锁,直接返回stamp。
  • 如果当前是读锁,没有其他线程是读锁模式,返回一个写锁stamp。
  • 如果当前是乐观读锁,并且没有线程获取写锁,返回一个stamp。

注意点

由于StampedLock是读写锁都是不可冲入锁,所以在获取锁后释放锁前不再调用获取锁操作,避免造成线程的阻塞,当多个线程同时尝试获取写锁和读锁,是随机性的,没有一定的规则,并且该锁不是实现Lock或ReadWriteLock接口,而是在其内部自己维护了一个双向队列。

案例说明

下面一个例子是官方的提供的一个二维点的的例子:

 package com.smart.home.ThreadTest;

import java.util.concurrent.locks.StampedLock;

class Point {
    // 内部定义表示坐标点
    private double x, y;
    //定义了StampedLock锁,
    private final StampedLock s1 = new StampedLock();

    // 写锁
    public void move(double deltaX, double deltaY) {
        // 获得写锁 凭据
        long stamp = s1.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            // 释放写锁
            s1.unlockWrite(stamp);
        }
    }

    // 乐观锁读
    public double distanceFormOrigin() {
        //尝试乐观读 返回stamp凭证
        long stamp = s1.tryOptimisticRead();
        //读取x和y的值,这时候我们并不确定x和y是否是一致的 需要下一步再次判断
        double currentX = x, currentY = y;
        /**
         * 判断stamp在读过程发生期间被修改过,如果没有被修改,则这次读取有效,直接return
         * 如果stamp被修改过,则有可能其他线程改写了数据,会出现脏读,可以使用死循环使用乐观锁读,直到成功
         * 也可以使用锁的级别,将乐观锁变为悲观锁
         */
        if (!s1.validate(stamp))
            // 使用悲观锁读 如果有写线程那么该线程会挂起
            stamp = s1.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            // 释放读锁
            s1.unlockRead(stamp);
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 读锁转为写锁
    public void moveIfAtOrigin(double newX, double newY) {
        // 读锁加锁 可以使用乐观读锁替代
        long stamp = s1.readLock();
        try {
            // 如果当前是原点 则修改
            while (x == 0.0 && y == 0.0) {
                // 尝试升级为写锁
                long ws = s1.tryConvertToWriteLock(stamp);
                // 升级成功 更新stamp凭据 设置坐标值 退出循环
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 升级失败 释放读锁,重新获取写锁,循环重试
                    s1.unlockRead(stamp);
                    stamp = s1.writeLock();
                }
            }
        } finally {
            // 释放锁
            s1.unlock(stamp);
        }
    }
}

使用乐观锁读可以避免写锁处于饥饿状态,增加吞吐量,但是使用乐观锁读也是很容易犯错误的,在使用上必须保证以下顺序。

      //乐观读 返回stamp凭证
        long stamp = lock.tryOptimisticRead();
        // 复制变量到方法栈
        CopyVariablesMethodStack();
        // 校验stamp凭证
        if (!lock.validate(stamp)){
            // 获取读锁
            stamp = lock.readLock();
            try {
            // 复制变量到方法栈
                CopyVariablesMethodStack();
            } finally {
                // 释放读锁
                lock.unlockRead(stamp);
            }
        }
         // 操作操作复制到方法栈中的变量
        ManipulateVariablesCopeMethodStack();
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值