管程
管程的概念
将锁机制和共享对象打包,让对象管理自己的同步机制。
管程组成
- 同步机制
- 对象
- 方法
管程提供的机制
空转锁:线程无法立即获得锁时,空转,不断得检测是否空闲
阻塞锁:暂时放弃处理进入休眠。
阻塞锁
用条件(Condition)对象来定义阻塞锁。相当于开创了一个线程等待区,当线程可以执行的时候,唤醒线程。
组成
- void await():线程休眠,进入条件对象定义的等待区。
- void signal():唤醒等待区内一个线程
- void signalAll():唤醒等待区内的所有线程
唤醒线程的两种方式处理
背景
假设线程A为唤醒者,调用signal()唤醒线程B,那么线程B为被唤醒者,同一时间内管程里只能有一个锁。
唤醒方式
- 非阻塞条件:唤醒者A继续执行,直到唤醒者A释放锁,被唤醒者B在竞争的进入管程。
- 阻塞条件:唤醒者A进入等待区,直到 被唤醒者B 释放锁。唤醒者A竞争的进入管程。
管程的工作流程
线程申请锁
线程获得锁,进入管程
线程调用await(),释放锁然后退出管程进入等待集。
线程被唤醒,获得锁,返回管程。
线程释放锁,退出管程。
经典锁
有界FIFO队列
Class LockedQueue<T> {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition(); //满的条件对象
final Condition notEmpty = lock.newCondition(); //空的条件对象
final T[] items;
int tail, head, count; …
}
public LockedQueue(int capacity) { items = (T[]) new Object[capacity]; }//构造函数,初始化管程
public void enq(T x) {
lock.lock();
try {
while (count == items.length) notFull.await(); //满则等待
Enqueue x;
++count;
notEmpty.signal(); //唤醒等待 notEmpty 的线程
} finally { lock.unlock(); }
}
public T deq() {
lock.lock();
try {
while (count == 0) notEmpty.await(); //空则等待
Dequeue the first element;
--count;
notFull.signal(); //唤醒等待 notFull 的线程
Return the element dequeued;
} finally { lock.unlock(); }
}
缺陷
在调用 signal()方法唤醒等待线程时,可能有等待线程会一直无法被唤醒。
改进
改为入队时,当count值从0变为1的时候就唤醒一个线程。
public void enq(T x) {
lock.lock();
try {
while (count == items.length)
notFull.await(); //满则等待
Enqueue x;
++count;
if (count == 1) {
notEmpty.signal(); //仅当队列从空变为非空时,唤醒等待中的线程
}
} finally {
lock.unlock();
}
}
缺陷
仍然存在唤醒丢失的问题
1.有四个线程ABCD,开始线程A和线程B调用deq。
2.此时队列为空,线程A和线程B进入等待。
3.线程C入队一个元素,并唤醒线程A(此时count值为1,需要唤醒一个,假设唤醒A)。
4.线程A醒来速度慢,被线程D抢先入队一个元素(D不用唤醒,因为此时count为2)。
5.最终队列中有一个元素,但线程B无法被唤醒。
解决方法:
- 用signalAll()唤醒所有线程,但会产生额外的开销;
- 使用限时等待机制,但会产生额外的开销;
读写锁
共享对象特性:以读取占多数,少数写。
读写锁特性(多个读,一个写):
- 任何线程持有读锁或写锁的时候,其他线程都不能获得写锁(只要有一个线程持有锁,其他都无法写)。
- 任一线程持有写锁的时候,其他线程不能获得读锁(当一个线程在写,任何线程都不能进来);
写的线程要保证临界区绝对干净才能写(不存在任何读者和写者)。
读者只需要保证临界区没有写者就可以读(多读单写)
代码的结构
public class SimpleReadWriteLock implements ReadWriteLock {
public SimpleReadWriteLock() {}
class ReadLock implements Lock {} //内部类——读锁,不存在写者的时候,才能读。最后一个读者走的时候唤醒所有线程
class WriteLock implements Lock {} //内部类——写锁,不存在写者 或者 读者数量为0才能写。
}
代码
public class SimpleReadWriteLock implements ReadWriteLock {
int readers; //记录有多少读者,writer == false 且 readers == 0 时可获得写锁
boolean writer; //记录是否有写者,writer == false 时可获得读锁
Lock lock; //同步所有的锁
Lock readLock, writeLock; //读锁,写锁
Condition condition; //条件对象,与 lock 关联
//--------------------------Init------------------------------------------------------Start
public SimpleReadWriteLock() {
writer = false; readers = 0; //初始没有写者,读者数量为0
lock = new ReentrantLock();
readLock = new ReadLock(); writeLock = new WriteLock();
condition = lock.newCondition(); //条件对象,与 lock 关联
}
//--------------------------Init------------------------------------------------------End
//--------------------------ReadLock------------------------------------------------------Start
class ReadLock implements Lock { //lock() 和 unlock()只能由内部类访问
public void lock() {
lock.lock();
try {
while (writer)
condition.await();//当不存在写者的时候,才能访问
readers++; //获得读锁,readers 计数器加 1
}
finally { lock.unlock(); }
}
public void unlock() {
lock.lock();
try {
readers--; //释放读锁,readers 计数器减 1
if (readers == 0) condition.signalAll();//唤醒等待 condition 的所有线程,可能有写者在等待
} finally { lock.unlock(); }
}
//--------------------------ReadLock------------------------------------------------------End
//--------------------------WriteLock------------------------------------------------------Start
class WriteLock implements Lock {
public void lock() {
lock.lock();
try {
while (writer || readers != 0)
condition.await(); //若有写者存在 或是读者没有清零,则等待
writer = true;
} finally { lock.unlock(); }
}
public void unlock() {
writer = false;
condition.signalAll();
}
}
//--------------------------WriteLock------------------------------------------------------End
}
缺陷
一半读线程比写线程多得多,可能导致写线程处于饥饿
读写锁的改进——公平读写锁
保证一旦写的线程申请写锁,就不再允许读者进入来读了。也就是有个线程想写,那么就直接卡住所有想要进来的读者,直到临界区的读者走完,自己就可以进入写了。
public class FifoReadWriteLock implements ReadWriteLock {
int readAcquires, readReleases; //已经进入了的读者数量,和已经释放了的读者数量
boolean writer;
Lock lock;
Condition condition;
Lock readLock, writeLock;
public FifoReadWriteLock() {
readAcquires = readReleases = 0;
writer = false; //writer 为 true 时,readAcquires 不再增加
lock = new ReentrantLock();
condition = lock.newCondition();
readLock = new ReadLock();
writeLock = new WriteLock();
}
...
}
private class ReadLock implements Lock {
public void lock() {
lock.lock();
try {
while (writer) condition.await(); //等待写者释放,此时readAcquires不再增加
readAcquires++;
} finally { lock.unlock(); }
}
public void unlock() {
lock.lock();
try {
readReleases++;
if (readAcquires == readReleases) condition.signalAll(); //离开的读者等于已经进入的读者,表示临界区读者已经走完了,唤醒所有
} finally { lock.unlock(); }
}
}
private class WriteLock implements Lock {
public void lock() {
lock.lock();
try {
while (writer) condition.await(); //等待写者释放
writer = true;
while (readAcquires != readReleases) condition.await();//等待读者清空
} finally { lock.unlock(); }
}
public void unlock() {
writer = false;
condition.signalAll();
}
}
改进之处
同时等待写者和读者释放锁 -> 等待写着释放锁,卡死读者进入,等待读者释放。
对比
改变前:
while (writer || readers != 0)
condition.await();
writer = true;
改变后:
while (writer)
condition.await(); //等待写者释放
writer = true;
while (readAcquires != readReleases) condition.await();
可重入锁
功能:
允许线程在不释放锁的情况下,重复申请获得锁。这样就防止了线程在递归或重复调用的时候不会陷入死循环。
本质:
设置一个owner来记录持有锁的线程ID,holdCount表示持有锁的线程重复获得的次数。当重复获得锁的时候,holdCount+1后直接结束,不做操作。
public class SimpleReentrantLock implements Lock {
Lock lock;
Condition condition;
int owner, holdCount; //owner记录当前持有锁的线程的ID,holdCount记录持有锁的线程重复获得过几次锁。
public SimpleReentrantLock() {
lock = new SimpleLock();
condition = lock.newCondition();
owner = 0;
holdCount = 0;
}
public void lock() {
int me = ThreadID.get();
lock.lock();
try {
if (owner == me) { //如果锁被持有在自己手中
holdCount++; //持有锁的记录数+1
return; //不再继续执行
}
while (holdCount != 0) condition.await(); //如果锁不在自己手中,且自己没有获得过锁,则等待其他人释放锁
owner = me; //将锁的持有者设为自己
holdCount = 1; //自己获得过锁的记录 设置为1
} finally { lock.unlock() }
}
public void unlock() {
lock.lock();
try {
if (holdCount == 0 || owner != ThreadID.get()) //如果没有获得过锁,且锁的持有者不是自己,此时自己还释放锁,抛出异常
throw new IllegalMonitorStateException();
holdCount--; //自己持有锁的记录数-1
if (holdCount == 0) { //如果持有锁的数量为0,表明该真正释放了。
condition.signal();
}
} finally { lock.unlock(); }
}
}
信号量机制
特点:
允许最多capacities个线程进入临界区。信号量是一个原子计数器,记录进入临界区的线程数。
public class Semaphore {
final int capacity;
int state;
Lock lock;
Condition condition;
public Semaphore(int c) { //初始化
capacity = c; //信号量容量
state = 0; //记录进入临界区的线程数
lock = new ReentrantLock();
condition = lock.newCondition();
}
public void acquire() {
lock.lock();
try {
while (state == capacity) condition.await(); //信号量满,等待
state++; //进入的线程数+1
} finally { lock.unlock(); }
}
public void release() {
lock.lock();
try {
state--; //进入的线程数-1
condition.signalAll(); //唤醒等待线程
} finally { lock.unlock(); }
}
}
本文来自博客园,作者:Laplace蒜子,转载请注明原文链接:https://www.cnblogs.com/RedNoseBo/p/16802710.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律