栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

并发编程学习笔记 之 Lock锁及其实现类ReentrantLock、ReentrantReadWriteLock和StampedLock的基本用法

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

并发编程学习笔记 之 Lock锁及其实现类ReentrantLock、ReentrantReadWriteLock和StampedLock的基本用法

一、Lock锁

  Lock接口是对锁操作方法的一个基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助于Lock创建不同的Condition对象进行多线程间的通信操作,与关键字synchronized进行方法同步代码块同步的方式不同,Lock提供了编程式的锁获取(lock())以及释放操作(unlock())等其他操作。

  Lock锁的实现类:


  Lock锁的接口定义:

public interface Lock {

    
    void lock();

    
    void lockInterruptibly() throws InterruptedException;

    
    boolean tryLock();

   	
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    
    void unlock();

    
    Condition newCondition();
}
二、ReentrantLock 可重入锁

  在显式锁Lock接口的实现中,我们用得最多的就是ReentrantLock,该类不仅完全实现了显示锁Lock接口所定义的接口,也扩展了对使用显式锁Lock的一些监控方法。

  • getHoldCount()方法:查询当前线程在某个Lock上的数量,如果当前线程成功获取了Lock,那么该值大于等于1;如果没有获取到Lock的线程调用该方法,则返回值为0。
  • isHeldByCurrentThread()方法:判断当前线程是否持有某个Lock,由于Lock的排他性,因此在某个时刻只有一个线程调用该方法返回true。
  • isLocked()方法:判断Lock是否已经被线程持有。
  • isFair()方法:创建的ReentrantLock是否为公平锁。
  • hasQueuedThreads()方法:在多个线程试图获取Lock的时候,只有一个线程能够正常获得,其他线程可能(如果使用tryLock()方法失败则不会进入阻塞)会进入阻塞,该方法的作用就是查询是否有线程正在等待获取锁。
  • hasQueuedThread(Thread thread)方法:在等待获取锁的线程中是否包含某个指定的线程。

注意细节:

  1. 多个原子性方法的组合不能确保原子性
  2. 避免锁的交叉使用引起死锁
  3. 确保已获取锁的释放(因为可重入性,所以多次获取锁,就需要多次释放锁)
//阻塞方式获取锁
public static void test1() throws InterruptedException {
    final ReentrantLock lock = new ReentrantLock();
    //线程A
    Thread t1 = new Thread(() ->{
        try {
            //第一次获取锁,阻塞方式获取
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "线程,第一次获取到lock!当前holdCount:" + lock.getHoldCount());
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "线程,第二次获取到lock!当前holdCount:" + lock.getHoldCount());
        } finally {
            lock.unlock();
            //lock.unlock();
            System.out.println(Thread.currentThread().getName() + "线程,第一次释放lock!当前holdCount:" + lock.getHoldCount());
        }
    });
    t1.start();

    //主程序休眠3秒,保证线程A创建完成,并启动
    TimeUnit.SECONDS.sleep(3);

    //主线程尝试获取锁,因为线程A获取锁两次,但是只释放了一次,所以线程A没有真正把锁释放掉,主线程无法获取锁,阻塞当前线程
    lock.lock();
    System.out.println(Thread.currentThread().getName() + "线程,是否可以执行到该位置呢?" );
    lock.unlock();
}
//主线程采用非阻塞的方式,获取锁,避免主线程被阻塞
if(lock.tryLock()){
    lock.lock();
    System.out.println(Thread.currentThread().getName() + "线程,获取到了锁!" );
    lock.unlock();
}else{
    System.out.println(Thread.currentThread().getName() + "线程,未获取到锁!" );
}
ReentrantLock 与 Synchronized关键字 的区别

  使用synchronized关键字进行共享资源的同步时,JVM提供了两个指令monitor enter和monitor exit来分别确保锁的获取和释放操作,这与显式锁Lock的lock和unlock方法的作用是一致的。

1、synchronized关键字的使用灵活性不如显式锁Lock
2、在单线程访问的情况下,synchronized关键字的性能要高于lock锁,这主要得益于JDK内部对于synchronized关键字的不断优化升级,另外在单线程的情况下,synchronized关键字的jvm指令在运行期间也会被优化。
3、在多线程访问情况下,显式锁Lock的性能要优于synchronized关键字。

三、ReadWriteLock 读写锁

  读写锁类,旨在允许某个特定时刻多线程并发读取共享资源,提高系统性能和访问吞吐量,即对共享资源的访问一般包括两种类型的动作,读和写(修改、删除等会引起资源发生变化的动作),当多个线程同时对某个共享资源进行读取操作时,并不会引起共享资源数据不一致情况的发生,因此这个时候我们不应该让资源的访问互斥。

  与ReentrantLock一样,只不过在使用的过程中需要分别派生出“读锁”和“写锁”,在进行共享资源读取操作时,需要使用读锁进行数据同步,在对共享资源进行写操作时,需要使用写锁进行数据一致性的保护。

public interface ReadWriteLock {
    
