Java领域中的线程机制-管程
我们都知道,经过多年的发展和无数Java开发者的不懈努力,Java已经由一门单纯的计算机编程语言,逐渐演变成一套强大的以及仍在可持续发展中的技术体系平台。
虽然,Java设计者们根据不同的技术规范,把Java划分为3种结构独立且又彼此依赖的技术体系,分别是Java SE,Java EE 以及Java ME,其中Java EE 在广泛应用在企业级开发领域中。
除了包括Java API组件外,其衍生和扩充了Web组件,事务组件,分布式组件,EJB组件,消息组件等,并且持续发展到如今,其中,虽然有许多组件现如今不再适用,但是许多组件在我们日常开发工作中,扮演着同样重要的角色和依旧服务着我们日新月异的业务需求。
综合Java EE的这些技术,我们可以根据我们的实际需要和满足我们的业务需求的情况下,可以快速构建出一个具备高性能,结构严谨且相对稳定的应用平台,虽然现在云原生时代异军突起许多基于非Java的其他技术平台,但是在分布式时代,Java EE是用于构建SOA架构的首先平台,甚至基于SpringCloud构建微服务应用平台也离不开Java EE 的支撑。
个人觉得,Java的持续发展需要感谢Google,正是起初Google将Java作为Android操作系统的应用层编程语言,使得Java可以在PC时代和移动互联网时代得到快速发展,可以用于手持设备,嵌入式设备,个人PC设备,高性能的集群服务器和大型机器平台。
当然,Java的发展也不是一帆风顺的,也曾被许多开发者诟病和嫌弃,但是就凭Java在行业里能否覆盖的场景来说,对于它的友好性和包容性,这不由让我们心怀敬意。其中,除了Java有丰富的内置API供我们使用外,尤其Java对于并发编程的支持,也是我们最难以释怀的,甚至是我们作为Java开发者最头疼的问题所在。
虽然,并发编程这个技术领域已经发展了半个世纪了,相关的理论和技术纷繁复杂。那有没有一种核心技术可以很方便地解决我们的并发问题呢?今天,我们就来一起走进Java领域的并发编程的核心——Java线程机制。
基本概述
在Java中,对于Java语言层面的线程,我们基本都不会太陌生,甚至耳熟能详。但是在此之前,我们先来探讨一下,什么是管程技术?Java 语言在 1.5 之前,提供的唯一的并发原语就是管程,而且 1.5 之后提供的 SDK 并发包,也是以管程技术为基础的。除此之外,其中C/C++、C# 等高级语言也都支持管程。
关于管程
管程(Monitor)是指定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。主要是指提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。
所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。
基本定义
首先,系统中的各种硬件资源和软件资源均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。
其次,可以利用共享数据结构抽象地表示系统中的共享资源,并且将对该共享数据结构实施的特定操作定义为一组过程。进程对共享资源的申请、释放和其它操作必须通过这组过程,间接地对共享数据结构实现操作。
然后,对于请求访问共享资源的诸多并发进程,可以根据资源的情况接受或阻塞,确保每次仅有一个进程进入管程,执行这组过程,使用共享资源,达到对共享资源所有访问的统一管理,有效地实现进程互斥。
最后,代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块,我们称之为管程,管程被请求和释放资源的进程所调用。
综上所述,管程(Monitor)是指定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。主要是指提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。
基本组成
由上述的定义可知,管程由四部分组成:
管程的名称;
局部于管程的共享数据结构说明;
对该数据结构进行操作的一组过程;
对局部于管程的共享数据设置初始值的语句
实际上,管程中包含了面向对象的思想,它将表征共享资源的数据结构及其对数据结构操作的一组过程,包括同步机制,都集中并封装在一个对象内部,隐藏了实现细节。
封装于管程内部的数据结构仅能被封装于管程内部的过程所访问,任何管程外的过程都不能访问它;反之,封装于管程内部的过程也仅能访问管程内的数据结构。
所有进程要访问临界资源时,都只能通过管程间接访问,而管程每次只准许一个进程进入管程,执行管程内的过程,从而实现了进程互斥。
基本特点
管程是一种程序设计语言的结构成分,它和信号量有同等的表达能力,从语言的角度看,管程主要有以下特点:
模块化,即管程是一个基本程序单位,可以单独编译;
抽象数据类型,指管程中不仅有数据,而且有对数据的操作;
信息屏蔽,指管程中的数据结构只能被管程中的过程访问,这些过程也是在管程内部定义的,供管程外的进程调用,而管程中的数据结构以及过程(函数)的具体实现外部不可见。
基本模型
在管程的发展史上,先后出现过三种不同的管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。其中,现在广泛应用的是 MESA 模型,并且 Java 管程的实现参考的也是 MESA 模型。
接下来,我们就针对几种管程模型分别来简单的说明一下,它们之间的区别。
假设有这样一个进程同步机制中的问题:如果进程P1因x条件处于阻塞状态,那么当进程P2执行了x.signal操作唤醒P1后,进程P1和P2此时同时处于管程中了,这是不被允许的,那么如何确定哪个执行哪个等待?
一般来说,我们都会采用如下两种方式来进行处理:
第一种方式:假如进程 P2进行等待,直至进程P1离开管程或者等待另一个条件
第二种方式:假如进程 P1进行等待,直至进程P2离开管程或者等待另一个条件
综上所述,三种不同的管程模型采取的方式如下:
1.Hasen 模型
Hansan管程模型,采用了基于两种的折中处理。主要是规定管程中的所有过程执行的signal操作是过程体的最后一个操作,于是,进程P2执行完signal操作后立即退出管程,因此进程P1马上被恢复执行。
2.Hoare 模型
Hoare 管程模型,采用第一种方式处理。只要进程 P2进行等待,直至进程P1离开管程或者等待。
3.MESA 模型
MESA 管程模型,采用第二种方式处理。只要进程 P1进行等待,直至进程P2离开管程或者等待。
基本实现
在并发编程领域,有两大核心问题:互斥和同步。其中:
互斥(Mutual Exclusion),即同一时刻只允许一个线程访问共享资源
同步(Synchronization),即线程之间如何通信、协作
这两大问题,管程都是能够解决的。主要是由于信号量机制是一种进程同步机制,但每个要访问临界资源的进程都必须自备同步操作wait(S)和signal(S)。
这样大量同步操作分散到各个进程中,可能会导致系统管理问题和死锁,在解决上述问题的过程中,便产生了新的进程同步工具——管程。其中:
信号量(Semaphere):操作系统提供的一种协调共享资源访问的方法。和用软件实现的同步比较,软件同步是平等线程间的的一种同步协商机制,不能保证原子性。而信号量则由操作系统进行管理,地位高于进程,操作系统保证信号量的原子性。
管程(Monitor):解决信号量在临界区的 PV 操作上的配对的麻烦,把配对的 PV 操作集中在一起,生成的一种并发编程方法。其中使用了条件变量这种同步机制。
综上所述,这也是Java中,最常见的锁机制的实现方案,即最典型的实现就是ReenTrantLock为互斥锁(Mutex Lock) 和synchronized 为同步锁(Synchronization Lock)。
具体表现
熟悉Java中synchronized 关键词的都应该知道,它是Java语言为开发者提供的同步工具,主要用来解决多线程并发执行过程中数据同步的问题,主要有wait()、notify()、notifyAll() 这三个方法。其中,最关键的实现是,当我们在代码中声明synchronized 之后,其被声明部分代码编译之后会生成一对monitorenter和monitorexit指令来指定某个同步块。
在JVM执行指令过程中,一般当遇到monitorenter指令表示获取互斥锁时,而当遇到monitorexit指令表示要释放互斥锁,这就是synchronized在Java层面实现同步机制的过程。除此之外,如果是获取锁失败,则会将当前线程放入到阻塞读队列中,当其他线程释放锁时,再通知阻塞读队列中的线程去获取锁。
由此可见,我们可以知道的是,synchronized 代码块是由一对 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元。
准确的说,JVM一般通过Monitor来实现monitorenter和monitorexit指令,而且Monitor 对象包括一个阻塞队列和一个等待队列。其中,阻塞队列用来保存锁竞争失败的线程,并且它处于阻塞状态,而等待队列则用来保存synchronized 代码块中调用wait方法后放置的队列,其调用wait方法后会通知阻塞队列。
当然,在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
这并不意味着,Java是提供信号量这种编程原语来支持解决并发问题的,虽然在《操作系统原理》中,我们知道用信号量能解决所有并发问题,但是在Java中并不是这样的。
其实,最根本的原因,就是Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程。
特别指出的是,相对于synchronized来说,ReentrantLock主要有以下几个特点:
从锁获取粒度上来看,比synchronized较为细,主要表现在是锁的持有是以线程为单位而不是基于调用次数。
从线程公平性上来看,ReentrantLock 可以设置公平性(fairness),能减少线程“饥饿”的发生。
从使用角度上来看,ReentrantLock 可以像普通对象一样使用,所以可以利用其提供的各种便利方法,进行精细的同步操作,甚至是实现 synchronized 难以表达的用例。
从性能角度上来看,synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大。虽然在 Java 6之后 中对其进行了非常多的改进,但在高竞争情况下,ReentrantLock 仍然有一定优势。
综上所述,我我相信你对Java中的管程技术已经有了一个明确的认识。接下来,我们便来进入今天的主题——Java线程机制。
更多关于“java培训”的问题,欢迎咨询千锋教育在线名师。千锋教育多年办学,课程大纲紧跟企业需求,更科学更严谨,每年培养泛IT人才近2万人。不论你是零基础还是想提升,都可以找到适合的班型,千锋教育随时欢迎你来试听。