Java中的Lock


Java 中保证线程安全,操作同步的方法有很多种,比如:

  • 使用synchronized关键字
  • 使用Lock的实现类

其中synchronized属于语言级别的处理,无需我们去处理细节,而Lock则是一个接口,我们可以自定义Lock或者使用内置的Lock实现类,比如ReentrantLock等去精确控制同步。

概念/锁分类

悲观锁 VS 乐观锁

悲观锁和乐观锁并不是特指哪个锁(比如叫做悲观锁,乐观锁的类),而是并发情形下的两种不同策略。

  • 悲观锁

    每次线程去读数据的时候都认为会被其他线程修改,因此每次访问数据的时候都会上锁,其他线程如果想访问必须等到它释放锁;

  • 乐观锁

    每次线程去读数据的时候都认为不会被其他线程修改,因此不会上锁;如果线程想更新数据,会在更新前检查一下自己在读取和更新的这段时间里有没有其他线程修改过这个数据,如果修改过,则重新读取,再次尝试更新,否则更新,依次循环;

总的来说,就是悲观锁阻塞事务,乐观锁回滚重试。

乐观锁比较适合用于修改比较少的情形,如果修改比较多,则冲突比较多,会降低性能,不如使用悲观锁。

CAS

即Compare And Swap,是用于实现多线程同步的原子指令。

  1. 比较:读取到一个值A,在将其更新为B之前,坚持原值是否仍为A
  2. 交换:如果是,则将A更新为B,结束,否则什么都不做;

上面的两个步骤是原子性的,在CPU看来就是一个指令。

CAS利用CPU指令,在硬件层面保证了操作的原子性。

CAS整个过程中并没有加锁,乐观锁就是基于CAS实现的。

CAS的使用可以参考AtomicInteger等数字并发类。

使用CAS可以有效解决并发的效率问题,但同时也会引入ABA问题,比如一个线程将A改成B又改回A,另一个线程对其进行更新,则会忽略这个操作,会产生问题。JDK中的实现类添加了特殊的标记,用来解决这个问题。

Java中几乎全部都是悲观锁,因为乐观锁本质上不是锁,只是CAS算法循环。

自旋锁、偏向锁、轻量级锁、重量级锁

synchronized关键字处理有以下几个情形:

  1. 初次执行synchronized代码块的时候,锁对象变成了偏向锁,通俗点说就是偏向于第一个获取它的线程,执行完同步块之后并不会主动释放锁,当第二次到达同步块的时候,此时线程判断持有锁的是自己,如果是正常往下执行,则由于之前没有释放锁,因此不必重新加锁,如果始终只有一个线程,偏向锁几乎没有多余的开销,性能比较好;
  2. 一旦有第二个线程加入锁竞争(当某个线程获取锁的时候发现锁已经被占用,只能等待其释放),偏向锁就升级为轻量级锁(自旋锁)。在轻量级锁状态下的继续锁竞争,没有抢到锁的线程将自旋(循环判断锁是否能被成功获取),长时间的自旋是很消耗资源的,一个线程有锁,其余线程只能空耗CPU,这种情况叫做忙等。如果多个线程用一个锁,但是没有发生锁竞争,或发生了轻微的锁竞争,synchronized使用轻量级锁,允许短时间的忙等。
  3. 忙等是有限度的,某个达到最大自旋次数的线程,会将轻量级锁升级成重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

一个锁,只能按照偏向锁 –> 轻量级锁 –> 重量级锁的顺序逐渐升级,不允许降级。

可重入锁

允许同一个线程多次获取同一把锁,比如一个递归方法中有加锁操作,递归操作不阻塞自己,这种锁就叫做可重入锁。

Java中几乎所有的场景都只需要使用可重入锁,ReentrantLock等(以Reentraant开头的锁),以及JDK提供的所有Lock的实现类,包括synchronized都是属于可重入锁。

公平锁 VS 非公平锁

公平锁: 如果多个线程申请同一把公平锁,当锁释放的时候,先申请的线程先得到锁;

非公平锁:如果多个线程申请同一把非公平锁,当锁释放的时候,后申请的线程可能先获得到锁,其顺序是随机的,或者是根据指定的优先级顺序。

对于synchronized而言,它就是一个非公平锁,而且没有办法变成公平锁。

可中断锁

Java中并没有提供任何直接中断线程的方法,只提供了中断机制,即线程A向线程B发起中断请求,但线程B并不会立即停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略该中断。

如果线程A持有锁,线程B等待获取该锁,由于线程A持有锁的时间比较长,线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程请求中断B,这就是可中断锁。

Java中synchronized是不可中断锁,Lock的实现类都是可中断锁

读写锁、共享锁、互斥锁

读写锁其实是一对锁,即一个读锁(共享锁),一个写锁(互斥锁,排他锁)。

看看代码中的定义:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

可以看到读写锁接口只定义两个方法,一个用来返回读锁,一个用来返回写锁。

如果读取一个值是为了更新它,加锁的时候就加写锁;

如果读取一个值只是为了展示,加锁的时候可以加读锁;

读写锁是悲观锁策略。

Lock的定义

public interface Lock {

    /**
     * 获取锁.
     */
    void lock();

    /**
     * 获取锁,直到当前线程被标记为中断
     * {@linkplain Thread#interrupt interrupted}.
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 如果获取到了锁就返回true,否则立即返回false
     */
    boolean tryLock();

    /**
     * 超时时间内获取到锁,则返回true,否则返回false
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 释放锁.
     */
    void unlock();

    /**
     * 返回一个绑定到该Lock对象的Condition实例
     */
    Condition newCondition();
}

再来看看它的常见实现类:

Lock

参考文章


文章作者: 姜康
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 姜康 !
评论
 上一篇
调试flutter_tools 调试flutter_tools
在运行flutter命令的时候,比如flutter run,实际上执行的是flutter_tools.snapshots,而这个快照文件的源码入口就是flutter_tools.dart. 既然是普通的dart命令行程序,那么按照Dart命
2020-05-15
下一篇 
Java中的Map Java中的Map
哈希表/散列表 通俗的说就是,使用散列函数将key值映射到数组下标,这样就可以根据key值直接访问到元素存储位置,这种结构就叫哈希表(散列表)。 将key值映射到数组下标的函数就做散列函数,这个映射过程是一个key值压缩的过程,因而不可避
2020-05-15
  目录