Java多线程专题5: JUC, 锁

什么是可重入锁、公平锁、非公平锁、独占锁、共享锁

可重入锁 ReentrantLock

A ReentrantLock is owned by the thread last successfully locking, but not yet unlocking it. A thread invoking lock will return, successfully acquiring the lock, when the lock is not owned by another thread. The method will return immediately

可重入锁, 指的是对同一个线程的可重入. 当一个线程获取锁并执行锁保护的代码区间后, 可以再次获取锁并执行此代码段, 这是很有用的特性.

公平锁与非公平锁

是指线程请求获取锁的过程中, 是否允许插队.

在公平锁上, 线程将按他们发出请求的顺序来获得锁;而非公平锁则允许在线程发出请求后立即尝试获取锁, 如果可用则可直接获取锁, 尝试失败才进行排队等待。

ReentrantLock提供了两种锁获取方式, FairSyn和NofairSync。

独占锁

每次只能有一个线程能持有锁, ReentrantLock就是以独占方式实现的互斥锁。

共享锁

允许多个线程同时获取锁, 并发访问 共享资源, 如:ReadWriteLock


什么是乐观锁、悲观锁

回答这个问题, 先要了解一下提问的场景, 如果是数据库操作相关, 就按数据库操作的乐观锁悲观锁回答. 下面的内容是Java并发相关的回答.

悲观锁

每次只给一个线程使用, 其它线程阻塞, 用完后再把资源转让给其它线程, Java中synchronizedReentrantLock等独占锁就是悲观锁的实现.

乐观锁

不对资源上锁, 但是在更新的时候会判断一下更新是否成功, 可以使用版本号机制和CAS算法实现. Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式.


讲讲 ReentrantLock 原理?

ReentrantLock重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock. 通过重写锁获取方式和锁释放方式这两个方法实现了公平锁和非公平锁.

ReentrantLock实现的是独占锁, 这个锁是悲观锁. 获取锁的逻辑为:

  • lock方法调用CAS方法设置state的值, 如果state等于期望值0(代表锁没有被占用), 那么就将state更新为1(代表该线程获取锁成功), 然后执行setExclusiveOwnerThread方法直接将该线程设置成锁的所有者. 如果CAS设置state的值失败, 即state不等于0, 代表锁正在被占领着, 则执行acquire(1), 即下面的步骤
  • nonfairTryAcquire方法首先调用getState方法获取state的值, 如果state的值为0(之前占领锁的线程刚好释放了锁), 那么用CAS这是state的值, 设置成功则将该线程设置成锁的所有者, 并且返回true. 如果state的值不为0, 那就调用getExclusiveOwnerThread方法查看占用锁的线程是不是自己, 如果是的话那就直接将state + 1然后返回true, 如果state不为0且锁的所有者又不是自己, 那就返回false, 然后线程会进入到同步队列中.

释放锁的逻辑为

  • 判断当前线程是不是锁的所有者, 如果是则进行步骤2, 如果不是则抛出异常
  • 判断此次释放锁后state的值是否为0, 如果是则代表锁有没有重入, 然后将锁的所有者设置成null且返回true, 然后执行步骤3, 如果不是则代表锁发生了重入执行步骤4
  • 现在锁已经释放完, 即state=0, 唤醒同步队列中的后继节点进行锁的获取。
  • 锁还没有释放完, 即state!=0, 不唤醒同步队列

Semaphore 的内部实现是怎样的?

Semaphore内部维护了一组虚拟的许可, 许可的数量可以通过构造函数的参数指定。访问特定资源前, 必须使用acquire方法获得许可, 如果许可数量为0, 该线程则一直阻塞, 直到有可用许可。
访问资源后, 使用release释放许可。Semaphore和ReentrantLock类似, 获取许可有公平策略和非公平许可策略, 默认情况下使用非公平策略。

Conceptually, a semaphore maintains a set of permits. Each acquire blocks if necessary until a permit is available, and then takes it. Each release adds a permit, potentially releasing a blocking acquirer. However, no actual permit objects are used; the Semaphore just keeps a count of the number available and acts accordingly.
Semaphores are often used to restrict the number of threads than can access some (physical or logical) resource. For example, here is a class that uses a semaphore to control access to a pool of items:

