January 9, 2012
| 作者:白菜
|
分类:JAVA
简介: Java™ 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile 变量的同步性较差(但有时它更简单并且开销更低),而且其使用也更容易出错。在这期的Java 理论与实践中,Brian Goetz 将介绍几种正确使用 volatile 变量的模式,并针对其适用性限制提出一些建议。
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile 变量的模式,并强调了几种不适合使用 volatile 变量的情形。
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
Volatile 变量
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使用 volatile 还不足以实现计数器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile 变量还可以提供优于锁的性能优势。
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。
清单 1. 非线程安全的数值范围类@NotThreadSafe
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
阅读剩余部分...
January 9, 2012
| 作者:白菜
|
分类:JAVA
在Java Concurrency in Practice中是这样定义线程安全的:
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。
显然只有资源竞争时才会导致线程不安全,因此。
原子操作的描述是: 多个线程执行一个操作时,其中,那么这个操作就是原子的。
枯燥的定义介绍完了,下面说更枯燥的理论知识。
Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能。
程序执行最简单的模型是按照指令出现的顺序执行,这样就与执行指令的CPU无关,最大限度的保证了指令的可移植性。这个模型的专业术语叫做顺序化一致性模型。但是现代计算机体系和处理器架构都不保证这一点(因为人为的指定并不能总是保证符合CPU处理的特性)。
我们来看最经典的一个案例。
阅读剩余部分...
December 27, 2011
| 作者:白菜
|
分类:编程算法
进程和线程这对概念的理解也是很难的,至今网络上可查的资料对其的理解出入都挺大,在不同的操作系统中,如linux和windows中,其概念和实现都是有出入的。因此,我在这里结合我自己的理解谈下这两个概念,讲的都是一般性的概念,并且主要是基以WINDOWS的。
一般将进程定义为一个正在运行的程序的实例。我们在任务管理器重所看到的每一项,就可以理解为一个进程,每个进程都有一个地址空间,这个地址空间里有可执行文件的代码和数据,以及线程堆栈等。一个程序至少有一个进程。进程可以创建子进程,创建的子进程可以和父进程一起工作,也可以独立运行。
而线程是隶属于进程的,也就是说,线程是不能单独存在的,线程存在于进程中。每个进程至少有一个主线程,进程里的线程就负责执行进程里的代码,这也叫做进程的“惰性”。线程所使用的资源是它所属的进程的资源。线程也有自己的资源,主要组成部分就是一些必要的计数器和线程栈,占用的资源很少。我们可以理解为进程就是个容器,而线程才是真正干活的。线程可以在内核空间实现,也可以在用户空间实现。
这里特别提一下linux,在linux中,每一个进程都必须有一个父进程(如果没有父进程,则把PID=1的根进程作为其父进程)。进程退出了,就成了僵尸进程,等待父进程的退出信号,如果父进程没有给他发信号,得给他找一个父进程,或者等待内核自动销毁。Linux内核只提供了轻量进程的支持,限制了更高效的线程模型的实现,但Linux着重优化了进程的调度开销,使得linux进程切换开销较小。所以linux系统没有真正意义上的线程机制。linux中,Linux线程是通过进程来实现,进程和线程是同一个层次的。(我这里提到的是基于linux 2.6 内核的,在最新的LINUX中,对线程的实现做了优化,但是还是基于进程的。有些linux实现了新的线程机制,但主流还是和我描述的一样)
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这也是进程和线程的重要区别。
比如,windows中的explorer就是资源管理器进程,我们每打开一个窗口,这个进程就会创建一个线程。有上面的描述,我们可以知道,进程有独立的地址空间,多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
在第一节中,我提到过,“并发!=多线程“,是的。如Unix操作系统和Windows操作系统支持多用户、多进程的并发执行,而Java这样的语言支持应用程序进程内部的多个执行线程的并发执行。我们知道,php在语法上是没有多线程这个概念的,那java/NET的多线程是怎么实现的。实际上,java只是通过native方法调用操作系统API来创建多线程而已。另外,我们经常说某个网站并发是多少多少,nginx是高并发服务器了。。这里的并发和我前面的并发概念是不同的。要想做到高并发,除了多线程等的实现,还需要各种好的IO机制,不然线程再多也没用,资源利用跟不上来。高并发又跟同步、异步等IO复用机制有了牵连。
下面列举一下进程和线程中的一些概念。
进程:创建,销毁,创建子进程(fork),优先级。
线程:创建,挂起,恢复,销毁,切换,协作,睡眠,唤醒,等待,同步,锁,优先级。
从上面的描述,基本可以理清进程和线程的关系了。最后总结下:
1.linux没有内核上的多线程实现,但是不能说linux没有多线程。WINDOWS有内核的多线程实现。
2.进程开销大,线程开销小,但是linux的进程和线程开销差异不明显。
3.并行的实现离不开多线程。
再留个悬念,多线程的实现和运行还会涉及到同步异步,原子性等概念。这些概念是相互交叉的,这里先不做深入。
December 27, 2011
| 作者:白菜
|
分类:编程算法
最近正想整理下这个专题,想了下,主要有以下几组概念,我们时不时会听到,但只要一细想就会觉得困惑,觉得没那么简单。这几组概念有:
1.并发和并行
2.线程与进程
3.缓冲与缓存
4.同步与异步
5.阻塞与非阻塞
6.原子性与事务性
这几组概念,不仅概念间容易混淆,而且一组概念和另一组概念之间又存在交叉,极容易搞混。
------------------------------------------------------------------------
下面,就我所了解的知识,分门别类的谈一下这几个概念间的关系,如有不当之处,还望指出和补充。
1.并发( concurrency)和并行( Parallelism)
狭义的并发和并行属于操作系统里的概念。并发和并行是两个即有相同处,又有区别的概念,相同的地方在于他们都是指同时处理多个任务。不同的地方在于并发是逻辑上的,并行是物理上真真实实的。
我们都清楚,在单个CPU的情况下,对于到达CPU执行单元的作业(作业就是待执行的进程和线程),都是需要排队的,因为我只有一个窗口,你要吃饭,就必须到我这个窗口来,也就是说CPU资源是有限的。假如说这个时候,某个作业一直占着CPU资源不放,那么其他作业就得不到执行,也就是被阻塞,被挂起了。这就需要一种机制,保证每个作业都有机会被执行到,这种机制就叫做处理器的调度机制,其中又分为抢占式和非抢占式的调度。细分后又有先来先服务(老老实实排队,谁先到就服务谁),最短作业优先(哪个好搞定就先服务谁),优先级调度,时间片调度等。并发就是通过时间片轮转的方式实现的,一个作业执行一段时间后被中断,换另外一个上去,大家都有机会被执行到。
时间片轮转,就涉及到一个进程切换的问题,频繁的切换必然带来性能开销。我们看到的一个程序一直在持续运行,但实际上并不是这样的,从CPU级别的粒度上讲,其实是多个程序间在快速的切换,只是这个切换的速度很快(想一下,CPU的主频现在都有2GHZ了),我们感觉不到而已。在早期的操作系统中,不支持多任务,一个任务在执行,其它任务就必须等待。
废话说了这么多,就是要说明,并发就是多个进程或线程在同时执行,但实际上,他们并不是真正的在同一个时间点上一起跑的,而是我跑一段时间后,接下来你跑,我再跑,看起来我们都在跑,这就是并发。
而并行,则是实实在在的多个进程同时跑在高速公路上,齐头并进。看下面的图,上面的是并发,下面的是并行。

通过我前面的描述,可以知道,并发是由CPU调度算法实现的,只需要一个CPU即可实现并发。而并行则要在多核或者多处理器情况下才能做到,单CPU是无法实现并行的,因为任一个时刻点上只有一个程序在处理器上运行。并行计算,这就是当今大型计算机所要实现的难题,如何让几千个CPU进行协作。并发和并行都需要处理器和OS的支持。
提到并发,就难免会提到多线程,并发!=多线程,多线程只是一种并发的实现模型。这里暂且留作后话。根据我们前面的描述,可以知道,时间片切换是有开销的,因此多线程不一定就能发挥出更佳的效率,特别是在单处理器的情况下。故,多线程也是不能盲目迷信的。
July 29, 2010
| 作者:白菜
|
分类:网页前端
| Browser |
HTTP/1.1 |
HTTP/1.0 |
| IE 6,7 |
2 |
4 |
| IE 8 |
6 |
6 |
| Firefox 3+ |
6 |
6 |
| Safari 3+ |
4 |
4 |
| Chrome 3+ |
4 |
4 |
| Opera 10+ |
4 |
4 |
扩展阅读:《Roundup on Parallel Connections》