在Java中,与线程通讯相关的几个方法,是定义在Object中的,你们都知道Object是Java中全部类的超类
在Java中,全部的类都是Object,借助于一个统一的形式Object,显然在有些处理过程当中能够更好地完成转换,传递,省去了一些没必要要的麻烦
另外有些东西,好比toString,的确是全部的类的特征
可是,为什么线程通讯相关的方法会被设计在Object中?
锁
对于多线程编程模型,一个少不了的概念就是锁
虽然叫作锁,可是其实至关于临界区大门的一个钥匙,那把钥匙就放到了临界区门口,有人进去了就把钥匙拿走揣在了身上,结束以后会把钥匙还回来
只有拿到了指定临界区的锁,才可以进入临界区,访问临界区资源,当离开临界区时,释放锁,其余线程才可以进入临界区
而对于锁自己,也是一种临界资源,是不容许多个线程共同持有的,同一时刻,只可以一个线程持有;
在前面的章节中,好比信号量介绍中,对于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中的监视器Monitor就是管程的概念,他是管程的一种实现
无论实现细节如何,无论对概念的实现程度如何,它的核心其实就是管程
在进程通讯的部分有介绍到:
“管程就是管理进程,管程的概念就是设计模式中“依赖倒置原则”,依赖倒置原则是软件设计的一个理念,IOC的概念就是依赖倒置原则的一个具体的设计
管程将对共享资源的同步处理封闭在管程内,须要申请和释放资源的进程调用管程,这些进程再也不须要自主维护同步。
有了管程这个大管家(秘书?)(门面模式?)进程的同步任务将会变得更加简单。
管程是墙,过程是门,想要访问共享资源,必须经过管程的控制(经过城墙上的门,也就是通过管程的过程)
而管程每次只准许一个进程进入管程,从而实现了进程互斥
管程的核心理念就是至关于构造了一个管理进程同步的“IOC”容器。”
简言之:Java的监视器就是管程的一种实现,借助于监视器能够实现线程的互斥与同步
监视区域
对于监视器“房间”内的内容被称为监视区域,说白了监视区域就是监视器掌管的空间区域
这个空间区域无论里面有多少内容,对于监视器来讲,他们是最小单位,是原子的,是不可分割的代码,只会被同一个线程执行
无论你多少并发,监视器会对他进行保障
(对于开发者来讲,你使用一个synchronized关键字就有了监视器的效果,监视器依赖JVM,而JVM依赖操做系统,操做系统则会进一步依赖软件甚至硬件,就是这样层层封装)
其实废话这么多,一个同步方法内(同步代码块)中全部的内容,就是属于同一个监视区域
Java监视器逻辑
去医院就医时,有时须要进一步检查,如今你感冒有时都会让你查血  ̄□ ̄||
大体的流程多是这样子的:
挂号后,你会在医生办公室外等待医生叫号,医生处理(开化验单)后,你会去缴费,化验、等待结果等,拿到结果后,在从新回来进入医生办公室,当医生给当前的病人结束后,就会帮你看
(也有些医院取结果后也有报道机,会有复诊的队列,此处我只是举个例子,不要较真,我想你确定见过这种场景:就是你挂号进去以后,医生旁边站了好几我的,那些要么是拿到结果回来的,要么是取药后回来咨询的)
在上面的流程中,至关于有两个队伍,一个是第一次挂号后等待叫号,另外一个是医生诊治后还须要再次诊治的等待队伍
而对于Java监视器,其实也是相似这样一种逻辑(相似!)
当一个线程到达时,若是一个监视器没有被任何线程持有,那么能够直接进入监视器执行任务;
若是监视器正在被其余线程持有,那么将会进入“入口区域”,至关于走廊,在走廊排队等待叫号;
在监视器中执行的线程,也可能由于某些事情,不得不暂停等待,能够经过调用等待命令;好比经典的“读者--写者”问题,读者必须等待缓冲区“非满”状态,这就至关于大夫开出来了化验单,你要去化验,你要暂时离开医生,医生也就所以空闲了;此时这个线程就进入了这个监视器的“等待区域”
一旦离开,医生空闲,监视区域空出来了,因此其余的线程就有机会进入监视区域运行了;
一个监视区域内运行的线程,也能够执行唤醒命令,经过唤醒命令能够将等待区域的线程从新有机会进入监视区域
简言之
- 一个监视区域先后各有一个区域:入口区域,等待区域:
- 若是监视区域有线程,那么入口区域须要等待,不然能够进入;
- 监视区域内执行的线程能够经过命令进入等待队列,也能够将等待队列的线程唤醒,唤醒后的线程就至关因而入口区域的队列同样,能够等待进入监控区域;
须要注意的是:
并非说监控区域内的线程必定要在或者会在最后一个时刻才会唤醒等待区域的线程,他随时均可以将等待区域内的线程唤醒
也就是说唤醒别人的同时,并不意味着他离开了监控区域,因此JVM的这种监控器实现机制也叫作“发信号并继续”
并且须要注意的是,等待线程并非唤醒后就当即醒来,当唤醒线程执行结束退出监视区域后,等待线程才会醒来
能够想一下,线程进入等待区域必然是有某些缘由不知足,因此才会等待,可是唤醒线程并非最后一步才唤醒的,既然是在继续执行,方才条件知足唤醒了,那如今是否还知足?另外若是唤醒线程退出监控区域以后,反而出现了第三个线程抢先进入了监控区域怎么办?这个线程也是有可能对资源进行改变的,执行结束后可能等待线程的条件是否仍旧仍是知足的?这都是不得而知的,因此也可能继续进入等待也可能退出等待区域,只能说除非逻辑有问题,否则只可以说在唤醒的那一刻,看起来是知足了的
进出监视器流程
- 线程到达监控区域开始处,经过途径1进入入口区域,若是没有任何线程持有监控区域,经过途径2进入监控区域,若是被占用,那么须要在入口区域等待;
- 一个活动线程在监控区域内,有两种途径退出监控区域,当条件不知足时,能够经过途径3借助于等待命令进入等待或者顺利执行结束后经过途径5退出并释放监视器
- 当监视器空闲时,入口区域的等待集合将会竞争进入监视器,竞争成功的将会进入监控区域,失败的继续等待(若是有等待的线程被唤醒,将会一同参与竞争)
- 对于等待区域,要么经过途径3进入,要么经过途径4退出,只有这两条途径,并且只有一个线程持有监视器时才能执行等待命令,也只有再次持有监视器时才能离开等待区
- 对于等待区域中的线程,若是是有超时设置的等待,时间到达后JVM会自动经过唤醒命令将他唤醒,不须要其余线程主动处理
关于唤醒
JVM中有两种唤醒命令,notify和notify all,唤醒一个和唤醒全部
唤醒更多的是一种标志、提示、请求,而不是说唤醒后当即投入运行,前面也已经讲过了, 若是条件再次不知足或者被抢占。
对于JVM如何选择下一个线程,依照具体的实现而定,是虚拟机层面的内容。好比按照FIFO队列?按照优先级?各类权重综合?等等方式
并且须要注意的是,除非是明确的知道只有一个等待线程,不然应该使用notify all,不然,就可能出现某个线程等待的时间过长,或者永远等下去的概率。
语法糖
对于开发者来讲,最大的好处就是线程的同步与调度这些是内置支持的,监视器和锁是语言附属的一部分,而不须要开发者去实现
synchronized关键字就是同步,借助于他就能够达到同步的效果,这应该算是语法糖了
对于同步代码块,JVM借助于monitorenter和monitorexit,而对于同步方法则是借助于其余方式,调用方法前去获取锁
只须要以下图使用关键字 synchronized就好,这些指令都不须要咱们去作
有关锁的几个概念
死锁
共享资源竞争时,好比两个锁a和b,A线程持有了a等待b,而B持有了b而等待a,此时就会出现互相等待的状况,这就叫作死锁
锁死
当一个线程等待某个资源时,或者等待其余线程的唤醒时,若是迟迟等不到结果,就可能永远的等待沉睡下去,这就是锁死
活锁
虽然线程一直在持续运行,处于RUNNABLE,可是若是任务迟迟不能继续进行,好比每次回来条件都不知足,好比一直while循环进行不下去,这就是活锁
饥饿
若是一个线程由于某种条件等待或者睡眠了,可是却再也没有获得CPU的临幸,迟迟得不到调度,或者永远都没有获得调度,这就是饥饿
锁泄露
若是一个线程得到锁以后,执行完临界区的代码,可是却并无释放锁,就会致使其余等待该锁的线程没法得到锁,这叫作锁泄露
总结
Java在语言级别支持多线程,是Java的一大优点,这种支持主要是线程的同步与通讯,这种机制依赖的就是监视器,而监视器底层也是对锁依赖的,对象锁是对监视器的支撑,也就是说,对象锁是根本,若是没有对象锁,根本就没有办法互斥,不能互斥的话,更别提协做同步了,监视器是构建于锁的基础上实现的一种程序,进一步提供了线程的互斥与协做的功能
开发时好比synchronized关键字的使用,底层也会依赖到监视器,好比两个线程调用一个对象的同步方法,一个进入,那么另外一个等待,就是在监视器上等待
在JVM中,每个类和对象在逻辑上都对应一个监视器
其实想要理解监视器的概念,仍是要理解管程的概念
而 wait方法和notify notifyAll方法不就是管程的过程吗?
管程就是至关于对于线程进行同步的一个“IOC”,借助于管程托管了线程的同步,若是想要深刻能够去研究下虚拟机
毕竟对于任何一种语言来讲,也都是一层层的封装最终转换为操做系统的指令代码,全部的这些功能在JVM层面看也毕竟都是字节码指令。
因此,说到这里,回到本文的最初问题上,“为何wait、notify、notifyAll 都是Object的方法”?
Java中全部的类和对象逻辑上都对应有一个锁和监视器,也就是说在Java中一切对象均可以用来线程的同步、因此这些管程(监视器)的“过程”方法定义在Object中一点也不奇怪
只要理解了锁和监视器的概念,就能够清晰地明白了