A semaphore initialized to one, and which is used such that it only has at most one permit available, can serve as a mutual exclusion lock. This is more commonly known as a binary semaphore, because it only has two states: one permit available, or zero permits available. When used in this way, the binary semaphore has the property (unlike many java.util.concurrent.locks.Lock implementations), that the "lock" can be released by a thread other than the owner (as semaphores have no notion of ownership). This can be useful in some specialized contexts, such as deadlock recovery.

Semaphore implementation: inner class Sync implementation for extends AQS, use AQS variable state to store permits. Subclassed into fair and nonfair versions. The operations on state use compareAndSetState() method to ensure the atomicity.


谈谈读写锁 ReentrantReadWriteLock 原理?

  • 读写锁或者重入读写锁, 它维护了一个读锁和一个写锁, 它允许同一时刻被多个读线程访问, 而此时写线程不能获取到锁, 并且当写线程获取到锁时后续的读写线程都将被阻塞不能获取到锁.
  • 支持重入, 同一线程获取读锁后能够再次获取读锁; 同一线程获取写锁之后能够再次获取写锁和读锁.
  • 锁降级, 获取写锁, 获取读锁之后再释放写锁, 写锁能降级为读锁(线程同时获取读写锁时, 必须先获取写锁, 再获取读锁, 反过来会直接导致死锁.
  • 锁的获取支持线程中断, 且writeLock 中支持 Condition

Sync类继承于AbstractQueuedSynchronizer, 是NonfairSync和FairSync的基类, 是ReentrantReadWriteLock 的核心类
读写锁的获取次数存放在 AQS 里面的state上, state的高 16 位存放 readLock 获取的次数, 低16位存放 writeLock 获取的次数.
每个线程获得读锁的次数用内部类HoldCounter保存, 并且存储在ThreadLocal里面

In ReentrantReadWriteLock, there are 3 inner classes: Sync, ReadLock and WriteLock. Sync is a subclass of AbstractQueuedSynchronizer.

Sync

The implementation of Sync is a bit interesting. the state of AQS is an int value, while in Sync it is splited into two unsigned shorts (by shifting 16 bit) , the lower one representing the exclusive (writer) lock hold count, and the upper the shared (reader) hold count.


StampedLock 锁原理的理解?

StampedLock是java8在java.util.concurrent.locks新增的一个类, 该类是读写锁的改进, 改进之处在于读不仅不阻塞读, 同时也不阻塞写, 在读的时候如果发生了写, 则重读, 而不是在读的时候阻塞写.

在之前的读写锁中, 在读线程非常多而写线程比较少的情况下, 写线程可能发生饥饿现象, 因为大量的读线程存在并且读线程都阻塞写线程, 因此写线程很可能很长时间不能调度成功. 改进之后执行读的时候另一个线程执行了写, 读线程会发现数据不一致, 这时候则执行重读即可.

所以使用StampedLock可以实现一种写保障, 即读写之间不会相互阻塞, 但是写和写之间还是阻塞的.

  • 所有获取锁的方法, 都返回一个Stamp, Stamp为0表示获取失败, 其余都表示成功.
  • 所有释放锁的方法, 都需要一个Stamp, 这个Stamp必须是和获取锁时得到的Stamp一致.
  • StampedLock是不可重入的, 如果一个线程已经持有了写锁, 再去获取写锁的话就会造成死锁.
  • StampedLock有三种访问模式:
    • Reading读模式: 功能和ReentrantReadWriteLock的读锁类似. 读锁是共享锁, 在没有线程获取写锁的情况下, 同时多个线程可以获取该锁.
    • Writing写模式: 功能和ReentrantReadWriteLock的写锁类似. 写锁是排它锁, 同时只有一个线程可以获取该锁, 当一个线程获取该锁后, 其它请求的线程必须等待
    • Optimistic reading 乐观读模式: 这是一种优化的读模式. 由于tryOptimisticRead并没有使用CAS设置锁状态所以不需要显示的释放该锁.
  • StampedLock支持读锁和写锁的相互转换. 我们知道ReentrantReadWriteLock中, 当线程获取到写锁后, 可以降级为读锁, 但是读锁是不能直接升级为写锁的.
  • StampedLock提供了读锁和写锁相互转换的功能, 使得该类支持更多的应用场景. 使用方法tryConvertToWriteLock(long stamp) If the lock state matches the given stamp, performs one of the following actions:
    • If the stamp represents holding a write lock, returns it.
    • Or, if a read lock, if the write lock is available, releases the read lock and returns a write stamp.
    • Or, if an optimistic read, returns a write stamp only if immediately available. This method returns zero in all other cases.
  • 无论写锁还是读锁, 都不支持Conditon等待

ReentrantReadWriteLock 在沒有任何读写锁时, 才可以取得写入锁, 这可用于实现了悲观读取(Pessimistic Reading), 即如果执行中进行读取时, 经常可能有另一执行要写入的需求, 为了保持同步, ReentrantReadWriteLock 的读取锁定就可派上用场。然而, 如果读取执行情况很多, 写入很少的情况下, 使用 ReentrantReadWriteLock 可能会使写入线程遭遇饥饿(Starvation)问题, 也就是写入线程吃吃无法竞争到锁定而一直处于等待状态。

StampedLock控制锁有三种模式(写, 读, 乐观读), 一个StampedLock状态是由版本和模式两个部分组成, 锁获取方法返回一个数字作为票据stamp, 它用相应的锁状态表示并控制访问, 数字0表示没有写锁被授权访问, 在读锁上分为悲观锁和乐观锁.

所谓的乐观读模式, 也就是若读的操作很多, 写的操作很少的情况下, 你可以乐观地认为, 写入与读取同时发生几率很少, 因此不悲观地使用完全的读取锁定, 程序可以查看读取资料之后, 是否遭到写入执行的变更, 再采取后续的措施(重新读取变更信息, 或者抛出异常) , 这一个小小改进, 可大幅度提高程序的吞吐量


分析下JUC 中倒数计数器 CountDownLatch 的使用与原理?

也是基于 AbstractQueuedSynchronizer, 用state记录许可数, 用于某个线程等待若干个线程执行完毕后, 它才执行的. 计数是不能够重用的, 只能用一次.


CountDownLatch 与线程的 Join 方法区别是什么?

CountDownLatch 可以用在多个线程开始时hold住线程, 让多个线程同时开始
CountDownLatch 可以在被等待的线程中任何时间执行countDown(), 不一定要等线程结束, 这个join是做不到的.


讲讲对JUC 中回环屏障 CyclicBarrier 的使用?

CyclicBarrier 是增长计数, 内部也是用ReentrantLock实现的,

  • 使用ReentrantLock保证每一次操作线程安全
  • 线程等待/唤醒使用Lock配合Condition来实现
  • 线程被唤醒的条件:等待超时或者所有线程都到达barrier

多个线程互相等待, 直到到达同一个同步点, 再继续一起执行


CyclicBarrier内部的实现与 CountDownLatch 有何不同?

对于CountDownLatch来说, 重点是“一个线程(多个线程)等待”, 而其他的N个线程在完成“某件事情”之后, 可以终止, 也可以等待。而对于CyclicBarrier, 重点是多个线程, 在任意一个线程没有完成, 所有的线程都必须互相等待, 然后继续一起执行。
CountDownLatch是计数器, 线程完成一个记录一个, 只不过计数不是递增而是递减, 而CyclicBarrier更像是一个阀门, 需要所有线程都到达, 阀门才能打开, 然后继续执行。

CountDownLatch是直接在AQS上实现的, 而CyClicBarrier是基于ReentrantLock


常见异步的手段有哪些?

  • 使用wait和notify
  • Lock.Condition
  • Future
  • CompletableFuture
  • FutureCallback
  • CountDownLatch
  • CyclicBarrier

posted on 2022-01-15 21:02  Milton  阅读(76)  评论(0编辑  收藏  举报

导航