Lock锁

Lock接口

1. 简介,地位,作用

锁是一种工具,用于控制对共享资源的访问

Lock不是用来替代Synchronized的,而是在Synchronized不满足的时候提供更多的高级功能的。

Lock接口中最常见的实现类是ReentrantLock.

通常情况下,Lock只允许一个线程访问这个共享资源,在有些时候,一些特殊实现也可以允许并发访问,比如ReadWriteLock中的ReadLock

2. Synchronized不够用?为什么需要Lock?

Synchronized效率低,不能设定超时,不能中断一个试图获得锁的线程获得锁。

Synchronized不够灵活,

Synchronized没法知道是否成功获取到了锁。

3. 方法介绍

lock()

获取锁,如果锁被其他线程占用,就等待。
lock一定得手动释放锁,通常在finally中释放。
lock()不能被中断,所以隐患是:陷入死锁,永久等待。

tryLock()

尝试获取锁。获取成功返回true,获取失败返回false。
可以根据获取结果决定后续的逻辑,怎么处理。
这个方法会立刻返回,拿不到锁不会一直等待

tryLock(long time,TimeUnit unit)

尝试获取锁,可以等一段时间,如果超时还没获取到就返回结果
false,如果在时间内拿到了锁,返回true。

if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
      //获取锁成功的代码段里面要try{}finally{}释放锁。
      try{
      //do some work
      } finally {
            lock.unLock();
      }
}

lockInterruptibly()

相当于tryLock(long time, TimeUnit unit)将time设置为无限大。在等待锁的过程中线程可以中断。

unlock() 解锁

建议在每次获取锁的时候先写finally{lock.unlock()},之后再写获得锁之后业务逻辑,这是一个好习惯。

4. 可见性保证

符合happens-before

锁的分类

乐观锁和悲观锁

乐观锁也叫非互斥同步锁,悲观锁也叫互斥同步锁

为什么会诞生非互斥同步锁(乐观锁)

互斥同步锁的劣势:

  • 阻塞和唤醒带来性能问题,互斥同步锁锁住之后就是独占的,其他线程想要同样的资源必需等待。涉及到上下文切换,带来性能问题。而乐观锁不需要挂起线程。
  • 互斥同步锁可能会陷入永久阻塞:比如遇到无限循环,死锁等活跃性问题。那等待锁的那几个悲催的线程永远也得不到执行。
  • 优先级反转:优先级高的线程等待优先级低的线程释放锁,如果低优先级的线程迟迟不释放, 高优先级的线程即使优先级更高,也得不到执行。

什么是乐观锁,悲观锁?

乐观锁:

  • 乐观锁,认为事情总是不大容易失败的,失败是小概率,先做事,遇到问题再面对。
  • 乐观锁不会锁住被操作对象。
  • 在更新的时候,去对比在我操作这段时间是否其他的线程修改过对象数据,如果没有修改过,就说明真的只有我自己在操作,那我就正产修改数据。
  • 如果数据和我一开始拿到的不一样,说明这段事件数据被其他线程改过了,那我就不能继续更新,此时可以采取放弃,报错,重试等等策略。
    乐观锁的实现一般都是利用CAS算法实现的。典型例子就是原子类,并发容器, Git

悲观锁:

  • 悲观锁,认为事情出问题是大概率的事件,出错可能是一种常态,担惊受怕。事无巨细都考虑的滴水不漏,保证万无一失,这个就是悲观锁的思想。
  • Java中悲观锁的实现就是Synchronized和Lock相关类

开销对比

悲观锁的原始开销要高于乐观锁,但是一劳永逸
相反,虽然乐观锁一开始的开销小于悲观锁,但是如果自旋时间很长或者不停重试,那消耗的资源也会越来越多。

两种锁各自的适用场景

悲观锁适用场景:

适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋消耗。
典型情况:

  1. 临界区有IO操作
  2. 临界区代码复杂或者循环量大
  3. 临界区竞争非常激烈

乐观锁适用场景:

适用并发写入少,大部分是读取的场景,不加锁能让读取性能大幅提高。

可重入锁和非可重入锁,以ReentrantLock为例

Reentrant:就是可重入的意思

