并发编程是现代计算机软件最常见的编程模式,是操作系统到应用软件到分布式系统都要关心的问题,其核心目标是解决并行事件的资源共享和访问问题。
自从学生时代,我就一直在学习和并试图理解这些概念。尤其是在学操作系统时,曾经想得觉得自己脑壳都要爆炸了。许多东西是要随着知识和实际工作经验的增长才能领悟的。现在的我觉得现在对这些概念的理解已经超出当初的理解水平,虽然许多更深入的概念和细节我现在都还是很糊涂,但为了梳理这些知识,防止遗忘,在这儿把我对这些东西的理解,以及它们在Java中的应用讲出来。可惜现在操作系统的书已经很长时间不看了,肯定有许多不对的地方,如果您觉得不对,希望您能及时指出。
一切都要从这个世界的并行性讲开。事物的发展总是并行进行的,汽车在奔驰的同时,自行车也在行驶;别人正在唱歌,你可能正在吃饭;等等。这些都是并行的例子,就单个事物来看,它发展的动作一般是顺序进行,而多个事物之间一般谁也不会妨碍谁,除非它们的动作要作用在同一个对象上:汽车过十字路口的绿灯时,另一个方向的汽车就不能行驶;如果你在食堂里举办歌舞晚会,别人就不能在这儿吃饭。
计算机软件的基本模型是顺序执行的,然而现代计算机在此基础上发展出了并发概念。什么是并发?什么是并行?它们之间的区别是什么?并发与并行是两个既相似而又不相同的概念:并发性,又称共行性,是指能处理多个同时性活动的能力;并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。并行一般是指没有互斥和同步的情况下独立进行同时发生的事件。因此单CPU操作系统的进程/线程严格意义上来说都不能算是并行事件,毕竟它们都要使用同一个CPU,真正的并行出现在多处理器的计算机上,当进程/线程独立运行在不同的CPU上,而且没有不需要共享对象时。粗略的说,没有资源互斥共享的进程和线程都是并行的。
如果进程/线程没有共享任何数据,它们编程所关心的许多概念就不会存在。就像现实世界,如果任何两个人都是独立的,没有任何关系的,那么也不会存在社会的各种机构来协调这些关系。在操作系统中,进程之间共享数据的方式一般通过IO(如:文件、管道、网络端口等等),当然也有时会通过内存共享。这种松耦合的共享造成的同步、互斥问题并不多。常见的同步问题发生同一进程内不同线程之间。由于线程存在于同个进程中,它们之间是可以共享内存的,所以就会有很多同步和互斥的问题。
那么什么是同步,什么是互斥?同步和互斥往往是共生的。所谓的同步是指不同实体的动作按照某些特定条件的顺序执行。最常见的莫过于生产者和消费者之间的关系。生产者的生产动作和消费者的消费动作是必须满足先后顺序的:只有生产者生产出东西来,消费者才能消费这些东西。它们之间就需要所谓的同步。什么是互斥?互斥是指两个实体的动作不允许同时发生,如果同时发生就会产生不可以预期的结果。互斥是同步的前提,如果两个动作不是互斥的,就不可能保证其发生的顺序。同步一定是互斥的,而互斥不一定需要同步。同步是固定顺序的动作的互斥。理解这一点非常重要。
举个我们都熟悉的例子,多个并发生产者和多个并发消费者。生产者生产的对象放在一个数组中,而消费者则从这个数组中获取对象。那么生产者的生产动作之间是需要互斥的,但不需要同步。不管是A先放在数组,还是B先放在数组中,它们之间除了为避免将产品放在数组中同一位置上,需要互斥地访问数组外,是不需要规定哪一个在前面放,哪一个在后面放的。它们之间的关系就属于互斥。而生产者和消费者之间就存在一个先后顺序问题,必须至少有生产者生产出产品放在数组中,消费者才能开始消费。因此生产者生产和消费者消费之间的就是同步关系。此外,消费者和消费者之间的消费关系也是需要互斥的,这样才能避免两个消费者之间争夺同数组里同一位置的对象。但是它们的消费行为是不需要同步的,只要互斥的进行就行了。
因此不同实体之间的动作有两个基本关系:同步和互斥。一般处理同步的方法是建立在互斥的基础上的。互斥的机制一般需要通过操作系统甚至底层硬件提供的信号量、管程(管程是建立在信号量基础上的更高层构件)等底层机制来实现。Java语言中通过提供互斥原语synchronized(虽然叫同步,更准确的说应该是互斥mutex)来保证的,当然Java实际上是通过JVM的monitor_enter和monitor_exit指令来实现的,这些指令最终以底层操作系统提供的机制来实现。同步的实现除了要依靠互斥原语,还要结合条件判断和线程挂起等语言构件来实现。其原理比较简单,首先要通过原语synchronized互斥两个需要同步的动作(也称作临界代码),当某个动作(比如消费)获得信号锁进入管程时,它首先判断某个条件是否满足(是否有可消费对象),不满足则挂起当前线程,释放信号锁,允许其他线程进入。当其他线程(比如生产者)进入后,也是检查是否满足某些条件(比如数组是否有空闲),如果不满足则和前面线程一样释放信号锁并挂起线程,如果满足(有空闲)则进行动作(生产并放在数组空闲处),然后这个动作一般要负责激活其他挂起的线程(当然也可以不负责任,其结果是往往造成死锁),然后自己释放信号锁退出管程。其他被激活的线程进入下一轮竞争,谁获得信号锁后继续检查它需要的条件是否满足,如此继续下去。
自从学生时代,我就一直在学习和并试图理解这些概念。尤其是在学操作系统时,曾经想得觉得自己脑壳都要爆炸了。许多东西是要随着知识和实际工作经验的增长才能领悟的。现在的我觉得现在对这些概念的理解已经超出当初的理解水平,虽然许多更深入的概念和细节我现在都还是很糊涂,但为了梳理这些知识,防止遗忘,在这儿把我对这些东西的理解,以及它们在Java中的应用讲出来。可惜现在操作系统的书已经很长时间不看了,肯定有许多不对的地方,如果您觉得不对,希望您能及时指出。
一切都要从这个世界的并行性讲开。事物的发展总是并行进行的,汽车在奔驰的同时,自行车也在行驶;别人正在唱歌,你可能正在吃饭;等等。这些都是并行的例子,就单个事物来看,它发展的动作一般是顺序进行,而多个事物之间一般谁也不会妨碍谁,除非它们的动作要作用在同一个对象上:汽车过十字路口的绿灯时,另一个方向的汽车就不能行驶;如果你在食堂里举办歌舞晚会,别人就不能在这儿吃饭。
计算机软件的基本模型是顺序执行的,然而现代计算机在此基础上发展出了并发概念。什么是并发?什么是并行?它们之间的区别是什么?并发与并行是两个既相似而又不相同的概念:并发性,又称共行性,是指能处理多个同时性活动的能力;并行是指同时发生的两个并发事件,具有并发的含义,而并发则不一定并行,也亦是说并发事件之间不一定要同一时刻发生。并行一般是指没有互斥和同步的情况下独立进行同时发生的事件。因此单CPU操作系统的进程/线程严格意义上来说都不能算是并行事件,毕竟它们都要使用同一个CPU,真正的并行出现在多处理器的计算机上,当进程/线程独立运行在不同的CPU上,而且没有不需要共享对象时。粗略的说,没有资源互斥共享的进程和线程都是并行的。
如果进程/线程没有共享任何数据,它们编程所关心的许多概念就不会存在。就像现实世界,如果任何两个人都是独立的,没有任何关系的,那么也不会存在社会的各种机构来协调这些关系。在操作系统中,进程之间共享数据的方式一般通过IO(如:文件、管道、网络端口等等),当然也有时会通过内存共享。这种松耦合的共享造成的同步、互斥问题并不多。常见的同步问题发生同一进程内不同线程之间。由于线程存在于同个进程中,它们之间是可以共享内存的,所以就会有很多同步和互斥的问题。
那么什么是同步,什么是互斥?同步和互斥往往是共生的。所谓的同步是指不同实体的动作按照某些特定条件的顺序执行。最常见的莫过于生产者和消费者之间的关系。生产者的生产动作和消费者的消费动作是必须满足先后顺序的:只有生产者生产出东西来,消费者才能消费这些东西。它们之间就需要所谓的同步。什么是互斥?互斥是指两个实体的动作不允许同时发生,如果同时发生就会产生不可以预期的结果。互斥是同步的前提,如果两个动作不是互斥的,就不可能保证其发生的顺序。同步一定是互斥的,而互斥不一定需要同步。同步是固定顺序的动作的互斥。理解这一点非常重要。
举个我们都熟悉的例子,多个并发生产者和多个并发消费者。生产者生产的对象放在一个数组中,而消费者则从这个数组中获取对象。那么生产者的生产动作之间是需要互斥的,但不需要同步。不管是A先放在数组,还是B先放在数组中,它们之间除了为避免将产品放在数组中同一位置上,需要互斥地访问数组外,是不需要规定哪一个在前面放,哪一个在后面放的。它们之间的关系就属于互斥。而生产者和消费者之间就存在一个先后顺序问题,必须至少有生产者生产出产品放在数组中,消费者才能开始消费。因此生产者生产和消费者消费之间的就是同步关系。此外,消费者和消费者之间的消费关系也是需要互斥的,这样才能避免两个消费者之间争夺同数组里同一位置的对象。但是它们的消费行为是不需要同步的,只要互斥的进行就行了。
因此不同实体之间的动作有两个基本关系:同步和互斥。一般处理同步的方法是建立在互斥的基础上的。互斥的机制一般需要通过操作系统甚至底层硬件提供的信号量、管程(管程是建立在信号量基础上的更高层构件)等底层机制来实现。Java语言中通过提供互斥原语synchronized(虽然叫同步,更准确的说应该是互斥mutex)来保证的,当然Java实际上是通过JVM的monitor_enter和monitor_exit指令来实现的,这些指令最终以底层操作系统提供的机制来实现。同步的实现除了要依靠互斥原语,还要结合条件判断和线程挂起等语言构件来实现。其原理比较简单,首先要通过原语synchronized互斥两个需要同步的动作(也称作临界代码),当某个动作(比如消费)获得信号锁进入管程时,它首先判断某个条件是否满足(是否有可消费对象),不满足则挂起当前线程,释放信号锁,允许其他线程进入。当其他线程(比如生产者)进入后,也是检查是否满足某些条件(比如数组是否有空闲),如果不满足则和前面线程一样释放信号锁并挂起线程,如果满足(有空闲)则进行动作(生产并放在数组空闲处),然后这个动作一般要负责激活其他挂起的线程(当然也可以不负责任,其结果是往往造成死锁),然后自己释放信号锁退出管程。其他被激活的线程进入下一轮竞争,谁获得信号锁后继续检查它需要的条件是否满足,如此继续下去。