一、线程类
Java是通过Java.lang.Thread类来实现多线程的,第个Thread对象描述了一个单独的线程。要产生一个线程,有两种方法:
1、需要从Java.lang.Thread类继承一个新的线程类,重载它的run()方法;
2、通过Runnalbe接口实现一个从非线程类继承来类的多线程,重载Runnalbe接口的run()方法。运行一个新的线程,只需要调用它的start()方法即可。如:
/**=====================================================================
* 文件:ThreadDemo_01.java
* 描述:产生一个新的线程
* ======================================================================
*/
class ThreadDemo extends Thread{
Threads()
{
}
Threads(String szName)
{
super(szName);
}
// 重载run函数
public void run()
{
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}
class ThreadMain{
public static void main(String argv[]){
ThreadDemo th = new ThreadDemo();
// 调用start()方法执行一个新的线程
th.start();
}
}
线程类的一些常用方法:
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
二、等待一个线程的结束
有些时候我们需要等待一个线程终止后再运行我们的另一个线程,这时我们应该怎么办呢?请看下面的例子:
/**=====================================================================
* 文件:ThreadDemo_02.java
* 描述:等待一个线程的结束
* ======================================================================
*/
class ThreadDemo extends Thread{
Threads()
{
}
Threads(String szName)
{
super(szName);
}
// 重载run函数
public void run()
{
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}
class ThreadMain{
public static void main(String argv[]){
//产生两个同样的线程
ThreadDemo th1 = new ThreadDemo();
ThreadDemo th2 = new ThreadDemo();
// 我们的目的是先运行第一个线程,再运行第二个线程
th1.start();
th2.start();
}
}
这里我们的目标是要先运行第一个线程,等第一个线程终止后再运行第二个线程,而实际运行的结果是如何的呢?实际上我们运行的结果并不是两个我们想要的直角三角形,而是一些乱七八糟的*号行,有的长,有的短。为什么会这样呢?因为线程并没有按照我们的调用顺序来执行,而是产生了线程赛跑现象。实际上Java并不能按我们的调用顺序来执行线程,这也说明了线程是并行执行的单独代码。如果要想得到我们预期的结果,这里我们就需要判断第一个线程是否已经终止,如果已经终止,再来调用第二个线程。代码如下:
/**=====================================================================
* 文件:ThreadDemo_03.java
* 描述:等待一个线程的结束的两种方法
* ======================================================================
*/
class ThreadDemo extends Thread{
Threads()
{
}
Threads(String szName)
{
super(szName);
}
// 重载run函数
public void run()
{
for (int count = 1,row = 1; row < 20; row++,count++)
{
for (int i = 0; i < count; i++)
{
System.out.print('*');
}
System.out.println();
}
}
}
class ThreadMain{
public static void main(String argv[]){
ThreadMain test = new ThreadMain();
test.Method1();
// test.Method2();
}
// 第一种方法:不断查询第一个线程是否已经终止,如果没有,则让主线程睡眠一直到它终止为止
// 即:while/isAlive/sleep
public void Method1(){
ThreadDemo th1 = new ThreadDemo();
ThreadDemo th2 = new ThreadDemo();
// 执行第一个线程
th1.start();
// 不断查询第一个线程的状态
while(th1.isAlive()){
try{
Thread.sleep(100);
}catch(InterruptedException e){
}
}
//第一个线程终止,运行第二个线程
th2.start();
}
// 第二种方法:join()
public void Method2(){
ThreadDemo th1 = new ThreadDemo();
ThreadDemo th2 = new ThreadDemo();
// 执行第一个线程
th1.start();
try{
th1.join();
}catch(InterruptedException e){
}
// 执行第二个线程
th2.start();
}
三、线程的同步问题
有些时候,我们需要很多个线程共享一段代码,比如一个私有成员或一个类中的静态成员,但是由于线程赛跑的问题,所以我们得到的常常不是正确的输出结果,而相反常常是张冠李戴,与我们预期的结果大不一样。看下面的例子:
/**=============================================================================
* 文件:ThreadDemo_04.java
* 描述:多线程不同步的原因
* =============================================================================
*/
// 共享一个静态数据对象
class ShareData{
public static String szData = "";
}
class ThreadDemo extends Thread{
private ShareData oShare;
ThreadDemo(){
}
ThreadDemo(String szName,ShareData oShare){
super(szName);
this.oShare = oShare;
}
public void run(){
// 为了更清楚地看到不正确的结果,这里放一个大的循环
for (int i = 0; i < 50; i++){
if (this.getName().equals("Thread1")){
oShare.szData = "这是第 1 个线程";
// 为了演示产生的问题,这里设置一次睡眠
try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 输出结果
System.out.println(this.getName() + ":" + oShare.szData);
}else if (this.getName().equals("Thread2")){
oShare.szData = "这是第 1 个线程";
// 为了演示产生的问题,这里设置一次睡眠
try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 输出结果
System.out.println(this.getName() + ":" + oShare.szData);
}
}
}
class ThreadMain{
public static void main(String argv[]){
ShareData oShare = new ShareData();
ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
ThreadDemo th2 = new ThreadDemo("Thread2",oShare);
th1.start();
th2.start();
}
}
由于线程的赛跑问题,所以输出的结果往往是Thread1对应“这是第 2 个线程”,这样与我们要输出的结果是不同的。为了解决这种问题(错误),Java为我们提供了“锁”的机制来实现线程的同步。锁的机制要求每个线程在进入共享代码之前都要取得锁,否则不能进入,而退出共享代码之前则释放该锁,这样就防止了几个或多个线程竞争共享代码的情况,从而解决了线程的不同步的问题。可以这样说,在运行共享代码时则是最多只有一个线程进入,也就是和我们说的垄断。锁机制的实现方法,则是在共享代码之前加入synchronized段,把共享代码包含在synchronized段中。上述问题的解决方法为:
/**=============================================================================
* 文件:ThreadDemo_05.java
* 描述:多线程不同步的解决方法--锁
* =============================================================================
*/
// 共享一个静态数据对象
class ShareData{
public static String szData = "";
}
class ThreadDemo extends Thread{
private ShareData oShare;
ThreadDemo(){
}
ThreadDemo(String szName,ShareData oShare){
super(szName);
this.oShare = oShare;
}
public void run(){
// 为了更清楚地看到不正确的结果,这里放一个大的循环
for (int i = 0; i < 50; i++){
if (this.getName().equals("Thread1")){
// 锁定oShare共享对象
synchronized (oShare){
oShare.szData = "这是第 1 个线程";
// 为了演示产生的问题,这里设置一次睡眠
try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 输出结果
System.out.println(this.getName() + ":" + oShare.szData);
}
}else if (this.getName().equals("Thread2")){
// 锁定共享对象
synchronized (oShare){
oShare.szData = "这是第 1 个线程";
// 为了演示产生的问题,这里设置一次睡眠
try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 输出结果
System.out.println(this.getName() + ":" + oShare.szData);
}
}
}
}
class ThreadMain{
public static void main(String argv[]){
ShareData oShare = new ShareData();
ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
ThreadDemo th2 = new ThreadDemo("Thread2",oShare);
th1.start();
th2.start();
}
}
由于过多的synchronized段将会影响程序的运行效率,因此引入了同步方法,同步方法的实现则是将共享代码单独写在一个方法里,在方法前加上synchronized关键字即可。
在线程同步时的两个需要注意的问题:
1、无同步问题:即由于两个或多个线程在进入共享代码前,得到了不同的锁而都进入共享代码而造成。
2、死锁问题:即由于两个或多个线程都无法得到相应的锁而造成的两个线程都等待的现象。这种现象主要是因为相互嵌套的synchronized代码段而造成,因此,在程序中尽可能少用嵌套的synchronized代码段是防止线程死锁的好方法。
在写上面的代码遇到的一个没能解决的问题,在这里拿出来,希望大家讨论是什么原因。
/**=============================================================================
* 文件:ThreadDemo_06.java
* 描述:为什么造成线程的不同步。
* =============================================================================
*/
class ThreadDemo extends Thread{
//共享一个静态数据成员
private static String szShareData = "";
ThreadDemo(){
}
ThreadDemo(String szName){
super(szName);
}
public void run(){
// 为了更清楚地看到不正确的结果,这里放一个大的循环
for (int i = 0; i < 50; i++){
if (this.getName().equals("Thread1")){
synchronized(szShareData){
szShareData = "这是第 1 个线程";
// 为了演示产生的问题,这里设置一次睡眠
try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 输出结果
System.out.println(this.getName() + ":" + szShareData);
}
}else if (this.getName().equals("Thread2")){
synchronized(szShareData){
szShareData = "这是第 1 个线程";
// 为了演示产生的问题,这里设置一次睡眠
try{
Thread.sleep((int)Math.random() * 100);
catch(InterruptedException e){
}
// 输出结果
System.out.println(this.getName() + ":" + szShareData);
}
}
}
}
class ThreadMain{
public static void main(String argv[]){
ThreadDemo th1 = new ThreadDemo("Thread1");
ThreadDemo th2 = new ThreadDemo("Thread2");
th1.start();
th2.start();
}
}
这段代码的共享成员是一个类中的静态成员,按理说,这里进入共享代码时得到的锁应该是同样的锁,而实际上以上程序的输入却是不同步的,为什么呢??
Java多线程学习笔记(二)
四、Java的等待通知机制
在有些时候,我们需要在几个或多个线程中按照一定的秩序来共享一定的资源。例如生产者--消费者的关系,在这一对关系中实际情况总是先有生产者生产了产品后,消费者才有可能消费;又如在父--子关系中,总是先有父亲,然后才能有儿子。然而在没有引入等待通知机制前,我们得到的情况却常常是错误的。这里我引入《用线程获得强大的功能》一文中的生产者--消费者的例子:
/* ==================================================================================
* 文件:ThreadDemo07.java
* 描述:生产者--消费者
* 注:其中的一些注释是我根据自己的理解加注的
* ==================================================================================
*/
// 共享的数据对象
class ShareData{
private char c;
public void setShareChar(char c){
this.c = c;
}
public char getShareChar(){
return this.c;
}
}
// 生产者线程
class Producer extends Thread{
private ShareData s;
Producer(ShareData s){
this.s = s;
}
public void run(){
for (char ch = 'A'; ch <= 'Z'; ch++){
try{
Thread.sleep((int)Math.random() * 4000);
}catch(InterruptedException e){}
// 生产
s.setShareChar(ch);
System.out.println(ch + " producer by producer.");
}
}
}
// 消费者线程
class Consumer extends Thread{
private ShareData s;
Consumer(ShareData s){
this.s = s;
}
public void run(){
char ch;
do{
try{
Thread.sleep((int)Math.random() * 4000);
}catch(InterruptedException e){}
// 消费
ch = s.getShareChar();
System.out.println(ch + " consumer by consumer.");
}while(ch != 'Z');
}
}
class Test{
public static void main(String argv[]){
ShareData s = new ShareData();
new Consumer(s).start();
new Producer(s).start();
}
}
在以上的程序中,模拟了生产者和消费者的关系,生产者在一个循环中不断生产了从A-Z的共享数据,而消费者则不断地消费生产者生产的A-Z的共享数据。我们开始已经说过,在这一对关系中,必须先有生产者生产,才能有消费者消费。但如果运行我们上面这个程序,结果却出现了在生产者没有生产之前,消费都就已经开始消费了或者是生产者生产了却未能被消费者消费这种反常现象。为了解决这一问题,引入了等待通知(wait/notify)机制如下:
1、在生产者没有生产之前,通知消费者等待;在生产者生产之后,马上通知消费者消费。
2、在消费者消费了之后,通知生产者已经消费完,需要生产。
下面修改以上的例子(源自《用线程获得强大的功能》一文):
/* ==================================================================================
* 文件:ThreadDemo08.java
* 描述:生产者--消费者
* 注:其中的一些注释是我根据自己的理解加注的
* ==================================================================================
*/
class ShareData{
private char c;
// 通知变量
private boolean writeable = true;
// -------------------------------------------------------------------------
// 需要注意的是:在调用wait()方法时,需要把它放到一个同步段里,否则将会出现
// "java.lang.IllegalMonitorStateException: current thread not owner"的异常。
// -------------------------------------------------------------------------
public synchronized void setShareChar(char c){
if (!writeable){
try{
// 未消费等待
wait();
}catch(InterruptedException e){}
}
this.c = c;
// 标记已经生产
writeable = false;
// 通知消费者已经生产,可以消费
notify();
}
public synchronized char getShareChar(){
if (writeable){
try{
// 未生产等待
wait();
}catch(InterruptedException e){}
}
// 标记已经消费
writeable = true;
// 通知需要生产
notify();
return this.c;
}
}
// 生产者线程
class Producer extends Thread{
private ShareData s;
Producer(ShareData s){
this.s = s;
}
public void run(){
for (char ch = 'A'; ch <= 'Z'; ch++){
try{
Thread.sleep((int)Math.random() * 400);
}catch(InterruptedException e){}
s.setShareChar(ch);
System.out.println(ch + " producer by producer.");
}
}
}
// 消费者线程
class Consumer extends Thread{
private ShareData s;
Consumer(ShareData s){
this.s = s;
}
public void run(){
char ch;
do{
try{
Thread.sleep((int)Math.random() * 400);
}catch(InterruptedException e){}
ch = s.getShareChar();
System.out.println(ch + " consumer by consumer.**");
}while (ch != 'Z');
}
}
class Test{
public static void main(String argv[]){
ShareData s = new ShareData();
new Consumer(s).start();
new Producer(s).start();
}
}
在以上程序中,设置了一个通知变量,每次在生产者生产和消费者消费之前,都测试通知变量,检查是否可以生产或消费。最开始设置通知变量为true,表示还未生产,在这时候,消费者需要消费,于时修改了通知变量,调用notify()发出通知。这时由于生产者得到通知,生产出第一个产品,修改通知变量,向消费者发出通知。这时如果生产者想要继续生产,但因为检测到通知变量为false,得知消费者还没有生产,所以调用wait()进入等待状态。因此,最后的结果,是生产者每生产一个,就通知消费者消费一个;消费者每消费一个,就通知生产者生产一个,所以不会出现未生产就消费或生产过剩的情况。
五、线程的中断
在很多时候,我们需要在一个线程中调控另一个线程,这时我们就要用到线程的中断。用最简单的话也许可以说它就相当于播放机中的暂停一样,当第一次按下暂停时,播放器停止播放,再一次按下暂停时,继续从刚才暂停的地方开始重新播放。而在Java中,这个暂停按钮就是Interrupt()方法。在第一次调用interrupt()方法时,线程中断;当再一次调用interrupt()方法时,线程继续运行直到终止。这里依然引用《用线程获得强大功能》一文中的程序片断,但为了更方便看到中断的过程,我在原程序的基础上作了些改进,程序如下:
/* ===================================================================================
* 文件:ThreadDemo09.java
* 描述:线程的中断
* ===================================================================================
*/
class ThreadA extends Thread{
private Thread thdOther;
ThreadA(Thread thdOther){
this.thdOther = thdOther;
}
public void run(){
System.out.println(getName() + " 运行...");
int sleepTime = (int)(Math.random() * 10000);
System.out.println(getName() + " 睡眠 " + sleepTime
+ " 毫秒。");
try{
Thread.sleep(sleepTime);
}catch(InterruptedException e){}
System.out.println(getName() + " 觉醒,即将中断线程 B。");
// 中断线程B,线程B暂停运行
thdOther.interrupt();
}
}
class ThreadB extends Thread{
int count = 0;
public void run(){
System.out.println(getName() + " 运行...");
while (!this.isInterrupted()){
System.out.println(getName() + " 运行中 " + count++);
try{
Thread.sleep(10);
}catch(InterruptedException e){
int sleepTime = (int)(Math.random() * 10000);
System.out.println(getName() + " 睡眠" + sleepTime
+ " 毫秒。觉醒后立即运行直到终止。");
try{
Thread.sleep(sleepTime);
}catch(InterruptedException m){}
System.out.println(getName() + " 已经觉醒,运行终止...");
// 重新设置标记,继续运行
this.interrupt();
}
}
System.out.println(getName() + " 终止。");
}
}
class Test{
public static void main(String argv[]){
ThreadB thdb = new ThreadB();
thdb.setName("ThreadB");
ThreadA thda = new ThreadA(thdb);
thda.setName("ThreadA");
thdb.start();
thda.start();
}
}
运行以上程序,你可以清楚地看到中断的过程。首先线程B开始运行,接着运行线程A,在线程A睡眠一段时间觉醒后,调用interrupt()方法中断线程B,此是可能B正在睡眠,觉醒后掏出一个InterruptedException异常,执行其中的语句,为了更清楚地看到线程的中断恢复,我在InterruptedException异常后增加了一次睡眠,当睡眠结束后,线程B调用自身的interrupt()方法恢复中断,这时测试isInterrupt()返回true,线程退出。
线程和进程(Threads and Processes)
第一个关键的系统级概念,究竟什么是线程或者说究竟什么是进程?她们其实就是操作系统内部的一种数据结构。
进程数据结构掌握着所有与内存相关的东西:全局地址空间、文件句柄等等诸如此类的东西。当一个进程放弃执行(准确的说是放弃占有CPU),而被操作系统交换到硬盘上,使别的进程有机会运行的时候,在那个进程里的所有数据也将被写到硬盘上,甚至包括整个系统的核心(core memory)。可以这么说,当你想到进程(process),就应该想到内存(memory) (进程 == 内存)。如上所述,切换进程的代价非常大,总有那么一大堆的内存要移来移去。你必须用秒这个单位来计量进程切换(上下文切换),对于用户来说秒意味着明显的等待和硬盘灯的狂闪(对于作者的我,就意味着IBM龙腾3代的烂掉,5555555)。言归正传,对于Java而言,JVM就几乎相当于一个进程(process),因为只有进程才能拥有堆内存(heap,也就是我们平时用new操作符,分出来的内存空间)。
那么线程是什么呢?你可以把它看成“一段代码的执行”---- 也就是一系列由JVM执行的二进制指令。这里面没有对象(Object)甚至没有方法(Method)的概念。指令执行的序列可以重叠,并且并行的执行。后面,我会更加详细的论述这个问题。但是请记住,线程是有序的指令,而不是方法(method)。
线程的数据结构,与进程相反,仅仅只包括执行这些指令的信息。它包含当前的运行上下文(context):如寄存器(register)的内容、当前指令的在运行引擎的指令流中的位置、保存方法(methods)本地参数和变量的运行时堆栈。如果发生线程切换,OS只需把寄存器的值压进栈,然后把线程包含的数据结构放到某个类是列表(LIST)的地方;把另一个线程的数据从列表中取出,并且用栈里的值重新设置寄存器。切换线程更加有效率,时间单位是毫秒。对于Java而言,一个线程可以看作是JVM的一个状态。
运行时堆栈(也就是前面说的存储本地变量和参数的地方)是线程数据结构一部分。这是因为多个线程,每一个都有自己的运行时堆栈,也就是说存储在这里面的数据是绝对线程安全(后面将会详细解释这个概念)的。因为可以肯定一个线程是无法修改另一个线程的系统级的数据结构的。也可以这么说一个不访问堆内存的(只读写堆栈内存)方法,是线程安全的(Thread Safe)。
线程安全和同步
线程安全,是指一个方法(method)可以在多线程的环境下安全的有效的访问进程级的数据(这些数据是与其他线程共享的)。事实上,线程安全是个很难达到的目标。
线程安全的核心概念就是同步,它保证多个线程:
同时开始执行,并行运行
不同时访问相同的对象实例
不同时执行同一段代码
我将会在后面的章节,一一细诉这些问题。但现在还是让我们来看看同步的一种经典的
实现方法——信号量。信号量是任何可以让两个线程为了同步它们的操作而相互通信的对象。Java也是通过信号量来实现线程间通信的。
不要被微软的文档所暗示的信号量仅仅是Dijksta提出的计数型信号量所迷惑。信号量其实包含任何可以用来同步的对象。
如果没有synchronized关键字,就无法用JAVA实现信号量,但是仅仅只依靠它也不足够。我将会在后面为大家演示一种用Java实现的信号量。
同步的代价很高哟!
同步(或者说信号量,随你喜欢啦)的一个很让人头痛的问题就是代价。考虑一下,下面的代码:
Listing 1.2:
import java.util.*;
import java.text.NumberFormat;
class Synch
{
private static long[ ] locking_time = new long[100];
private static long[ ] not_locking_time = new long[100];
private static final long ITERATIONS = 10000000;
synchronized long locking (long a, long b){return a + b;}
long not_locking (long a, long b){return a + b;}
private void test( int id )
{
long start = System.currentTimeMillis();
for(long i = ITERATIONS; --i >= 0 ;)
{ locking(i,i); }
locking_time[id] = System.currentTimeMillis() - start;
start = System.currentTimeMillis();
for(long i = ITERATIONS; --i >= 0 ;)
{ not_locking(i,i); }
not_locking_time[id] = System.currentTimeMillis() - start;
}
static void print_results( int id )
{ NumberFormat compositor = NumberFormat.getInstance();
compositor.setMaximumFractionDigits( 2 );
double time_in_synchronization = locking_time[id] - not_locking_time[id];
System.out.println( "Pass " + id + ": Time lost: "
+ compositor.format( time_in_synchronization )
+ " ms. "
+ compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 )
+ "% increase" );
}
static public void main(String[ ] args) throws InterruptedException
{
final Synch tester = new Synch();
tester.test(0); print_results(0);
tester.test(1); print_results(1);
tester.test(2); print_results(2);
tester.test(3); print_results(3);
tester.test(4); print_results(4);
tester.test(5); print_results(5);
tester.test(6); print_results(6);
final Object start_gate = new Object();
Thread t1 = new Thread()
{ public void run()
{ try{ synchronized(start_gate) { start_gate.wait(); } }
catch( InterruptedException e ){}
tester.test(7);
}
};
Thread t2 = new Thread()
{ public void run()
{ try{ synchronized(start_gate) { start_gate.wait(); } }
catch( InterruptedException e ){}
tester.test(8);
}
};
Thread.currentThread().setPriority( Thread.MIN_PRIORITY );
t1.start();
t2.start();
synchronized(start_gate){ start_gate.notifyAll(); }
t1.join();
t2.join();
print_results( 7 );
print_results( 8 );
}
}
这是一个简单的基准测试程序,她清楚的向大家揭示了同步的代价是多么的大。test(…)方法调用2个方法1,000,000,0次。其中一个是同步的,另一个则否。下面是在我的机器上输出的结果(CPU: P4 2.4G(B); Memory: 1GB; OS: windows 2000 server(sp3); JDK: Ver1.4.01 and HotSpot 1.4.01-b01):
C:/>java -verbose:gc Synch
Pass 0: Time lost: 251 ms. 727.5% increase
Pass 1: Time lost: 250 ms. 725% increase
Pass 2: Time lost: 251 ms. 602% increase
Pass 3: Time lost: 250 ms. 725% increase
Pass 4: Time lost: 261 ms. 752.5% increase
Pass 5: Time lost: 260 ms. 750% increase
Pass 6: Time lost: 261 ms. 752.5% increase
Pass 7: Time lost: 1,953 ms. 1,248.82% increase
Pass 8: Time lost: 3,475 ms. 8,787.5% increase
这里为了使HotSpot JVM充分的发挥其威力,test( )方法被多次反复调用。一旦这段程序被彻底优化以后,也就是大约在Pass 6时,同步的代价达到最大。Pass 7 和Pass 8与前面的区别在于,我new了两个新的线程来并行执行test方法,两个线程竞争执行(后面是适当的地方,我会解释什么是“竞争”,如果你已经等不及了,买本大学的操作系统课本看看吧! J),这使结果更加接近真实。同步的代价是如此之高的,应该尽量避免无谓的同步代价。
现在是时候我们更深入的讨论一下同步的代价了。HotSpot JVM一般会使用一到两个方法来实现同步,这主要取决于是否存在线程的竞争。当没有竞争的时候,计算机的汇编指令顺序的执行,这些指令的执行是不被打断。指令试图测试一个比特(bit),然后设置各种二进制位来表示测试的结果,如果这个bit没有被设置,指令就设置它。这可以说是非常原始的信号量,因为当两个线程同步的企图设置一个bit的值时,只有一个线程可以成功,两个线程都会检查结果,看看是不是自己设成功了。
如果bit已经被设置(这里说的是有线程竞争的情况下),失败的JVM(线程)不得不离开操作系统的核心进程等待这个bit位被清零。这样来回的在系统核心中切换是非常耗时的。在NT系统下,需要600次机械指令循环来进入一次系统内核,这还仅仅是进入所耗费的时间还不包括做操作的时间。
是不是觉得很无聊了,呵呵!今天似乎都是些不顶用的东西。但这是必须的,为了使你能够读懂后面的内容。下一篇,我将会谈到一些更有趣的话题,例如如何避免同步,如果大家不反对,我还想讲一些设计模式的东西。下回见!
避免同步
大部分显示的同步都可以避免。一般不操作对象状态信息(例如数据成员)的方法都不需要同步,例如:一些方法只访问本地变量(也就是说在方法内部声明的变量),而不操作类级别的数据成员,并且这些方法不会通过传入的引用参数来修改外部的对象。符合这些条件的方法都不需要使用synchronization这种重量级的操作。除此之外,还可以使用一些设计模式(Design Pattern)来避免同步(我将会在后面提到)。
你甚至可以通过适当的组织你的代码来避免同步。相对于同步的一个重要的概念就是原子性。一个原子性的操作事不能被其他线程中断的,通常的原子性操作是不需要同步的。
Java定义一些原子性的操作。一般的给变量付值的操作是原子的,除了long和double。看下面的代码:
class Unreliable
{
private long x;
public long get_x( ) {return x;}
public void set_x(long value) { x = value; }
}
线程1调用:
obj.set_x( 0 );
线程2调用:
obj.set_x( 0x123456789abcdef )
问题在于下面这行代码:
x = value;
JVM为了效率的问题,并没有把x当作一个64位的长整型数来使用,而是把它分为两个32-bit,分别付值:
x.hgh_word = value.high_word;
x.low_word = value.low_word;
因此,存在一个线程设置了高位之后被另一个线程切换出去,而改变了其高位或低位的值。所以,x的值最终可能为0x0123456789abcdef、0x01234567000000、0x00000000abcdef和0x00000000000000。你根本无法确定它的值,唯一的解决方法是,为set_x( )和get_x()方法加上synchronized这个关键字或者把这个付值操作封装在一个确保原子性的代码段里。
所以,在操作的long型数据的时候,千万不要想当然。强迫自己记住吧:只有直接付值操作是原子的(除了上面的例子)。其它,任何表达式,象x = ++y、x += y都是不安全的,不管x或y的数据类型是否是小于64位的。很可能在付值之前,自增之后,被其它线程抢先了(preempted)。
竞争条件
在术语中,对于前面我提到的多线程问题——两个线程同步操作同一个对象,使这个对象的最终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用synchronized的地方。在这个意义上,可以把synchronized看作一种保证复杂的、顺序一定的操作具有原子性的工具,例如给一个boolean值变量付值,就是一个隐式的同步操作。
不变性
一种有效的语言级的避免同步的方法就是不变性(immutability)。一个自从产生那一刻起就无法再改变的对象就是不变性对象,例如一个String对象。但是要注意类似这样的表达式:string1 += string2;本质上等同于string1 = string1 + string2;其实第三个包含string1和string2的string对象被隐式的产生,最后,把string1的引用指向第三个string。这样的操作,并不是原子的。
由于不变对象的值无法发生改变,所以可以为多个线程安全的同步操作,不需要synchronized。
把一个类的所有数据成员都声明为final就可以创建一个不变类型了。那些被声明为final的数据成员并不是必须在声明的时候就写死,但必须在类的构造函数中,全部明确的初始化。例如:
Class I_am_immutable
{
private final int MAX_VALUE = 10;
priate final int blank_final;
public I_am_immutable( int_initial_value )
{
blank_final = initial_value;
}
}
一个由构造函数进行初始化的final型变量叫做blank final。一般的,如果你频繁的只读访问一个对象,把它声明成一个不变对象是个保证同步的好办法,而且可以提高JVM的效率,因为HotSpot会把它放到堆栈里以供使用。
同步封装器(Synchronization Wrappers)