我们再回顾一下,原子性问题的根源是CPU切换线程执行指令所导致的,当前一个对共享变量的操作没有完成之前,CPU又切换到另外一个线程来操作对应的共享变量,那么最终产生的结果就可能出现问题。
比如如果现在有两个线程都在执行number=number+1,他们最终的结果可能还是为1,因为PU执行流程可能会如下,:
如解决原子性问题
从上面的案例看,原子性问题的丢失完全是因为CPU切换线程执行指令导致的,那么是否意味着只要禁止CPU切换线程执行指令就可以呢,结果是行不通的,禁止CPU切换指令在单核CPU的确可以解决这个问题,但是多核CPU的场景下,CPU可以同时调度多个线程执行指令,那么该问题还是存在的。
所以我们必须另找出路,回过头来思考,我们会发现一个共性,就是不管是线程切换还是多核CPU同时执行指令,其实根本原因就是,对于共享变量在修改操作,在一个线程没有完成之前,另外一个线程是可以同时介入操作,所以才会导致一个线程的结果可能被另外一个线程覆盖。如果从这个角度来考虑的话,那么是不是只要达成一个线程在操作共享变量的过程中,另外一个线程是不能介入操作,只有等前面一个线程执行完之后,后面的线程才可以操作,也就是让两个线程对于共享变量的操作是互斥的,那么问题就可以解决,而让两个线程操作互斥我们常用的手段就是“加锁”。
互斥锁
能保证多个线程(进程、操作者)对于共享变量(共享资源)的操作是互斥的也就是我们常说的“互斥锁”,锁是一个通用的概念在很多领域都有锁的机制、使用锁的目的也很简单,就是“保证操作的原子性”。
锁这个名字虽然很形象,但是类比到我们现实世界往往容易造成困惑,比如现实世界的门锁,我们开门的必须是用钥匙,而不是需要获取锁,而且现实世界一个锁会有多个钥匙,这在编程领域是不允许的,所以我更愿意把锁的意思解释成“使用权”。每个操作者需要操作共享资源时,必须首先获得这个共享资源的使用权才可以进行操作,而当一个人拥有了共享资源的使用权之后,另外一个人是想要操作共享资源就之后就只能等待前者操作结束后释放共享资源的使用权。
当我们对某个共享资源加锁之后,如果线程想要访问共享资源,那么它首先要拿到这个对象的锁,当某一个线程获取到锁时,它便可以访问共享资源, 没有获取到锁的线程只能等待,直到上一个线程执行完毕之后释放锁再进行下轮锁的竞争,因为只有一把锁,所以永远只会有一个线程操作该资源。加了锁之后那么最后执行的流程就如下:
管程模型
使用互斥锁是为了线程杜宇共享资源的互斥性,对于共享资源的操作只允许有一个线程进行。但是在锁的获得与释放线程之间需要如何进行配合和协调又是一个问题,这也就是线程“同步”问题,所以解决共享变量的访问过程的原子性其实需要解决两个问题,一个是线程之间的互斥,二是线程之间的协调同步。对于这两个问题计算机领域有有一种成熟的方法论来解决,它就是管程。
管程是一个抽象的概念模型,为了解决多个进程或线程同时访问一个共享资源时能达到"互斥"和"同步"的效果,,它定义了管理共享资源的访问过程的模型,任何语言都可用通过都可以通过这套模型编写出安全的并发程序,管程实现必须达到下面几点要求
1、管程中的共享变量对于外部都是不可见的,只能通过管程才能访问对应的共享资源(意思是共享变量的操作必须通过管程,无法通过其他途径操作)。
2、管程是互斥的,某个时刻只能允许一个进程或线程访问共享资源(线程对于管程的访问是互斥的)。
3、管程中需要有线程等待队列和相应等待和唤醒操作(没获得锁的线程放入一个队列中等待,等前一个线程释放锁后可以通过某种机制唤醒等待队列中的线程)。
4、必须有一种办法使进程无法继续运行时被阻塞(在程序要求的逻辑条件不满足的时候,可以使其阻塞)。
我们来理解下上面几个条件:
首先第1点 和第2点我们都能理解,只能通过管程访问共享资源,并且每次只能有一个线程获得管程的执行权,这两个要求理解起来很简单,其实就是为了让线程之间达到互斥的效果。
然后看第3点要求,管程中要有等待队列和响应的等待和唤醒操作,这个也好理解,等待队列和唤醒可以使线程之间达到同步有序的执行。
第4点是比较让人费解的,什么时候线程会无法继续运行呢?为什么要在这个时候提供线程可以进入阻塞的方法。
咱们看一个案例:
场景:假如我们正在开发一个互联网项目;
角色:项目参与人员有产品经理、开发人员、测试人员参与;
限制:只有一个办公室可以使用,一个办公室一次只能容纳一个角色进入。
节点: 每个角色负责对应的节点,产品经理产品文档、开发人员产出项目代码、测试人员测试代码质量、产品进行验收。
条件:开发人员必须有了产品文档之后再产出项目代码、测试人员在开发人员开发完毕了之后进入测试、产品人员在测试完毕了之后进行验收。
在这个场景里面,多个角色就是系统的多个线程,办公室是一个共享资源同一时刻只能有一个角色进入,这个场景里面就有一个阻塞场景,就是当一个开发人员抢到了办公室钥匙之后,进入到办公室,结果发现产品的需求都没有出来,这个时候开发人员是没有办法进行工作的,所以只能一直等,等到有产品文档之后继续下一步,但是这个时候产品是没办法进入办公室工作的,因为锁在开发人员手里,所以开发人员一直等不到需求文档,而产品经理一直进入不了办公室,导致死锁。
那么这里就需要有一种方式,当开发人员发现条件不成立的时候,此时开发人员可以主动的放弃办公室的锁,然后告诉办公室门口的产品经理,让产品经理先进办公室完成工作,开发人员自己则进入一个等待队列,当产品经理完成了工作之后,产品经理通知开发人员,然后自己放弃房间钥匙,等待需求验收再开始下一轮的工作。
最后以这种条件阻塞的方式让获得锁的线程可以主动让出锁,并等待其他线程唤醒再来检测条件,避免了某一个线程因为条件不满足导致任务无法进行,而因为别的线程无法进入到管程里,导致这个条件永远也无法改变锁造成的死锁问题。
下面这张图虽然不严谨,但是有助于你理解整个管程模型:
JAVA中的管程
通过上面的管程我们再来看JAVA里面的管程,JAVA是通过Synchronized关键字,和wait()、notify、notifyAll() 方法实现了整个管程模型, 与上面标准的管程模型不同的是,JAVA的Monitor属于一种简单的管程模型,因为它并没有使用多个条件变量的队列,不管是竞争锁产生的阻塞,还是拿到锁因为某个条件不合格导致的阻塞,统一都放入一个队列了。
下面我们同样通过一张图来理解: