这是javaeye上很是经典的关于线程的帖子,写的很是通俗易懂的,适合任何读计算机的同窗. 线程同步
咱们能够在计算机上运行各类计算机软件程序。每个运行的程序可能包括多个独立运行的线程(Thread)。 线程(Thread)是一份独立运行的程序,有本身专用的运行栈。线程有可能和其余线程共享一些资源,好比,内存,文件,数据库等。
当多个线程同时读写同一份共享资源的时候,可能会引发冲突。这时候,咱们须要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢做一团。
同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为何要用这个很容易引发误解的词。既然你们都这么用,我们也就只好这么将就。
线程同步的真实意思和字面意思刚好相反。线程同步的真实意思,实际上是“排队”:几个线程之间要排队,一个一个对共享资源进行操做,而不是同时进行操做。
所以,关于线程同步,须要紧紧记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
关于线程同步,须要紧紧记住的第二点是 “共享”这两个字。只有共享资源的读写访问才须要同步。若是不是共享资源,那么就根本没有同步的必要。
关于线程同步,须要紧紧记住的第三点是,只有“变量”才须要同步访问。若是共享的资源是固定不变的,那么就至关于“常量”,线程同时读取常量也不须要同步。至少一个线程修改共享资源,这样的状况下,线程之间就须要同步。
关于线程同步,须要紧紧记住的第四点是:多个线程访问共享资源的代码有多是同一份代码,也有多是不一样的代码;不管是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就须要同步。
为了加深理解,下面举几个例子
有两个采购员,他们的工做内容是相同的,都是遵循以下的步骤:
(1)到市场上去,寻找并购买有潜力的样品。
(2)回到公司,写报告。
这两我的的工做内容虽然同样,他们都须要购买样品,他们可能买到一样种类的样品,可是他们绝对不会购买到
同一件样品,他们之间没有任何共享资源。因此,他们能够各自进行本身的工做,互不干扰。这两个采购员就至关于两个线程;两个采购员遵循相同的工做步骤,至关于这两个线程执行同一段代码。
下面给这两个采购员增长一个工做步骤。采购员须要根据公司的“布告栏”上面公布的信息,安排本身的工做计划。 这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。由于布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息 下面增长一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。
若是行政人员先到达布告栏,而且正在修改布告栏的内容。两个采购员这个时候,刚好也到了。这两个采购员就必须等待行政人员完成修改以后,才能观看修改后的信息。
若是行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员须要等待两个采购员把当前信息记录下来以后,才可以写上新的信息。
上述这两种状况,行政人员和采购员对布告栏的访问就须要进行同步。由于其中一个线程(行政人员)修改了共享资源(布告栏)。并且咱们能够看到,行政人员的工做流程和采购员的工做流程(执行代码)彻底不一样,可是因为他们访问了同一份可变共享资源(布告栏),因此他们之间须要同步。
同步锁
前面讲了为何要线程同步,下面咱们就来看如何才能线程同步。
线程同步的基本实现思路仍是比较容易理解的。咱们能够给共享资源加一把锁,这把锁只有一把钥匙。哪一个线程获取了这把钥匙,才有权利访问该共享资源。
生活中,咱们也可能会遇到这样的例子。一些超市的外面提供了一些自动储物箱。每一个储物箱都有一把锁,一把钥匙。人们可使用那些带有钥匙的储物箱,把东西放到储物箱里面,把储物箱锁上,而后把钥匙拿走。这样,该储物箱就被锁住了,其余人不能再访问这个储物箱。(固然,真实的储物箱钥匙是能够被人拿走复制的,因此不要把贵重物品放在超市的储物箱里面。因而不少超市都采用了电子密码锁。)
线程同步锁这个模型看起来很直观。可是,还有一个严峻的问题没有解决,这个同步锁应该加在哪里? 固然是加在共享资源上了。反应快的读者必定会抢先回答。
没错,若是可能,咱们固然尽可能把同步锁加在共享资源上。一些比较完善的共享资源,好比,文件系统,数据库系统等,自身都提供了比较完善的同步锁机制。咱们不用另外给这些资源加锁,这些资源本身就有锁。
可是,大部分状况下,咱们在代码中访问的共享资源都是比较简单的共享对象。这些对象里面没有地方让咱们加锁。
读者可能会提出建议:为何不在每个对象内部都增长一个新的区域,专门用来加锁呢?这种设计理论上固然也是可行的。问题在于,线程同步的状况并非很广泛。若是由于这小几率事件,在全部对象内部都开辟一块锁空间,将会带来极大的空间浪费。得不偿失。
因而,现代的编程语言的设计思路都是把同步锁加在代码段上。确切的说,是把同步锁加在“访问共享资源的代码段”上。这一点必定要记住,同步锁是加在代码段上的。
同步锁加在代码段上,就很好地解决了上述的空间浪费问题。可是却增长了模型的复杂度,也增长了咱们的理解难度。
如今咱们就来仔细分析“同步锁加在代码段上”的线程同步模型。
首先,咱们已经解决了同步锁加在哪里的问题。咱们已经肯定,同步锁不是加在共享资源上,而是加在访问共享资源的代码段上。
其次,咱们要解决的问题是,咱们应该在代码段上加什么样的锁。这个问题是重点中的重点。这是咱们尤为要注意的问题:访问同一份共享资源的不一样代码段,应该加上同一个同步锁;若是加的是不一样的同步锁,那么根本就起不到同步的做用,没有任何意义。
这就是说,同步锁自己也必定是多个线程之间的共享对象。
Java语言的synchronized关键字
为了加深理解,举几个代码段同步的例子。
不一样语言的同步锁模型都是同样的。只是表达方式有些不一样。这里咱们以当前最流行的Java语言为例。Java语言里面用synchronized关键字给代码段加锁。整个语法形式表现为
synchronized(同步锁) {
// 访问共享资源,须要同步的代码段
}
这里尤为要注意的就是,同步锁自己必定要是共享的对象。
… f1() {
Object lock1 = new Object(); // 产生一个同步锁
synchronized(lock1){
// 代码段 A
// 访问共享资源 resource1
// 须要同步
}
}
上面这段代码没有任何意义。由于那个同步锁是在函数体内部产生的。每一个线程调用这段代码的时候,都会产生一个新的同步锁。那么多个线程之间,使用的是不一样的同步锁。根本达不到同步的目的。
同步代码必定要写成以下的形式,才有意义。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 A
// 访问共享资源 resource1
// 须要同步
}
你不必定要把同步锁声明为static或者public,可是你必定要保证相关的同步代码之间,必定要使用同一个同步锁。
讲到这里,你必定会好奇,这个同步锁究竟是个什么东西。为何随便声明一个Object对象,就能够做为同步锁?
在Java里面,同步锁的概念就是这样的。任何一个Object Reference均可以做为同步锁。咱们能够把Object Reference理解为对象在内存分配系统中的内存地址。所以,要保证同步代码段之间使用的是同一个同步锁,咱们就要保证这些同步代码段的synchronized关键字使用的是同一个Object Reference,同一个内存地址。这也是为何我在前面的代码中声明lock1的时候,使用了final关键字,这就是为了保证lock1的Object Reference在整个系统运行过程当中都保持不变。
一些求知欲强的读者可能想要继续深刻了解synchronzied(同步锁)的实际运行机制。Java虚拟机规范中(你能够在google用“JVM Spec”等关键字进行搜索),有对synchronized关键字的详细解释。synchronized会编译成 monitor enter, … monitor exit之类的指令对。Monitor就是实际上的同步锁。每个Object Reference在概念上都对应一个monitor。
这些实现细节问题,并非理解同步锁模型的关键。咱们继续看几个例子,加深对同步锁模型的理解。
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 A
// 访问共享资源 resource1
// 须要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 B
// 访问共享资源 resource1
// 须要同步
}
}
上述的代码中,代码段A和代码段B就是同步的。由于它们使用的是同一个同步锁lock1。
若是有10个线程同时执行代码段A,同时还有20个线程同时执行代码段B,那么这30个线程之间都是要进行同步的。
这30个线程都要竞争一个同步锁lock1。同一时刻,只有一个线程可以得到lock1的全部权,只有一个线程能够执行代码段A或者代码段B。其余竞争失败的线程只能暂停运行,进入到该同步锁的就绪(Ready)队列。 每个同步锁下面都挂了几个线程队列,包括就绪(Ready)队列,待召(Waiting)队列等。好比,lock1对应的就绪队列就能够叫作lock1 - ready queue。每一个队列里面均可能有多个暂停运行的线程。 注意,竞争同步锁失败的线程进入的是该同步锁的就绪(Ready)队列,而不是后面要讲述的待召队列(Waiting Queue,也能够翻译为等待队列)。就绪队列里面的线程老是时刻准备着竞争同步锁,时刻准备着运行。而待召队列里面的线程则只能一直等待,直到等到某个信号的通知以后,才可以转移到就绪队列中,准备运行。
成功获取同步锁的线程,执行完同步代码段以后,会释放同步锁。该同步锁的就绪队列中的其余线程就继续下一轮同步锁的竞争。成功者就能够继续运行,失败者仍是要乖乖地待在就绪队列中。 所以,线程同步是很是耗费资源的一种操做。咱们要尽可能控制线程同步的代码段范围。同步的代码段范围越小越好。咱们用一个名词“同步粒度”来表示同步代码段的范围。
同步粒度
在Java语言里面,咱们能够直接把synchronized关键字直接加在函数的定义上。
好比。
… synchronized … f1() {
// f1 代码段
}
这段代码就等价于
… f1() {
synchronized(this){ // 同步锁就是对象自己
// f1 代码段
}
}
一样的原则适用于静态(static)函数
好比。
… static synchronized … f1() {
// f1 代码段
}
这段代码就等价于
…static … f1() {
synchronized(Class.forName(…)){ // 同步锁是类定义本
// f1 代码段
}
} 可是,咱们要尽可能避免这种直接把synchronized加在函数定义上的偷懒作法。由于咱们要控制同步粒度。同步的代码段越小越好。synchronized控制的范围越小越好。
咱们不只要在缩小同步代码段的长度上下功夫,咱们同时还要注意细分同步锁。
好比,下面的代码
public static final Object lock1 = new Object();
… f1() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 A
// 访问共享资源 resource1
// 须要同步
}
}
… f2() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 B
// 访问共享资源 resource1
// 须要同步
}
}
… f3() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 C
// 访问共享资源 resource2
// 须要同步
}
}
… f4() {
synchronized(lock1){ // lock1 是公用同步锁
// 代码段 D
// 访问共享资源 resource2
// 须要同步
}
}
上述的4段同步代码,使用同一个同步锁lock1。全部调用4段代码中任何一段代码的线程,都须要竞争同一个同步锁lock1。
咱们仔细分析一下,发现这是没有必要的。
由于f1()的代码段A和f2()的代码段B访问的共享资源是resource1,f3()的代码段C和f4()的代码段D访问的共享资源是resource2,它们没有必要都竞争同一个同步锁lock1。咱们能够增长一个同步锁lock2。f3()和f4()的代码能够修改成:
public static final Object lock2 = new Object();
… f3() {
synchronized(lock2){ // lock2 是公用同步锁
// 代码段 C
// 访问共享资源 resource2
// 须要同步
}
}
… f4() {
synchronized(lock2){ // lock2 是公用同步锁
// 代码段 D
// 访问共享资源 resource2
// 须要同步
}
} 这样,f1()和f2()就会竞争lock1,而f3()和f4()就会竞争lock2。这样,分开来分别竞争两个锁,就能够大大较少同步锁竞争的几率,从而减小系统的开销。
信号量
同步锁模型只是最简单的同步模型。同一时刻,只有一个线程可以运行同步代码。
有的时候,咱们但愿处理更加复杂的同步模型,好比生产者/消费者模型、读写同步模型等。这种状况下,同步锁模型就不够用了。咱们须要一个新的模型。这就是咱们要讲述的信号量模型。
信号量模型的工做方式以下:线程在运行的过程当中,能够主动停下来,等待某个信号量的通知;这时候,该线程就进入到该信号量的待召(Waiting)队列当中;等到通知以后,再继续运行。
不少语言里面,同步锁都由专门的对象表示,对象名一般叫Monitor。
一样,在不少语言中,信号量一般也有专门的对象名来表示,好比,Mutex,Semphore。
信号量模型要比同步锁模型复杂许多。一些系统中,信号量甚至能够跨进程进行同步。另一些信号量甚至还有计数功能,可以控制同时运行的线程数。
咱们没有必要考虑那么复杂的模型。全部那些复杂的模型,都是最基本的模型衍生出来的。只要掌握了最基本的信号量模型——“等待/通知”模型,复杂模型也就迎刃而解了。
咱们仍是以Java语言为例。Java语言里面的同步锁和信号量概念都很是模糊,没有专门的对象名词来表示同步锁和信号量,只有两个同步锁相关的关键字——volatile和synchronized。
这种模糊虽然致使概念不清,但同时也避免了Monitor、Mutex、Semphore等名词带来的种种误解。咱们没必要执着于名词之争,能够专一于理解实际的运行原理。
在Java语言里面,任何一个Object Reference均可以做为同步锁。一样的道理,任何一个Object Reference也能够做为信号量。
Object对象的wait()方法就是等待通知,Object对象的notify()方法就是发出通知。 具体调用方法为
(1)等待某个信号量的通知
public static final Object signal = new Object();
… f1() {
synchronized(singal) { // 首先咱们要获取这个信号量。这个信号量同时也是一个同步锁
// 只有成功获取了signal这个信号量兼同步锁以后,咱们才可能进入这段代码
signal.wait(); // 这里要放弃信号量。本线程要进入signal信号量的待召(Waiting)队列
// 可怜。辛辛苦苦争取到手的信号量,就这么被放弃了
// 等到通知以后,从待召(Waiting)队列转到就绪(Ready)队列里面
// 转到了就绪队列中,离CPU核心近了一步,就有机会继续执行下面的代码了。
// 仍然须要把signal同步锁竞争到手,才可以真正继续执行下面的代码。命苦啊。 … }
}
须要注意的是,上述代码中的signal.wait()的意思。signal.wait()很容易致使误解。signal.wait()的意思并非说,signal开始wait,而是说,运行这段代码的当前线程开始wait这个signal对象,即进入signal对象的待召(Waiting)队列。
(2)发出某个信号量的通知
… f2() {
synchronized(singal) { // 首先,咱们一样要获取这个信号量。同时也是一个同步锁。
// 只有成功获取了signal这个信号量兼同步锁以后,咱们才可能进入这段代码
signal.notify(); // 这里,咱们通知signal的待召队列中的某个线程。
// 若是某个线程等到了这个通知,那个线程就会转到就绪队列中
// 可是本线程仍然继续拥有signal这个同步锁,本线程仍然继续执行
// 嘿嘿,虽然本线程好心通知其余线程,
// 可是,本线程可没有那么高风亮节,放弃到手的同步锁
// 本线程继续执行下面的代码
… } }
须要注意的是,signal.notify()的意思。signal.notify()并非通知signal这个对象自己。而是通知正在等待signal信号量的其余线程。
以上就是Object的wait()和notify()的基本用法。
实际上,wait()还能够定义等待时间,当线程在某信号量的待召队列中,等到足够长的时间,就会等无可等,无需再等,本身就从待召队列转移到就绪队列中了。
另外,还有一个notifyAll()方法,表示通知待召队列里面的全部线程。
这些细节问题,并不对大局产生影响。
绿色线程
绿色线程(Green Thread)是一个相对于操做系统线程(Native Thread)的概念。
操做系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操做系统的线程,线程的运行和调度都是由操做系统控制的
绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操做系统的线程,而是由语言运行平台自身来调度。
当前版本的Python语言的线程就能够映射到操做系统线程。当前版本的Ruby语言的线程就属于绿色线程,没法映射到操做系统的线程,所以Ruby语言的线程的运行速度比较慢。
难道说,绿色线程要比操做系统线程要慢吗?固然不是这样。事实上,状况可能正好相反。Ruby是一个特殊的例子。线程调度器并非很成熟。
目前,线程的流行实现模型就是绿色线程。好比,stackless Python,就引入了更加轻量的绿色线程概念。在线程并发编程方面,不管是运行速度仍是并发负载上,都优于Python。
另外一个更著名的例子就是ErLang(爱立信公司开发的一种开源语言)。
ErLang的绿色线程概念很是完全。ErLang的线程不叫Thread,而是叫作Process。这很容易和进程混淆起来。这里要注意区分一下。
ErLang Process之间根本就不须要同步。由于ErLang语言的全部变量都是final的,不容许变量的值发生任何变化。所以根本就不须要同步。
final变量的另外一个好处就是,对象之间不可能出现交叉引用,不可能构成一种环状的关联,对象之间的关联都是单向的,树状的。所以,内存垃圾回收的算法效率也很是高。这就让ErLang可以达到Soft Real Time(软实时)的效果。这对于一门支持内存垃圾回收的语言来讲,可不是一件容易的事情。java