19.Lock锁介绍与应用案例

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了19.Lock锁介绍与应用案例相关的知识,希望对你有一定的参考价值。

在JUC中,Lock是一个接口,其作用与synchronized类似,都是为了保证线程安全而提前加锁,不同的是Lock只定义了与抢锁和释放锁相关的操作,没有具体实现,而且要用lock()和unlock()将要保护的代码给包起来才可以。其中常用的接口有三个:

  • lock():抢占锁资源方法,如果当前线程没有抢占到锁,则阻塞。

  • trylock():尝试抢占锁资源,如果抢占成功则返回true,否则返回false。

  • unlock():释放锁。

Lock既然是一个接口,那么必须有具体的实现才可以,常见的有三个:

  • 重入锁ReentrantLock

  • 读写锁ReentrantReadWriteLock

  • 读写锁的改进版StampedLock

1 重入锁ReentrantLock

ReentrantLock是一个支持重入的排他锁。所谓排他就是同一时刻只允许一个线程获得锁。

那重入是什么意思呢?意思是如果一个线程获得了锁,那么后面再访问相同的资源时,不需要再加锁就可以访问,一般此时会记录重入次数的。如果还是不清楚,我们看一个例子:

void update(a)
    insert(a);

这是我们常见的一种写法,在update某个记录的时候,我们可能在其内部要先做一个插入操作,一般来说,update和insert都需要先对资源进行加锁。如果操作的是同一个资源,比如这里的update对a加锁了,那执行insert时一定还没有释放,那insert该如何获得锁呢?

此时最好的方式就是让a在执行insert时可以直接获得锁资源,完成之后updat再继续执行,这就是重入的意思,synchronized和ReetrantLock都支持重入。

重入锁的例子:

public class ReentrantLockExample 
    static Lock lock=new ReentrantLock();
    private int count = 0;
    public void incr()
        lock.lock();
        try 
            count++;
        finally 
            lock.unlock();
        
    
    public static void main(String[] args) throws InterruptedException 
        ReentrantLockExample atomicExample = new ReentrantLockExample();
        Thread[] threads=new Thread[2];
        for (int j = 0;j<2;j++) 
            threads[j]=new Thread(() -> 
                for (int k=0;k<10000;k++) 
                    atomicExample.incr();
                
            );
            threads[j].start();
        
        threads[0].join();//保证线程执行结束
        threads[1].join();
        System.out.println(atomicExample.count);
    

上面的例子中,针对count++进行了加锁,从而保证count++在多线程访问下的线程安全性。

2 读写锁ReentrantReadWriteLock

ReentrantReadWriteLock表示可以重入的读写锁,那什么是读写锁呢?想象一下,开会的时候领导在画大饼,大家可以同时看,如果两个领导同时画,我们该怎么看呢?这就是读写的实际价值。读的时候可以多个人同时读,而写的时候只能一个人写。另外为了防止写的过程中,读到的信息可能前后不一致,因此一般写的时候也不能读,具体来说读写锁的特征就是:

  • 读/读不互斥,多个线程访问,线程不会阻塞。

  • 读/写互斥,如果一个读一个写,此时就要阻塞一个。

  • ReentrantReadWriteLock写/写互斥,多个线程同时写,要互斥

看个例子:

public class ReadWriteLockExample1 
    private final Lock lock=new ReentrantLock();
    private List<String> dataList=new ArrayList<>();
    public void add(String data)  
        lock.lock();
        try 
            dataList.add(data);
        finally 
             lock.unlock();
        
    
    
    public String get(int idx)
        lock.lock();
        try
            return dataList.get(idx);
        finally 
            lock.unlock();
        
    
    

这个例子中,dataList不是线程安全的,为此采用重入加锁机制,但是这里的get()和add()是同一级别的操作。如果get()操作很多,那么add()就会被阻塞。不过get方法本身不会影响到数据,因此对get的加锁是不必要的,因此我们需要一个更合理的加锁方法,这就是读写锁。

基于读写锁的代码如下:

public class ReadWriteLockExample 
    private final ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
    private final Lock readLock=readWriteLock.readLock();
    private final Lock writeLock=readWriteLock.writeLock();
    private List<String> dataList=new ArrayList<>();
    public void add(String data)  
        writeLock.lock();
        try 
            dataList.add(data);
        finally 
            try 
                Thread.sleep(5000);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            writeLock.unlock();
        
    
    public String get(int idx)
        readLock.lock();
        try
            return dataList.get(idx);
        finally 
            readLock.unlock();
        
    

上面代码中,通过读锁readLock和writeLock把读写的操作做了分离,从而减少读锁带来的竞争,其具体实现原理,我们后面章节再看。

3 StampedLock

ReentrantReadWriteLock有个问题,就是如果有get()操作,那么所有的add()必须等待,也就是读的过程中是不允许写的,而如果get()过多,就会导致写线程一直被阻塞。为了解决这个问题,Jdk8引入了StampedLock锁,对读写访问进行了优化,其基本原理是提供了一种乐观的策略,当有线程调用get()方法读取数据时,不会阻塞准备执行写操作的过程,具体使用方法如下:

public class StampedLockExample 
    private int balance;
    private StampedLock lock = new StampedLock();
    public StampedLockExample() 
        balance=10;
    

    public void conditionReadWrite (int value) 
        // 首先判断balance的值是否符合更新的条件
        long stamp = lock.readLock();
        while (balance > 0) 
            long writeStamp = lock.tryConvertToWriteLock(stamp);
            if(writeStamp != 0)  // 成功转换成为写锁
                stamp = writeStamp;
                balance += value;
                break;
             else 
                // 没有转换成写锁,这里需要首先释放读锁,然后再拿到写锁
                lock.unlockRead(stamp);
                // 获取写锁
                stamp = lock.writeLock();
            
        
        lock.unlock(stamp);
    

    public void optimisticRead() 
        long stamp = lock.tryOptimisticRead();
        int c = balance;
        // 这里可能会出现了写操作,因此要进行判断
        if(!lock.validate(stamp)) 
            // 要重新读取
            stamp = lock.readLock();
            try
                c = balance;
            
            finally
                lock.unlockRead(stamp);
            
        
        System.out.println("读取的值为:"+c);
    

    public void read () 
        long stamp = lock.readLock();
        lock.tryOptimisticRead();
        int c = balance;
        System.out.println("读取的值为:"+c);
        // ...
        lock.unlockRead(stamp);
    

    public void write(int value) 
        long stamp = lock.writeLock();
        balance += value;
        lock.unlockWrite(stamp);
    
    public static void main(String[] args) 
        StampedLockExample demo=new StampedLockExample();
        new Thread(new Runnable() 
            @Override
            public void run() 
                while(true)
                    demo.read();
                    demo.optimisticRead();
                    try 
                        Thread.sleep(1000);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            
        ).start();
        new Thread(new Runnable() 
            @Override
            public void run() 
                while(true)
                    demo.write(2);
                    demo.conditionReadWrite(3);
                    try 
                        Thread.sleep(1000);
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            
        ).start();

    

以上是关于19.Lock锁介绍与应用案例的主要内容,如果未能解决你的问题,请参考以下文章

Mysql 死锁过程及案例详解之记录锁与间隔锁Record Lock Gap Lock

灰度迎来最大解锁 市场反馈如何?

MongoDB Secondary 延时高(同步锁)案例分析

从构建分布式秒杀系统聊聊分布式锁

从构建分布式秒杀系统聊聊分布式锁

MySQL高级篇——锁的概述与案例应用