ReentrantLock lock = new ReentrantLock();
//加锁几次
lock.lock();
lock.lock();
lock.lock();
//对应就要解锁几次
lock.unLock();
lock.unLock();
lock.unLock();

可重入锁ReentrantLock和非可重入锁ThreadPoolExecutor的Worker类,源码对比

可重入锁和不可重入锁源码对比.png

公平锁和非公平锁

什么是公平和非公平

公平指的是按照请求顺序分配锁。非公平指的是可以插队。
非公平也同样不提倡插队,“插队”也要在合适的时机插队,不能盲目插队。
非公平是为了避免唤醒带来的空档期,提高效率.

ReentrantLock默认是非公平的,但是可以通过给构造方法参数设置为true,成为公平的。
针对tryLock(),它不遵守设定的公平策略,tryLock() 的时候如果正好有线程释放锁,那么tryLock() 的线程就是获得锁,不管其他线程是否在队列里等待。

公平锁:每个线程总有执行的机会,但是稍慢,吞吐量小。
不公平锁:快,吞吐量大,但是线程可能会饥饿。

对比公平锁和非公平锁源码.png

共享锁和排它锁

以ReentrantReadWriteLock读写锁为例

排它锁

也叫独占锁,独享锁

共享锁

也称为读锁,获取共享锁之后,可以查看数据,但不能修改和删除,

ReentrantReadWriteLock中,读锁是共享锁,写锁是排它锁。

读写锁的规则

  1. 多个线程只申请读锁都可以申请到。
  2. 如果有一个线程已经占用了读锁,那其他线程如果要申请写锁,只能等待读锁释放。因为在别人读的时候,你来写是有风险的,所以等他读完才能写。
  3. 如果有线程已经占用了写锁,那其他线程不论读、写都得等待。
    简单说:要么多读,要么一写。

换一种方式理解:
读锁,写锁,只是一把锁,通过读锁定,写锁定这两种方式锁定。

ReentrantReadWriteLock

适用于读多,写少的情况

//创建读写锁,true代表是公平锁
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
//拿到读锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//拿到写锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

插队

公平锁:不允许插队
非公平锁:
1.写锁可以插队
2.读锁只有在等待队列头节点同样是想获得读锁的线程这种情况,才允许插队
不允许读锁插队,如果允许会造成想要写锁的线程饥饿

升降级

升级:获取了读锁,还要获得写锁
降级:获得了写锁,还想持有读锁

策略:允许降级,不允许升级
    //读锁升级,不允许
    private static void readUpgrading() {
        //获得读锁
        readLock.lock();
        try {
            //获得写锁
            writeLock.lock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
           //释放读锁
            readLock.unlock();
        }
    }

    //写锁降级,允许
    private static void readUpgrading() {
        //获得写锁
        writeLock.lock();
        try {
            //获得读锁
            readLock.lock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
           //释放读锁
            readLock.unlock();
            //释放写锁
            writeLock.unlock();
        }
    }

降级能提高效率,升级可能会产生死锁

自旋锁和阻塞锁

阻塞或唤醒线程需要切换CPU状态,这种状态转换需要耗费处理器时间,如果同步的代码内容过于简单,状态转换等待时间比代码执行的事件还要长,

为了让当前线程稍等一下,让当前线程自旋,如果前面的线程释放了锁,当前自旋的线程可以不必阻塞直接获得锁,从而避免切换线程的开销。

自旋锁的缺点:
如果锁被占用的时间很长,自旋会浪费处理器资源。自旋的过程,其实一直在消耗CPU,起始的开销低,但是随着自旋时间的增加,开销也是线程增长的。
自旋锁原理是CAS

可中断锁,不可中断锁

Synchronized即不可中断锁
Lock类即可中断锁

Java虚拟机对锁的优化

自旋锁和自适应

自适应就是多次自旋依然不能获得锁,就就转为阻塞

锁消除

有些场景不需要加锁,是安全的,jvm会把锁去掉

锁粗化

如果一片代码段反反复复加同一把锁,性能并不好,干脆给这一片代码加一个锁

写代码时如何优化锁

1.缩小同步代码块
2.尽量不锁方法
3.减少请求锁的次数
4.锁中尽量不包含锁,处理不好容易死锁
5.选择合适的锁类型,合适的工具类

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容