管程

管程的概念

将锁机制和共享对象打包,让对象管理自己的同步机制。


管程组成

  • 同步机制
  • 对象
  • 方法

管程提供的机制

空转锁:线程无法立即获得锁时,空转,不断得检测是否空闲

阻塞锁:暂时放弃处理进入休眠。

 

阻塞锁

用条件(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(); }

}

}

 

posted @   Laplace蒜子  阅读(61)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示