驯服Java线程(二)

类别:Java 点击:0 评论:0 推荐:
Java 线程的支持不是平台独立的

非常不幸,作为Java语言所保证的平台独立性最重要的组成部分-------Java线程,并非是平台独立的。这增加了实现不依赖于平台的线程系统的难度。在实现的时候,不得不考虑每个平台的细微区别,以确保你的程序在每个平台都保持一致。其实,写一个独立于平台的程序,还是有可能的,但必须非常小心。不过你可以放心,这个令人失望的事实,并不是Java的问题。(“Ace”Framework 就是一个非常好的,也非常复杂的平台独立线程系统http://www.cs.wustl.edu/~schmidt/ACE.html)。所以,在我继续讲下去之前,我不得不线讨论一下,由于平台的多样性,而导致的JVM的不一致性。

线程和进程(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)可以在多线程的环境下安全的有效的访问进程级的数据(这些数据是与其他线程共享的)。事实上,线程安全是个很难达到的目标。

线程安全的核心概念就是同步,它保证多个线程:

l         同时开始执行,并行运行

l         不同时访问相同的对象实例

l         不同时执行同一段代码

我将会在后面的章节,一一细诉这些问题。但现在还是让我们来看看同步的一种经典的

实现方法——信号量。信号量是任何可以让两个线程为了同步它们的操作而相互通信的对象。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次机械指令循环来进入一次系统内核,这还仅仅是进入所耗费的时间还不包括做操作的时间。

 

    是不是觉得很无聊了,呵呵!今天似乎都是些不顶用的东西。但这是必须的,为了使你能够读懂后面的内容。下一篇,我将会谈到一些更有趣的话题,例如如何避免同步,如果大家不反对,我还想讲一些设计模式的东西。下回见!

本文地址:http://com.8s8s.com/it/it17953.htm