[并发编程] -- 锁篇
-
简介
- 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。
1)synchronized
关键字与java.util.concurrent.locks的lock接口
synchronized
关键字- JVM层面,会隐式地获取锁,获取和释放固化了,也就是先获取再释放。
- lock
- 是一个接口,它定义了锁获取和释放的基本操作。
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
-
2)队列同步器
-
2.1.简介
-
AbstractQueuedSynchronizer
,队列同步器,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO
队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。- getState():获取当前同步状态。
- setState(int newState):设置当前同步状态。
- compareAndSetState(int expect,intupdate):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
-
同步器提供的模板方法基本上分为3类:
- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况。
-
独占锁就是在同一时刻只能有
一个线程
获取到锁,而其他获取锁的线程只能处于同步队列中等待
,只有获取锁的线程释放了锁,后继的线程才能够获取锁。 -
2.2 实现
-
同步队列
- 依赖内部的同步队列(FIFO双向队列)来完成同步状态的管理
- 当前线程
获取同步状态失败
时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞
当前线程,当同步状态释放
时,会把首节点中的线程唤醒
,使其再次尝试获取
同步状态。 - 节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点。
- 节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),
没有成功获取同步状态的线程将会成为节点加入该队列的尾部
。
- 当前线程
- 依赖内部的同步队列(FIFO双向队列)来完成同步状态的管理
节点的属性类型与名称以及描述
同步队列结构图
CAS设置尾结点图
首节点的设置图
- 独占式同步状态获取与释放
- 线程获取同步状态失败后进行同步队列中,后续对线程进行
中断
操作时,线程不会从同步队列中移出
。 - 关于
acquireQueued(final Node node,int arg)
方法,只有前驱节点是头节点才能够尝试获取同步状态原因- 头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点。
- 维护同步队列的FIFO原则,便于对过早通知的处理。
- 线程获取同步状态失败后进行同步队列中,后续对线程进行
- 过早通知:指前驱节点不是头节点的线程由于中断而被唤醒
节点自旋获取同步状态图
/**
* 1.tryAcquire保证线程安全的获取同步状态(CAS)
* 2.假如1获取失败,以独占式Node.EXCLUSIVE构造同步节点,即是一个同个时刻只能有一个线程成功获取同步状态,并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部
* 3.调用acquireQueued(Node node,intarg)方法(自旋),使得该节点以“死循环”的方式获取同步状态
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
总结
- 在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;
- 移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时同步器调用
tryRelease(int arg)
方法释放同步状态,然后唤醒头节点的后继节点。
- 共享式同步状态获取与释放
- 同一时刻有多个线程同时获取到同步状态。
- 超时获取同步状态等同步器的核心数据结构与模板方法
- 独占式超时获取同步状态:通过调用同步器的
doAcquireNanos(int arg,long nanosTimeout)
方法可以
超时获取同步状态,即在指定的时间段内获取同步状态,如果获取到同步状态则返回true,否则,返回false。
- 独占式超时获取同步状态:通过调用同步器的
-
3)重入锁
- 该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
- synchronized关键字隐式的支持重进入。
- 重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
- 锁的最终释放:线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等
于0时表示锁已经成功释放。
- 公平锁:锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。(会进行大量的线程切换)
- 非公平锁:锁的获取顺序不按照请求的绝对时间顺序。(会造成线程“饥饿”,极少的线程切换,保证更大吞吐量)
-
4)读写锁
- 简介:读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
例子:
public class Cache { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); // 获取一个key对应的value public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } // 设置key对应的value,并返回旧的value public static final Object put(String key, Object value) { //其他线程对于读锁和写锁的均被阻塞 w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } // 清空所有的内容 public static final void clear() { w.lock(); try { map.clear(); } finally { w.unlock(); } } }
-
写锁:支持重进入的排它锁。
- 如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,
读锁已经被获取
(读状态不为0)或者该线程不是已经获取写锁的线程
,则当前线程进入等待状态。
- 如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,
-
存在读锁,则写锁不能被获取。
- 原因:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
-
锁降级:指的是写锁降级成为读锁。
- 指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
使用场景:
public void processData() { readLock.lock(); if (!update) { // 必须先释放读锁 readLock.unlock(); // 锁降级从写锁获取到开始 writeLock.lock(); try { if (!update) { // 准备数据的流程(略) update = true; } readLock.lock(); } finally { writeLock.unlock(); } // 锁降级完成,写锁降级为读锁 } try { // 使用数据的流程(略) } finally { readLock.unlock(); } }
- Condition接口
- Condition:一个同步队列跟多个等待队列。
Object的监视器方法与Condition接口的对比
明明可以靠才华吃饭,非要靠脸~