    Lock readLock();

    
    Lock writeLock();
}

  ReentrantReadWriteLock实现自ReadWriteLock接口并且提供了一些ReadWriteLock监控查询方法。

  读锁、写锁的方法:

  读写锁使用示例,在main方法中,多次调用readLock()方法,发现不会被阻塞,但是如果分别调用readLock()方法和writeLock()方法,或者同时调用writeLock()方法都会被阻塞。

public class ReadWriteLockTest {
    //读写锁
    private static final ReadWriteLock lock = new ReentrantReadWriteLock();
    //读锁
    private static final Lock readLock = lock.readLock();
    //写锁
    private static final  Lock writeLock = lock.writeLock();
    //共享数据
    private static List list = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {

        ReadWriteLockTest.writeLock();
        TimeUnit.SECONDS.sleep(2);//保证上句先执行
        System.out.println(Thread.currentThread().getName() + "主线程继续执行!");
        ReadWriteLockTest.readLock();
        //ReadWriteLockTest.readLock();
    }

    public static void readLock(){
        new Thread(() ->{
            try{
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + "读线程,执行了读操作:");
                list.forEach(i -> System.out.println(i));
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + "读线程,继续执行!");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                readLock.unlock();
            }
        }).start();
    }
    public static void writeLock(){
        new Thread(() ->{
            try{
                writeLock.lock();
                System.out.println(Thread.currentThread().getName() + "写线程,执行了写操作:");
                list.add("test");
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + "写线程,继续执行!");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                writeLock.unlock();
            }
        }).start();
    }
}
四、Condition对象

  Condition对象是由某个显式锁Lock创建的,一个显式锁Lock可以创建多个Condition对象与之关联,Condition的作用在于控制锁并且判断某个条件(临界值)是否满足,如果不满足,那么使用该锁的线程将会被挂起等待另外的线程将其唤醒,与此同时被挂起的线程将会进入阻塞队列中并且释放对显式锁Lock的持有,这一点与对象监视器的wait()方法非常类似。
  Condition一经推出,就大规模地替代了传统对象监视器(Object Monitor)方式进行多个线程间的通信和数据交换,同时Condition又提供了更多的操作方法,比如用于线程监控等。相比对象监视器的方式,Condition更加高效,避免了很多无谓的线程上下文切换,从而提高了CPU的利用率。

public interface Condition {

    
    void await() throws InterruptedException;

    
    void awaitUninterruptibly();

    
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    
    boolean awaitUntil(Date deadline) throws InterruptedException;

    
    void signal();

    
    void signalAll();
}

  根据Condition的用法,我们模拟一个生产者消费者模式,来验证Condition的用法,示例如下:

public class ConditionTest {

    private static final Lock lock = new ReentrantLock();
    //用于共享变量是否达到上限的判断
    private static  final Condition FULL_CONDITION = lock.newCondition();
    //用于共享变量是否为空的判断
    private static  final Condition EMPTY_CONDITION = lock.newCondition();
    //共享变量,模拟生产消费者中的池
    private static final LinkedList list = new LinkedList<>();
    //共享变量允许的最大值
    private static final  int CAPACITY = 10;
    //初始值
    private static long i = 0;

