锁
对于多线程编程模型,一个少不了的概念就是锁 虽然叫做锁,但是其实相当于临界区大门的一个钥匙,那把钥匙就放到了临界区门口,有人进去了就把钥匙拿走揣在了身上,结束之后会把钥匙还回来 只有拿到了指定临界区的锁,才能够进入临界区,访问临界区资源,当离开临界区时,释放锁,其他线程才能够进入临界区 而对于锁本身,也是一种临界资源,是不允许多个线程共同持有的,同一时刻,只能够一个线程持有; 在前面的章节中,比如信号量介绍中,对于PV操作,就是对临界区资源的访问,下面的S就是临界区资源 Wait(S)和 signal(S)操作可描述为: wait(S): while (S<=0); S:=S-1; signal(S):S:=S+1; 但是上面的S,只是一种抽象的概念,在Java中如何表达? 换个问题就是:在Java中是如何描述锁这种临界区资源的? 其实任何一个对象都可以被当做锁 锁在Java中是对象头中的数据结构中的数据,在JVM中每个对象中都拥有这样的数据 如果任何线程想要访问该对象的实例变量,那么线程必须拥有该对象的锁(也就是在指定的内存区域中进行一些数据的写入) 当所有的其他线程想要访问该对象时,就必须要等到拥有该对象的锁的那个线程释放锁 一个线程拥有了一个对象的锁之后,他就可以再次获取锁,也就是平常说的可重入,如下图所示,两个方法同一个锁 假设methodA中调用了methodB(下面没调用),如果不可重入的话,一个线程获取了锁,进入methodA然后等待进入methodB的锁,但是他们是同一个锁 自己等待自己,岂不是死锁了?所以锁具有可重入的特性 对于锁的可重入性,JVM会维护一个计数器,记录对象被加锁了多少次,没有被锁的对象是0,后续每重入一次,计数器加1(只有自己可以重入,别人是不可以,是互斥的) 只有计数器为0时,其他的线程才能够进入,所以,同一个线程加锁了多少次,也必然对应着释放多少次 而对于这些事情,计数器的维护,锁的获取与释放等,是JVM帮助我们解决的,开发人员不需要直接接触锁 简言之,在对象头中有一部分数据用于记录线程与对象的锁之间的关系,通过这个对象锁,进而可以控制线程对于对象的互斥访问监视器
对于对象锁,可以做到互斥,但是仅仅互斥就足够了吗?比如一个同步方法(实例方法)以当前对象this为锁,如果多个线程过来,只有一个线程可以持有锁,其他线程需要等待 这个过程是如何管理的? 而且,在Java中,还可以借助于wait notify方法进行线程间的协作,这又是如何做到的? 其实在Java中还有另外一个概念,叫做监视器 《深入Java虚拟机》中如下描述监视器:可以将监视器比作一个建筑,它有一个很特别的房间,房间里有一些数据,而且在同一时间只能被一个线程占据。 一个线程从进入这个房间到它离开前,它可以独占地访问房间中的全部数据。 如果用一些术语来定义这一系列动作:
- 进入这个建筑叫做“进入监视器”
- 进入建筑中的那个特别的房间叫作“获得监视器”
- 占据房间叫做“持有监视器”
- 离开房间叫做“释放监视器”
- 离开建筑叫做“退出监视器”
简言之:Java的监视器就是管程的一种实现,借助于监视器可以实现线程的互斥与同步
监视区域
对于监视器“房间”内的内容被称为监视区域,说白了监视区域就是监视器掌管的空间区域 这个空间区域不管里面有多少内容,对于监视器来说,他们是最小单位,是原子的,是不可分割的代码,只会被同一个线程执行 不管你多少并发,监视器会对他进行保障 (对于开发者来说,你使用一个synchronized关键字就有了监视器的效果,监视器依赖JVM,而JVM依赖操作系统,操作系统则会进一步依赖软件甚至硬件,就是这样层层封装) 其实废话这么多,一个同步方法内(同步代码块)中所有的内容,就是属于同一个监视区域Java监视器逻辑
去医院就医时,有时需要进一步检查,现在你感冒有时都会让你查血  ̄□ ̄|| 大致的流程可能是这样子的: 挂号后,你会在医生办公室外等待医生叫号,医生处理(开化验单)后,你会去缴费,化验、等待结果等,拿到结果后,在重新回来进入医生办公室,当医生给当前的病人结束后,就会帮你看 (也有些医院取结果后也有报道机,会有复诊的队列,此处我只是举个例子,不要较真,我想你肯定见过这种场景:就是你挂号进去之后,医生旁边站了好几个人,那些要么是拿到结果回来的,要么是取药后回来咨询的) 在上面的流程中,相当于有两个队伍,一个是第一次挂号后等待叫号,另一个是医生诊治后还需要再次诊治的等待队伍 而对于Java监视器,其实也是类似这样一种逻辑(类似!) 当一个线程到达时,如果一个监视器没有被任何线程持有,那么可以直接进入监视器执行任务; 如果监视器正在被其他线程持有,那么将会进入“入口区域”,相当于走廊,在走廊排队等待叫号; 在监视器中执行的线程,也可能因为某些事情,不得不暂停等待,可以通过调用等待命令;比如经典的“读者--写者”问题,读者必须等待缓冲区“非满”状态,这就相当于大夫开出来了化验单,你要去化验,你要暂时离开医生,医生也就因此空闲了;此时这个线程就进入了这个监视器的“等待区域” 一旦离开,医生空闲,监视区域空出来了,所以其他的线程就有机会进入监视区域运行了; 一个监视区域内运行的线程,也可以执行唤醒命令,通过唤醒命令可以将等待区域的线程重新有机会进入监视区域 简言之- 一个监视区域前后各有一个区域:入口区域,等待区域:
- 如果监视区域有线程,那么入口区域需要等待,否则可以进入;
- 监视区域内执行的线程可以通过命令进入等待队列,也可以将等待队列的线程唤醒,唤醒后的线程就相当于是入口区域的队列一样,可以等待进入监控区域;
进出监视器流程
- 线程到达监控区域开始处,通过途径1进入入口区域,如果没有任何线程持有监控区域,通过途径2进入监控区域,如果被占用,那么需要在入口区域等待;
- 一个活动线程在监控区域内,有两种途径退出监控区域,当条件不满足时,可以通过途径3借助于等待命令进入等待或者顺利执行结束后通过途径5退出并释放监视器
- 当监视器空闲时,入口区域的等待集合将会竞争进入监视器,竞争成功的将会进入监控区域,失败的继续等待(如果有等待的线程被唤醒,将会一同参与竞争)
- 对于等待区域,要么通过途径3进入,要么通过途径4退出,只有这两条途径,而且只有一个线程持有监视器时才能执行等待命令,也只有再次持有监视器时才能离开等待区
- 对于等待区域中的线程,如果是有超时设置的等待,时间到达后JVM会自动通过唤醒命令将他唤醒,不需要其他线程主动处理
关于唤醒
JVM中有两种唤醒命令,notify和notify all,唤醒一个和唤醒所有 唤醒更多的是一种标志、提示、请求,而不是说唤醒后立即投入运行,前面也已经讲过了, 如果条件再次不满足或者被抢占。 对于JVM如何选择下一个线程,依照具体的实现而定,是虚拟机层面的内容。比如按照FIFO队列?按照优先级?各种权重综合?等等方式 而且需要注意的是,除非是明确的知道只有一个等待线程,否则应该使用notify all,否则,就可能出现某个线程等待的时间过长,或者永远等下去的几率。语法糖
对于开发者来说,最大的好处就是线程的同步与调度这些是内置支持的,监视器和锁是语言附属的一部分,而不需要开发者去实现 synchronized关键字就是同步,借助于他就可以达到同步的效果,这应该算是语法糖了 对于同步代码块,JVM借助于monitorenter和monitorexit,而对于同步方法则是借助于其他方式,调用方法前去获取锁 只需要如下图使用关键字 synchronized就好,这些指令都不需要我们去做有关锁的几个概念
- 死锁
- 锁死
- 活锁
- 饥饿
- 锁泄露