知行

总结之后必有收获 开始使用

Java 并发 - - 锁

昨天有人问到了乐观锁和悲观锁的使用场景,虽然有所了解但是在 Java 中关于锁的概念还有很多,今天趁着妇女之友的节日总结一下。

CAS

CAS 全称 Compare And Swap(比较与交换)是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent 包中的原子类就是通过 CAS 来实现了乐观锁。

AQS

AQS 是 AbustactQueuedSynchronizer(抽象队列同步器)的简称,用来构建锁或者其他同步组件的基础框架,用一个 int 类型的 volatile 变量(命名为 state)来维护同步状态,并提供了一系列的 CAS 操作来管理这个同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。AQS 的主要作用是为 Java 中的并发同步组件提供统一的底层支持,例如 ReentrantLock,CountdowLatch 就是基于 AQS 实现的,用法是通过继承 AQS 实现其模版方法,然后将子类作为同步组件的内部类。
image.png

concurrent 包的实现结构如下图所示,AQS、非阻塞数据结构和原子变量类等基础类都是基于 volatile 变量的读/写和 CAS 实现,而像 Lock、同步器、阻塞队列、Executor 和并发容器等高层类又是基于基础类实现。
image.png

乐观锁 vs 悲观锁

乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度,在 Java 和数据库中都有此概念对应的实际应用。

乐观锁

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
传统的 MySQL 关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 的同步 synchronized 关键字的实现就是典型的悲观可重入锁。
悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

公平锁 vs 非公平锁

公平非公平锁也是很直观的从线程获取锁的优先顺序上表述。

公平锁

就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大

非公平锁

上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
java jdk并发包中的ReentrantLock可以指定构造函数的boolean类型来创建公平锁和非公平锁(默认),比如:公平锁可以使用new ReentrantLock(true)实现。

独享所 vs 共享锁

独享所

是指该锁一次只能被一个线程所持有。

共享锁

是指该锁可被多个线程所持有。
对于 Java ReentrantLock 而言,其是独享锁。但是对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的方法,来实现独享或者共享。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

我们以 ConcurrentHashMap 来说一下分段锁的含义以及设计思想,ConcurrentHashMap 中的分段锁称为 Segment,它即类似于 HashMap(JDK7 与 JDK8 中 HashMap 的实现)的结构,即内部拥有一个 Entry 数组,数组中的每个元素又是一个链表;同时又是一个 ReentrantLock(Segment 继承了 ReentrantLock)。

当需要 put 元素的时候,并不是对整个 hashmap 进行加锁,而是先通过 hashcode 来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程 put 的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

但是,在统计 size 的时候,可就是获取 hashmap 全局信息的时候,就需要获取所有的分段锁才能统计。

分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

评论
留下你的脚步
推荐阅读