【刨根问底】带你深入理解JUC并发工具类 — 信号量和管程

大家好,我是Java不惑(WX公众号同名),这是专栏的第三篇文章。

在前面的两篇文章中,我介绍了volatile、cas以及其在处理器中的实现。

我们需要知道,volatile和cas是最基础的工具,实际的业务场景中共享变量的同步问题是非常复杂的,所以很少直接使用它们来处理同步问题。

基于这个工具上面我们可以封装成“锁”,方便我们解决多线程同步问题。今天我会讲一下基于volatile+cas实现的同步机制,也就是信号量和管程。

信号量

信号量的概念是由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)于1965年发明,广泛的应用于不同的操作系统中。在系统中,给予每一个进程一个信号量,代表每个进程目前的状态,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量是一个任意的整数,通常被称为计数信号量。

信号量在《计算机操作系统》一书中应该已经学过了,下面我再简单介绍一下。

信号量可以看成特殊的变量,可以对它进行增加和减少的操作,并且是原子性的。

整型信号量

首先我看先看一下整型信号量,整型信号量定义为表示资源数目的整型量S,仅能通过原子操作P、V操作。

当线程想要使用共享变量时,会先获取信号量,如果拿到信号量,才被允许使用共享变量。

wait(S):
    while(S <= 0) {
        //循环等待
    }
    S = S - 1;
    // 使用共享变量
signal(S):
    S = S + 1;

举个例子解释一下:假设有一间会议室,因为会议室椅子有限,所以容纳的参会人员有限(不允许人站着),这时我们把会议室当做共享变量,参会人员当做线程。为了防止多个人(线程)同时进入会议室(共享变量),所以主办方在会议室门口放了一个盒子,盒子里面是一些门票(信号量S),小盒只允许一个人伸手拿门票(原子性),拿到门票就代表可以进入会议室。

当盒子里只有一张门票时(信号量S值为1),会议室(共享变量)只允许一个人(线程)进入,此时会议室(共享变量)是互斥访问的。

好了,上面就是整型信号量的介绍,使用整型信号量,我们就实现了一把非常简单的锁。我们抽象出来了门票的概念,想要同步的访问共享变量只需要获取到门票(信号量S)就可以了。

记录型信号量

上面的整型信号量是非常简陋的,并且还存在缺陷。比如在wait操作中,只要S<=0就会不断地循环,多个线程访问同一信号量,会严重消耗CPU资源。

解决方法也很简单,我们增加一个链表(队列)L,进程获取信号量资源失败,会调用block原语自我阻塞,并插入链表L中等待。

当其他线程释放信号量时,会执行signal操作,释放资源,这时链表L中如果存在进程,则调用wakeup原语,将链表中第一个等待进程唤醒,等待中的线程就会去尝试获取信号量。

AND型信号量

上述问题是解决多个进程共享同一个共享变量,如果多个进程共享多个共享变量,会出现死锁。

例如下面的例子,进程A获取S1的资源,进程B获取S2的资源,当进程A想要获取资源S2时,会因为获取不到而发生阻塞,这时进程B想要获取S1资源。因此在无外力作用下,两者都无法在阻塞状态中恢复而发生死锁。

process A: wait(S1);
process B: wait(S2);
process A: wait(S2);
process B: wait(S1);

AND同步机制的思想是:将进程在整个运行过程中所需要的资源,一致性分配给进程,如果有一个资源未能分配给进程,其他资源也需要释放。

wait(S1,……,Sn):
    if(S1 >= 1 and …… and Sn >= 1) {
        for(int i = 1; i <= n; i++) {
            Si = Si - 1;
        }
    } else {
        //放入等待队列中
    }
        
signal(S1,……,S3):
    for(int i = 1; i <= n; i++) {
        Si = Si + 1; 
    }
    //等待队列中移除

信号量可以解决并发中的同步问题,Java中仅JUC并发工具类中的Semaphore实现了信号量。 Java中的Semaphore用处并不大,多个线程同时访问临界区,同时进入的线程也会导致原子性问题。当然可以设置为仅允许一个线程进入,但这种情况下使用管程可能会更好。

Java中基于管程实现了锁,下面我们看一下管程是什么,以及在Java中的实现。

管程

管程(Monitor,也称为监视器)是东尼·霍尔与泊·派克·汉森提出的,并由泊·派克·汉森首次在并行Pascal中实现。东尼·霍尔证明了这与信号量是等价的。

对于管程,我们直接看下图(图片来源网络):

image

首先当多个线程想要进入临界区时,会首先获取到锁,如果获取不到锁就进入同步队列中,同步队列中的线程等待其他线程释放锁。拿到锁之后进入管程,进入后会判断条件变量(condition),如果不符合条件会进入等待队列中,要想再次获取锁,必须等待其他线程调用notify唤醒等待队列中的第一个线程,并进入同步队列中排队;或者等待其他线程调用notifyAll将所有等待队列中的线程放入同步队列中。

是不是想到了什么,Java中的synchronized就是使用了管程。

synchronized和管程

synchronized中,和上图不同,它仅存在一个条件变量(condition)。

Java中每个对象对应一个管程(monitor),所以在Object类中存在wait() 、notify()和notifyAll ()等方法都属于对象中的管程。

final Object lock = new Object();
synchronized (lock) {
    while(条件不满足) {
        lock.wait();
    }
    //处理完成
    //释放资源,条件满足
    lock.notifyAll();
}

如上面代码所示,线程进入管程时,会先加锁,如果获取到了锁就进入管程,如果获取不到锁,就进入同步队列中。

那么synchronized中的锁是怎么实现的呢?monitor中存在两个变量,一个是_owner变量,这个变量保存是哪个线程进入了管程,这个变量和信号量S是比较像的。因为synchronized是可重入锁,所以需要有一个计数器用于保存重入的次数,计数器这个变量是:count。

获取到锁之后,进入管程,也就是上面代码中synchronized修饰的大括号。

进入管程后,可以使用while()判断条件变量是否满足。当然在实际的开发过程中,条件变量(condition)使用次数还是比较少的。

如果条件不满足,会调用Object对象中的wait()方法进入等待队列中。

如果其他线程满足条件,调用notifyAll()后,会 将等待队列中的线程放入同步队列中,争用到锁之后进入管程继续执行。

上面的过程也解释了为什么wait() 、notify()和notifyAll ()需要获取到锁才能执行。

总结

在这篇文章中,我介绍了信号量和管程,以及管程在Java中的实现。希望你看完有所收获,受限于个人水平,文章若有错漏,还望读者不吝赐教。

在接下来的文章中,我将进入这个专栏的主题,也就是JUC并发类相关的介绍和实现。

最后,如果我的文章对你有帮助,请帮我点赞转发!

如果你对volatile和cas不熟悉,可以看第一篇文章《【刨根问底】带你深入理解JUC并发工具类 — volatile和cas》

如果你向了解一下valatile的实现原理,可以看一下第二篇文章《【刨根问底】带你深入理解JUC并发工具类 — 缓存一致性和内存屏障》

也可以访问该专栏的导航文章:《【刨根问底】带你深入理解JUC并发工具类 — 开篇》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值