    public static void main(String[] args) {
        //启动3个生产者线程
        IntStream.range(0,3).forEach(k ->{
            new Thread(()->{
                try {
                    for (;;){
                        produce();
                        TimeUnit.SECONDS.sleep(3);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            },"Producer" + k).start();
        });
        //启动2个消费者线程
        IntStream.range(0,2).forEach(k ->{
            new Thread(()->{
                try {
                    for (;;){
                        consume();
                        TimeUnit.SECONDS.sleep(3);
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            },"Consumer" + k).start();
        });
    }

    private static void produce(){
        lock.lock();//获取阻塞锁
        try{
            System.out.println("生产者线程" + Thread.currentThread().getName() + ",获取到锁,尝试产生数据!");
            while (list.size() >= CAPACITY){//共享变量已经达到存储上限
                System.out.println("生产者线程" + Thread.currentThread().getName() + ",当前数据达到上限,阻塞当前线程,释放锁!");
                FULL_CONDITION.await();//阻塞当前线程,释放锁
            }
            i++;
            list.addLast(i);
            System.out.println("生产者线程" + Thread.currentThread().getName() + ",产生数据:" + i);
            //唤醒Condition阻塞队列中的一个线程,Condition的wait队列采用FiFO的方式,因此在wait队列中,第一个进入阻塞队列的线程将会被首先唤醒
            EMPTY_CONDITION.signal();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();//释放锁
        }
    }

    private static void consume(){
        lock.lock();
        try {
            System.out.println("消费者线程" + Thread.currentThread().getName() + ",获取到锁,尝试消费数据!");
            while (list.isEmpty()){//没有数据可使用
                System.out.println("消费者线程" + Thread.currentThread().getName() + ",当前数据为空,阻塞当前线程,释放锁!");
                EMPTY_CONDITION.await();//阻塞当前线程,释放锁
            }
            Long v = list.removeFirst();
            System.out.println("消费者线程" + Thread.currentThread().getName() + ",消费数据:" + v);
            //唤醒Condition wait队列中的所有线程。
            FULL_CONDITION.signalAll();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
五、StampedLock锁

  JDK1.8版本引入了StampedLock,该锁由一个long型的数据戳(stamp)和三种模型构成,当获取锁(比如调用readLock(),writeLock())的时候会返回一个long型的数据戳(stamp),该数据戳将被用于进行稍后的锁释放参数。如果返回的数据戳为0(比如调用tryWriteLock()),则表示获取锁失败,同时StampedLock还提供了一种乐观读的操作方式。和ReentrantLock、ReentrantReadWriteLock不一样的是:在StampedLock内部没有hold计数器,所以是不可重入的。StampedLock的引入可以解决读写锁的饥饿写问题。

读写锁的饥饿写问题

  饥饿写是指在使用读写锁(非公平锁)的时候,读线程的数量远远大于写线程的数量,导致锁长期被读线程霸占,写线程无法获得对数据进行写操作的权限从而进入饥饿的状态。当然可以在构造读写锁时指定其为公平锁,读写线程获得执行权限得到的机会相对公平,但是当读线程大于写线程时,性能效率会比较低下。

StampedLock实现ReentrantLock锁的功能

  在ReentrantLock锁中不存在读写分离锁,使用StampedLock锁
的writeLock()方法模拟ReentrantLock类的lock()方法,该方法会返回一个数据戳(stamp),在稍后的锁释放过程中需要用到该数据戳(stamp),即lock.unlock(stamp)。

和ReentrantLock的区别:不可重入。

StampedLock实现ReentrantReadWriteLock锁的功能

  StampedLock也提供了读锁和写锁这两种模式,因此StampedLock天生就支持读写分离锁的使用方式。使用StampedLock锁不需要额外创建出不同类型的Lock(ReadLock或WriteLock)就可以很轻易地完成读写锁的分离,提高并发情况下的数据读取性能。

  实现示例如下,可以验证StampedLock完全可以支持:读读不互斥,读写互斥,写写互斥。

public class StampedLockTest1 {

    //共享变量,模拟生产消费者中的池
    private static final LinkedList list = new LinkedList<>();
    private static int i = 0;
    private static  final StampedLock lock = new StampedLock();

    public static void main(String[] args) {
        IntStream.range(0,10).forEach(k ->{
            new Thread(()->{
                consume();
            },"Consumer" + k).start();
        });
        

    }

    
    public static void produce(){
        long stamp = lock.writeLock();
        System.out.println("生产者线程" + Thread.currentThread().getName() + "获取写锁!");
        try {
            i++;
            list.addLast(i);
            System.out.println("生产者线程" + Thread.currentThread().getName() + ",获取到锁,产生数据:" + i);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock(stamp);
            System.out.println("生产者线程" + Thread.currentThread().getName() + "释放写锁!");
        }
    }

    
    public static void consume(){
        long stamp = lock.readLock();
        try{
            System.out.println("消费者线程" + Thread.currentThread().getName() + "获取读锁!");
            if(!list.isEmpty()){
                Integer v = list.removeFirst();
                System.out.println("消费者线程" + Thread.currentThread().getName() + ",消费数据:" + v);
            }else{
                System.out.println("消费者线程" + Thread.currentThread().getName() + ",当前没有数据可消费");
            }
            TimeUnit.SECONDS.sleep(new Random().nextInt(5));
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock(stamp);
            System.out.println("消费者线程" + Thread.currentThread().getName() + "释放读锁!");
        }
    }
}
StampedLock实现乐观读模式

  StampedLock还提供了一个乐观读模式,使用tryOptimisticRead()方法获取一个非排他锁并且不会进入阻塞状态,与此同时该模式依然会返回一个long型的数据戳用于接下来的验证(该验证主要用来判断共享资源是否有写操作发生)。

  首先通过tryOptimisticRead()获取,然后通过validate()校验,通过说明当前读锁可用,直接返回,否则,就通过readLock()方法进入阻塞,等待CPU调度,直到获取到读锁。

public static int consume(){
    //乐观锁,立即返回,并不会导致当前线程进入阻塞等待,
    //该方法会返回一个long型的数据戳(stamp),如果获取成功,则数据戳为非0,如果失败,则数据戳为0。
    long stamp = lock.tryOptimisticRead();
    if(lock.validate(stamp)){//校验通过,即当前没有写线程在操作变量
        return shareData;
    }else{//如果有写锁在使用,就使用readLock()获取读锁,进入阻塞,等待CPU调度,直到获取到读锁
        stamp = lock.readLock();
        try{
            return shareData;
        }finally {
            //释放锁
            lock.unlock(stamp);
        }
    }
}
六、总结

  我们这篇内容主要学习了Lock锁及其实现类ReentrantLock、ReentrantReadWriteLock和StampedLock的基本用法,关于实现原理和读写锁的饥饿写问题产生的原因等,我们在后续内容中逐渐进行整理。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1024055.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号