21 Jul

论Object.wait()要放到while循环里

wait()放while里面算是一个常识性的准则。为什么要这样呢,如果放到if里面会有什么后果?今天水木有人贴出了一段出错的代码,对这个问题现身说法:

public class A {
        private Object[] queue = new Object[1024];
        private int cMsg;

        public synchronized boolean accept(Object msg, Object token)  {
                if (cMsg >= queue.length) {
                        try {
                                wait();
                        }
                        catch (InterruptedException e) {
                                return false;
                        }
                }

                queue[cMsg++] = token;
                queue[cMsg++] = msg;
                return true;
        }

        public synchronized Object[] getMessages()  {
                if (cMsg == 0) {
                        return null;
                }

                Object[] tmp = (Object[]) Arrays.copyOf(queue, cMsg);
                Arrays.fill(queue, 0, cMsg, null);
                cMsg = 0;
                notify();
                return tmp;
        }
}

这个代码在大并发下测试,抛出了java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 1025异常。

要分析原因的话,就是wait()被唤醒后,队列已经满了,cMsg >= queue.length这个条件已经不满足了,再往后移下标的话就数组越界了。

问题是为什么wait()唤醒后队列会满。在代码里,将队列清空后,才执行notify(),这个时候它应该只唤醒了一个线程,那么谁把队列填满的呢

答案是一个阻塞在accept上面的线程。首先要知道一点:其他线程收到信号并不是在notify调用的那一刻!notify的信号是在退出同步函数后才发出的,从退出同步函数,到信号发出,这中间有个时间差,因而就有可能出现以下执行序列:

1. getMessages方法中的 notify()调用
2. getMessages退出,此线程A释放类实例上的monitor
3. 一个阻塞在accept上的线程B,得以进入accept方法,因为此时数组被清空,线程B填入数据,下表+2;accept不断的调用,直到数组被填满,而阻塞在wait()调用上
4. notify信号发出,一个线程C被唤醒。这时没有再判断数组下标位置,直接想数组中塞数据,数组越界。

解决这个问题的方法当然就是把if改为while:

                while (cMsg >= queue.length) {
                        try {
                                wait();
                        }
                        catch (InterruptedException e) {
                                return false;
                        }
                }

在被唤醒后,重新判断条件是否满足。

19 Jul

如何停止一个超时的线程

经常,我们启动了一组线程,让他们去工作,并等待他们完成,获取他们的返回结果。为了保证程序不会卡死在这些线程的执行上,我们为线程设定了一个超时时间,希望线程如果超过这个时间没有完成,就终止执行。

在线程启动的时候,为其建立一个定时器就可以计算超时时间了。那么,问题就是,如何在一个线程已经超时的时候,停止这个线程的执行。

这是个从线程出现以后就在纠结的问题。

Java最初提供了一个Thread.stop方法用于终止线程,但是这个方法随后就因难以解决的线程安全问题被标记为deprecated,不建议继续使用。Thread.stop的原理是让线程抛出ThreadDeath异常(确切的说,它是一个Error),由于程序都不会捕获这个Error,所以这个线程依次退出调用栈,最终退到栈底而终止。在退栈的过程中,会执行所有的finally代码块,并释放线程持有的所有的锁!看起来这是一个设计相当完美的方案,当初那帮设计者应该会被自己的聪明智慧感动得流泪了吧。然而在实际使用的时候,由于被终止的线程释放了所有的锁,被这些锁保护的对象都变得可以被其他线程所访问,从而引发了意想不到的问题——问题可大可小,可能根本察觉不到,也可能造成莫名其妙的错误。

在剥夺了Thread.stop方法后,JDK转而建议使用共享条件变量来控制线程退出,就好似这样:

Class MyThread extents Thread{

    public volatile boolean stop = false;

    public void run(){
        dosomething1();
        if(stop){
            return;
        }

        dosomething2();
        if(stop){
            return;
        }

        dosomething3();

    }

}

需要停止时只要此线程的stop设为true就行了。这需要小心翼翼的编码,如果在最耗时的操作中间没有对退出标识进行判断,那所有其他的工作也就是白费了。

设置条件变量仍然没有办法处理线程被阻塞的情况(如调用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()等方法时)。一个阻塞中的线程是没法检查条件变量的,它只有等到条件满足,解除阻塞时,才能对条件变量进行判断。这时候可以调用Thread.interrupt方法打破阻塞。Thread.interrupt会使目标线程抛出InterruptedException异常,这个异常通常在代码中被捕获,线程因而跳过阻塞的请求,继续执行。

所以应如下停止一个线程:

MyThread thread = new MyThread();
//wait for some time.
thread.stop = true;
thread.interrupt();

事情还没有完。有些阻塞的线程是不响应Thread.interrupt方法的(例如阻塞在socket.accept() 等旧式IO请求上),Thread.interrupt可以打断的阻塞调用只有Object.wait, Thread.join和Thread.sleep三种。对于这些情况没有通用的处理方法,只能却别对待。例如对于阻塞在socket.accept()的线程,我们可以调用socket.close()来接触阻塞。需要说的是,这种不响应Thread.interrupt的阻塞线程,也不会响应Thread.stop。

综上:多线程是强大的工具,但是也面临很多难以解决的问题,停止正在执行的线程就是其中之一。

25 Apr

java.util.concurrent中的线程工具

从1.5版本开始,jdk引入了java.util.concurrent包,其中包含了很多用于简化多线程开发的工具,java的并发编程,可以说变得很容易了。

一、线程池。

ExecutorService提供了线程池功能。ExecutorService的实例可以使用new SingleThreadExecutor()或者new FixedThreadPool()来获得。ExecutorService可以用来执行实现了Runnable接口的对象,或者实现了Callable接口的对象。Runnable是久已存在的接口,以前用作传给Thread对象的接口,现在也被各样的线程池用于传入的对象接口。Runnable接口只有一个run函数,函数没有参数,也没有返回值,需要获取运行状态的时候,就只能通过另外的手段。Callable接口的call方法则带有返回值,这个返回值是泛型化的,类型就是实现Callable接口时指定的那个T。

实例化一个ExecutorService后,就可以调用submit,invokeAll或者invokeAny来执行了。这些方法会返回会返回Future或者List对象,调用Future的get方法就可以拿到执行结果了。Future的这个get方法在子线程结束前是会堵塞的。

二、同步机制Lock/Condition,Semaphore,CountDownLatch,CyclicBarrier

java的同步是基于对象锁实现的,所有的对象都天然的带有一个单一锁,JVM会跟踪对象上锁的次数,未加锁时对象上的技术为0,一旦一个对象上锁的计数不为0,则只有已经在这个对象上加锁的线程才能继续获得这个线程上的锁。jdk1.5之前的互斥是通过synchronized关键字实现的,synchronized可以指定加锁的对象,如果没有指定锁定的对象,就锁在同步代码段自己所在的对象实例上。在进入synchronized代码段时对象上锁的技术加1,离开时减1,当同步代码段中有异常抛出时,JVM也会自动释放锁,因而synchronized是个挺好使的东西。

Lock的机制和synchronized是一样的,但是程序员可以手工控制lock和unlock的时机,并且提供了trylock(尝试能够获取锁但不会真的加锁)这样的方法,可控性更强一些。另外,unlock是需要写在finally代码段中的,以防有异常抛出造成锁没有释放。

Lock的一个好处是可以绑定多个Condition。Lock+Condition的组合,类似与synchronized+其加锁的对象上的监视器方法(wait,notify等)。Sun官方给出了一个生产者-消费者的例子,来说明Lock的这种用法。http://gceclub.sun.com.cn/Java_Docs/html/zh_CN/api/java/util/concurrent/locks/Condition.html

Semaphore。顾名思义,它确实就是一个信号量……acquire方法获取一个资源,没有资源就会被阻塞,release释放一个资源。与锁的区别……参见各操作系统教科书上的习题集解答。

CountDownLatch是一个倒计时似的计数器,调用countDown()方法会减少计数,计数为0时被其阻塞的线程就被唤醒。CyclicBarrier与CountDownLatch类似,不过不实在计数为0时恢复阻塞的线程的执行,而是在计数到达设定的某个数值时;并且CyclicBarrier是可以用用的,CountDownLatch是一次性的。

三、并发集合类

包括ConcurrentHashMap,ConcurrentLinkedQueue,CopyOnWriteArrayList,BlockingQueue等。这些都是对应的java.utils中的集合类的高性能、线程安全的实现版本。以ConcurrentHashMap来说,其对应的容器版本是HashTable和HashMap/synchronizedMapHashMap不是线程安全的,而HashTable和synchronizedMap对单个操作是线程安全的,但是多个操作序列如果要保证同步,则必须要加锁,比如put-if-absent操作:如果不包含一个key,那么新增加一个key-value值。HashTable/synchronizedMap同步的代价是比较昂贵的,同步是基于加在这个对象本身上的一个单一锁,这样两个线程甚至没办法并发调用他们get方法,这个同步事实上是读-读也会互斥的。

ConcurrentHashMap实现了putIfAbsenct方法,并且,实现了好得多的并发性。几乎所有的读-读操作可以并发进行,读-写大部分也可以并发进行,甚至写-写操作有时也可以并发。

CopyOnWriteArrayList是一个实现了COW机制的集合类,比较适合于读多写少的情况。

BlockingQueue是一个阻塞队列。为空时获取元素的方法会阻塞,容量满事添加元素的操作会被阻塞。从这个集合类的功能设计可以看出,它的目的用途非常明确……以后用java来写生产者-消费者大作业的同学有福了。

四、原子操作类

Jdk2.5提供了以系列的atomic class来简化同步处理,包括AtomicLong, AtomicInteger, AtomicReference等。在java中,赋值、自增等运算都是无法保证原子性的,原子操作类通过包装了同步操作实现了相关操作的原子化版本。

22 Apr

UTF-8到GBK转码的特殊字符问题

Unicode字符集现在有超过10万个字符,其BMP部分也有六万多个字符;而GBK字符集只有两万以前多个字符。这样的话,从支持unicode字符集或者unicode字符集BMP的编码方式,转化到GBK编码的时候,就会有编码落到GBK字符集以外,不能转化成GBK编码。在java中,转换之后的字符串,这部分字符都变成了’?’。

通常这些都是非常生僻的字符,倒是可以不考虑;但是有一个特殊的unicode字符,不在GBK字符集中。却频繁用于xml/html等格式的文件中。这个字符unicode序号为0xA0,utf-8编码结果为C2A0,作用是一个排版空格——普通的ascii空格在xml/html中是被忽略的。大量UTF-编码的网页使用这个字符用作占位的空格。而且似乎浏览器对它的处理方式也不同:IE8浏览器会认得这个空格,firefox3.6简单的把它替换成 。当把一个utf-8编码的网页转成gbk编码时,这个字符就变成讨厌的问号了。

处理方法,就是在字符串以GBK编码写出之前,把这个字符替换掉:

str = str.replace('\u00A0', ' ');

彻底而保险的方法是过滤所有GBK不能表示的字符:

str = str.replaceAll("[^\u4E00-\u9FA5\u3000-\u303F\uFF00-\uFFEF\u0000-\u007F\u201c-\u201d]", " ");

02 Mar

Oracle准备将JRockit/Sun Hotspot集成

目前Oracle有两个JVM,一个是JRockit, 这是两年前收购BEA Systems时得到的;另一个则是Sun的Hotspot VM,这是前不久收购Sun时得到的。在上个月举行的Sun-Oracle未来路线图会议上,Oracle的管理团队表示要合并这两个项目。Oracle 首席工程师、Sun前雇员Mark Reinhold最近在播客上透露该合并计划“仍在进行当 中”,为此也“召开了很多会议”。

Reinhold说到:

从长期的合并计划来看,目前很难对这二者作出取舍。现在我们还不会停止这两个JVM的开发工作,因为有很多客户的产品是运行在这 两个JVM之上并且使用了每个JVM独有的特性。我们可不想搞出什么震荡,那样只会把系统搞死,但还是衷心希望未来能有JVM一统天下。

Reinhold说这个计划至少还需要一年半到两年的时间才能成行。

这两个JVM各具优缺点,因此最好的方式还是取其精华,弃其糟粕。Reinhold说“在Oracle内,无论是工程团队还是管理团队都在尽最大努 力找出每个JVM的优点”。他接着说到:

坦率地说,我们这几年一直在嫉妒JRockit中的某些特性,其任务控制特性就非常棒。

而HotSpot的性能优势是比较明显的,他说到“我们对HotSpot代码基,尤其是server编译器的印象是其有很多的head room,这是一个更加复杂的系统”。

前几个月我们一直在学习JRockit,这真是一段令人难忘的时光。JRockit绝对是世界一流的VM,但其内部却是那么的不 同。JRockit和HotSpot各具优势,因此我们将要创建一个非常帅的项目——综合JRockit和HotSpot各自的优势。

Reinhold推测合并后的VM将使用JRockit的垃圾回收器与服务功能,使用HotSpot的运行时编译器与混合的运行时系统。

在播客中,Reinhold还提到了JDK7的模块化特性(模块化可以让Java更有效地进入到小型设备领域)、通过invokeDynamic实 现的多语言能力以及通过ProjectCoin提升Java语言本身的生产力。开发者应该玩玩Jigsaw,而openJDK Build 88则将于下月中旬发布。

 

查看英文原文:Mark Reinhold Talks About JRockit/Hotspot Integration

09 Oct

GAE生成验证码

我的blog开放了匿名评论以后,一直以来都挺平静的,没有垃圾评论,多半因为它本来看的人就不多……但是前端时间不晓得是哪里的谁忽然看上它了,spam扑面而来。国庆前两天就有苗头了,但我直接把垃圾评论删了了事,也没在意。回家待了几天回来,已经上白天spamming信息了。

所以我终于决定要解决一下GAE上的验证码问题了。本来生成验证码是件挺简单的事,JDK就有把文字打到图片中的方法;而且还富有大量第三方的Captcha库,能生成各式各样的验证码,诸如背景带斑点的,文字扭曲的,英文的,汉字的,机器看不懂人也看不懂的……一应俱全。但是悲剧的GAE啊!作为谷歌一直鼓吹的云,连一点点图片io操作都不支持,所有和图片相关的jdk接口全上了它的blacklist!

真是恨GAE不成钢啊。无奈之下决定山寨到底,自己从头动手:

  1. 开个矩阵存放图片每个像素的RGB值,自己手动在RGB矩阵里划出数字和字符来……由于字符实在太多就只画了10个阿拉伯数字,而且横平竖直的像老式电子表中的数字
  2. 写个纯java的图片编码工具,把RGB值矩阵编码成gif或者png格式的byte流,写到response里。

山寨是山寨了点,不过总算是整出来了,对付一般的小蟊贼够用了。

13 Sep

Java线程池与工作队列

Brian Goetz (brian@quiotix.com), 首席顾问, Quiotix Corp

为什么要用线程池?

诸如 Web 服务器、数据库服务器、文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务。请求以某种方式到达服务器,这种方式可能是通过网络协议(例如 HTTP、FTP 或 POP)、通过 JMS 队列或者可能通过轮询数据库。不管请求如何到达,服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的。

构建服务器应用程序的一个过于简单的模型应该是:每当一个请求到达就创建一个新线程,然后在新线程中为请求服务。实际上,对于原型开发这种方法工作得很好,但如果试图部署以这种方式运行的服务器应用程序,那么这种方法的严重不足就很明显。每个请求对应一个线程(thread-per-request)方法的不足之一是:为每个请求创建一个新线程的开销很大;为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。

除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目。

线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

线程池的替代方案

线程池远不是服务器应用程序内使用多线程的唯一方法。如同上面所提到的,有时,为每个新任务生成一个新线程是十分明智的。然而,如果任务创建过于频繁而任务的平均处理时间过短,那么为每个任务生成一个新线程将会导致性能问题。

另一个常见的线程模型是为某一类型的任务分配一个后台线程与任务队列。AWT 和 Swing 就使用这个模型,在这个模型中有一个 GUI 事件线程,导致用户界面发生变化的所有工作都必须在该线程中执行。然而,由于只有一个 AWT 线程,因此要在 AWT 线程中执行任务可能要花费相当长时间才能完成,这是不可取的。因此,Swing 应用程序经常需要额外的工作线程,用于运行时间很长的、同 UI 有关的任务。

每个任务对应一个线程方法和单个后台线程(single-background-thread)方法在某些情形下都工作得非常理想。每个任务一个线程方法在只有少量运行时间很长的任务时工作得十分好。而只要调度可预见性不是很重要,则单个后台线程方法就工作得十分好,如低优先级后台任务就是这种情况。然而,大多数服务器应用程序都是面向处理大量的短期任务或子任务,因此往往希望具有一种能够以低开销有效地处理这些任务的机制以及一些资源管理和定时可预见性的措施。线程池提供了这些优点。

工作队列

就线程池的实际实现方式而言,术语“线程池”有些使人误解,因为线程池“明显的”实现在大多数情形下并不一定产生我们希望的结果。术语“线程池”先于 Java 平台出现,因此它可能是较少面向对象方法的产物。然而,该术语仍继续广泛应用着。

虽然我们可以轻易地实现一个线程池类,其中客户机类等待一个可用线程、将任务传递给该线程以便执行、然后在任务完成时将线程归还给池,但这种方法却存在几个潜在的负面影响。例如在池为空时,会发生什么呢?试图向池线程传递任务的调用者都会发现池为空,在调用者等待一个可用的池线程时,它的线程将阻塞。我们之所以要使用后台线程的原因之一常常是为了防止正在提交的线程被阻塞。完全堵住调用者,如在线程池的“明显的”实现的情况,可以杜绝我们试图解决的问题的发生。

我们通常想要的是同一组固定的工作线程相结合的工作队列,它使用 wait()notify() 来通知等待线程新的工作已经到达了。该工作队列通常被实现成具有相关监视器对象的某种链表。清单 1 显示了简单的合用工作队列的示例。尽管 Thread API 没有对使用 Runnable 接口强加特殊要求,但使用 Runnable 对象队列的这种模式是调度程序和工作队列的公共约定。

清单 1. 具有线程池的工作队列

public class WorkQueue{
    private final int nThreads;
    private final PoolWorker[] threads;
    private final LinkedList queue;
    public WorkQueue(int nThreads){
        this.nThreads = nThreads;
        queue = new LinkedList();
        threads = new PoolWorker[nThreads];
        for (int i=0; i<nThreads; i++) {
            threads[i] = new PoolWorker();
            threads[i].start();
        }
    }
    public void execute(Runnable r) {
        synchronized(queue) {
            queue.addLast(r);
            queue.notify();
        }
    }
    private class PoolWorker extends Thread {
        public void run() {
            Runnable r;
            while (true) {
                synchronized(queue) {
                    while (queue.isEmpty()) {
                        try
                        {
                            queue.wait();
                        }
                        catch (InterruptedException ignored)
                        {
                        }
                    }
                    r = (Runnable) queue.removeFirst();
                }
                // If we don't catch RuntimeException,
                // the pool could leak threads
                try {
                    r.run();
                }
                catch (RuntimeException e) {
                    // You might want to log something here
                }
            }
        }
    }
}

您可能已经注意到了清单 1 中的实现使用的是 notify() 而不是 notifyAll() 。大多数专家建议使用 notifyAll() 而不是 notify() ,而且理由很充分:使用 notify() 具有难以捉摸的风险,只有在某些特定条件下使用该方法才是合适的。另一方面,如果使用得当, notify() 具有比 notifyAll() 更可取的性能特征;特别是, notify() 引起的环境切换要少得多,这一点在服务器应用程序中是很重要的。

清单 1 中的示例工作队列满足了安全使用 notify() 的需求。因此,请继续,在您的程序中使用它,但在其它情形下使用 notify() 时请格外小心。

用线程池的风险

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

死锁

任何多线程应用程序都有死锁风险。当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,我们就说这组进程或线程 死锁了。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。

虽然任何多线程程序中都有死锁的风险,但线程池却引入了另一种死锁可能,在那种情况下,所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。

资源不足

线程池的一个优点在于:相对于其它替代调度机制(有些我们已经讨论过)而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。线程消耗包括内存和其它系统资源在内的大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM 可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。

如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间,而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如 JDBC 连接、套接字或文件。这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配 JDBC 连接。

并发错误

线程池和其它排队机制依靠使用 wait()notify() 方法,这两个方法都难于使用。如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。使用这些方法时,必须格外小心;即便是专家也可能在它们上面出错。而最好使用现有的、已经知道能工作的实现,例如在下面讨论的 util.concurrent 包。

线程泄漏

各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。

有些任务可能会永远等待某些资源或来自用户的输入,而这些资源又不能保证变得可用,用户可能也已经回家了,诸如此类的任务会永久停止,而这些停止的任务也会引起和线程泄漏同样的问题。如果某个线程被这样一个任务永久地消耗着,那么它实际上就被从池除去了。对于这样的任务,应该要么只给予它们自己的线程,要么只让它们等待有限的时间。

请求过载

仅仅是请求就压垮了服务器,这种情况是可能的。在这种情形下,我们可能不想将每个到来的请求都排队到我们的工作队列,因为排在队列中等待执行的任务可能会消耗太多的系统资源并引起资源缺乏。在这种情形下决定如何做取决于您自己;在某些情况下,您可以简单地抛弃请求,依靠更高级别的协议稍后重试请求,您也可以用一个指出服务器暂时很忙的响应来拒绝请求。

有效使用线程池的准则

只要您遵循几条简单的准则,线程池可以成为构建服务器应用程序的极其有效的方法:

  • 不要对那些同步等待其它任务结果的任务排队。这可能会导致上面所描述的那种形式的死锁,在那种死锁中,所有线程都被一些任务所占用,这些任务依次等待排队任务的结果,而这些任务又无法执行,因为所有的线程都很忙。
  • 在为时间可能很长的操作使用合用的线程时要小心。如果程序必须等待诸如 I/O 完成这样的某个资源,那么请指定最长的等待时间,以及随后是失效还是将任务重新排队以便稍后执行。这样做保证了:通过将某个线程释放给某个可能成功完成的任务,从而将最终取得 某些进展。
  • 理解任务。要有效地调整线程池大小,您需要理解正在排队的任务以及它们正在做什么。它们是 CPU 限制的(CPU-bound)吗?它们是 I/O 限制的(I/O-bound)吗?您的答案将影响您如何调整应用程序。如果您有不同的任务类,这些类有着截然不同的特征,那么为不同任务类设置多个工作队列可能会有意义,这样可以相应地调整每个池。

调整池的大小

调整线程池的大小基本上就是避免两类错误:线程太少或线程太多。幸运的是,对于大多数应用程序来说,太多和太少之间的余地相当宽。

请回忆:在应用程序中使用线程有两个主要优点,尽管在等待诸如 I/O 的慢操作,但允许继续进行处理,并且可以利用多处理器。在运行于具有 N 个处理器机器上的计算限制的应用程序中,在线程数目接近 N 时添加额外的线程可能会改善总处理能力,而在线程数目超过 N 时添加额外的线程将不起作用。事实上,太多的线程甚至会降低性能,因为它会导致额外的环境切换开销。

线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。若在一个具有 N 个处理器的系统上只有一个工作队列,其中全部是计算性质的任务,在线程池具有 N 或 N+1 个线程时一般会获得最大的 CPU 利用率。

对于那些可能需要等待 I/O 完成的任务(例如,从套接字读取 HTTP 请求的任务),需要让池的大小超过可用处理器的数目,因为并不是所有线程都一直在工作。通过使用概要分析,您可以估计某个典型请求的等待时间(WT)与服务时间(ST)之间的比例。如果我们将这一比例称之为 WT/ST,那么对于一个具有 N 个处理器的系统,需要设置大约 N*(1+WT/ST) 个线程来保持处理器得到充分利用。

处理器利用率不是调整线程池大小过程中的唯一考虑事项。随着线程池的增长,您可能会碰到调度程序、可用内存方面的限制,或者其它系统资源方面的限制,例如套接字、打开的文件句柄或数据库连接等的数目。

无须编写您自己的池

Doug Lea 编写了一个优秀的并发实用程序开放源码库 util.concurrent ,它包括互斥、信号量、诸如在并发访问下执行得很好的队列和散列表之类集合类以及几个工作队列实现。该包中的 PooledExecutor 类是一种有效的、广泛使用的以工作队列为基础的线程池的正确实现。您无须尝试编写您自己的线程池,这样做容易出错,相反您可以考虑使用 util.concurrent 中的一些实用程序。参阅 参考资料以获取链接和更多信息。

util.concurrent 库也激发了 JSR 166,JSR 166 是一个 Java 社区过程(Java Community Process (JCP))工作组,他们正在打算开发一组包含在 java.util.concurrent 包下的 Java 类库中的并发实用程序,这个包应该用于 Java 开发工具箱 1.5 发行版。

结束语

线程池是组织服务器应用程序的有用工具。它在概念上十分简单,但在实现和使用一个池时,却需要注意几个问题,例如死锁、资源不足和 wait()notify() 的复杂性。如果您发现您的应用程序需要线程池,那么请考虑使用 util.concurrent 中的某个 Executor 类,例如 PooledExecutor ,而不用从头开始编写。如果您要自己创建线程来处理生存期很短的任务,那么您绝对应该考虑使用线程池来替代。

13 Sep

Sun HotSpot Jvm GC分代

来自IBM的一组统计数据:
98%的java对象,在创建之后不久就变成了非活动对象;只有2%的对象,会在长时间一直处于活动状态。
如果能对这两种对象区分对象,那么会提交GC的效率。在sun jdk gc中(具体的说,是在jdk1.4之后的版本),提出了不同生命周期的GC策略。

young generation:

生命周期很短的对象,归为young generation。由于生命周期很短,这部分对象在gc的时候,很大部分的对象已经成为非活动对象。因此针对young  generation的对象,采用copy算法,只需要将少量的存活下来的对象copy到to space。存活的对象数量越少,那么copy算法的效率越高。

young generation的gc称为minor gc。经过数次minor gc,依旧存活的对象,将被移出young generation,移到tenured generation

young generation分为:
eden:每当对象创建的时候,总是被分配在这个区域
survivor1:copy算法中的from space
survivor2:copy算法中的to sapce (备注:其中survivor1和survivor2的身份在每次minor gc后被互换)
minor gc的时候,会把eden+survivor1(2)的对象copy到survivor2(1)去。

tenured generation:
生命周期较常的对象,归入到tenured generation。一般是经过多次minor gc,还 依旧存活的对象,将移入到tenured generation。(当然,在minor gc中如果存活的对象的超过survivor的容量,放不下的对象会直接移入到tenured generation)
tenured generation的gc称为major gc,就是通常说的full gc。
采用compactiion算法。由于tenured generaion区域比较大,而且通常对象生命周期都比较长,compaction需要一定时间。所以这部分的gc时间比较长。
minor gc可能引发full gc。当eden+from space的空间大于tenured generation区的剩余空间时,会引发full gc。这是悲观算法,要确保eden+from space的对象如果都存活,必须有足够的tenured generation空间存放这些对象。

Permanet Generation:
该区域比较稳定,主要用于存放classloader信息,比如类信息和method信息。
对于spring hibernate这些需要动态类型支持的框架,这个区域需要足够的空间。
这部分内容相对比较理论,可以结合jstat,jmap等命令(当然也可以使用jconsole,jprofile,gciewer等工具),观察jdk gc的情

13 Sep

JDK1.5中的线程池

在多线程大师Doug Lea的贡献下,在JDK1.5中加入了许多对并发特性的支持,例如:线程池。

一、简介

线程池类为 java.util.concurrent.ThreadPoolExecutor,常用构造方法为:
        ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue workQueue,RejectedExecutionHandler handler)
        corePoolSize: 线程池维护线程的最少数量
        maximumPoolSize:线程池维护线程的最大数量
        keepAliveTime: 线程池维护线程所允许的空闲时间
        unit: 线程池维护线程所允许的空闲时间的单位
        workQueue: 线程池所使用的缓冲队列
        handler: 线程池对拒绝任务的处理策略
一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法。
当一个任务通过execute(Runnable)方法欲添加到线程池时:
        如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
        如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
        如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
        如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
也就是:处理任务的优先级为:
        核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
        当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
unit可选的参数为java.util.concurrent.TimeUnit中的几个静态属性:
        NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS。
workQueue我常用的是:java.util.concurrent.ArrayBlockingQueue
handler有四个选择:
        ThreadPoolExecutor.AbortPolicy()
                抛出java.util.concurrent.RejectedExecutionException异常
        ThreadPoolExecutor.CallerRunsPolicy()
                重试添加当前的任务,他会自动重复调用execute()方法
        ThreadPoolExecutor.DiscardOldestPolicy()
                抛弃旧的任务
        ThreadPoolExecutor.DiscardPolicy()
                抛弃当前的任务

二、一般用法举例

//------------------------------------------------------------
//TestThreadPool.java
//package cn.simplelife.exercise;
import java.io.Serializable;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestThreadPool {
    private static int produceTaskSleepTime = 2;
    private static int consuskSleepTime = 2000;
    private static int produceTaskMaxNumber = 10;

    public static void main(String[] args) {
        // 构造一个线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3),
                new ThreadPoolExecutor.DiscardOldestPolicy());
        for (int i = 1; i <= produceTaskMaxNumber; i++) {
            try {
                // 产生一个任务,并将其加入到线程池
                String task = "task@ " + i;
                System.out.println("put " + task);
                threadPool.execute(new ThreadPoolTask(task));
                // 便于观察,等待一段时间
                Thread.sleep(produceTaskSleepTime);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 线程池执行的任务
     *
     * @author hdpan
     */
    public static class ThreadPoolTask implements Runnable, Serializable {
        private static final long serialVersionUID = 0;
        // 保存任务所需要的数据
        private Object threadPoolTaskData;

        ThreadPoolTask(Object tasks) {
            this.threadPoolTaskData = tasks;
        }

        public void run() {
            // 处理一个任务,这里的处理方式太简单了,仅仅是一个打印语句
            System.out.println("start .." + threadPoolTaskData);
            try {
                // //便于观察,等待一段时间
                Thread.sleep(consuskSleepTime);
            } catch (Exception e) {
                e.printStackTrace();
            }
            threadPoolTaskData = null;
        }

        public Object getTask() {
            return this.threadPoolTaskData;
        }
    }
}
// ------------------------------------------------------------


说明:
1、在这段程序中,一个任务就是一个Runnable类型的对象,也就是一个ThreadPoolTask类型的对象。
2、一般来说任务除了处理方式外,还需要处理的数据,处理的数据通过构造方法传给任务。
3、在这段程序中,main()方法相当于一个残忍的领导,他派发出许多任务,丢给一个叫 threadPool的任劳任怨的小组来做。
这个小组里面队员至少有两个,如果他们两个忙不过来,任务就被放到任务列表里面。
如果积压的任务过多,多到任务列表都装不下(超过3个)的时候,就雇佣新的队员来帮忙。但是基于成本的考虑,不能雇佣太多的队员,至多只能雇佣 4个。
如果四个队员都在忙时,再有新的任务,这个小组就处理不了了,任务就会被通过一种策略来处理,我们的处理方式是不停的派发,直到接受这个任务为止(更残忍!呵呵)。
因为队员工作是需要成本的,如果工作很闲,闲到 3SECONDS都没有新的任务了,那么有的队员就会被解雇了,但是,为了小组的正常运转,即使工作再闲,小组的队员也不能少于两个。
4、通过调整 produceTaskSleepTime和 consuskSleepTime的大小来实现对派发任务和处理任务的速度的控制,改变这两个值就可以观察不同速率下程序的工作情况。
5、通过调整4中所指的数据,再加上调整任务丢弃策略,换上其他三种策略,就可以看出不同策略下的不同处理方式。
6、对于其他的使用方法,参看jdk的帮助,很容易理解和使用。

12 Sep

使用 Eclipse 远程调试 Java 应用程序

级别: 中级
Charles Lu, 软件工程师, IBM
2009 年 1 月 12 日

在本地计算机上调试 Java™ 应用程序并不是惟一的选择。学习如何使用构成远程调试的不同连接类型进行远程调试。本文概述了设置远程应用程序调试的特性和示例。

远程调试对应用程序开发十分有用。例如,为不能托管开发平台的低端机器开发程序,或在专用的机器上(比如服务不能中断的 Web 服务器)调试程序。其他情况包括:运行在内存小或 CUP 性能低的设备上的 Java 应用程序(比如移动设备),或者开发人员想要将应用程序和开发环境分开,等等。

先决条件

启动配置类型
启动配置 保存一组用于启动程序的属性。启动配置类型是一种可以在 Eclipse 平台上启动的独特程序。

如果您还没安装该程序,请下载 Eclipse V3.4(Ganymede)。在 Ganymede 中,套接字(socket)监听连接器被添加到 Remote Java Application 启动配置类型。Eclipse 最新的套接字监听连接器允许您打开 Java 调试器,它能够监听特定套接字上的连接。可以从命令行选项打开被调试的程序,并将其连接到调试器。在 Ganymede 发布之前,仅有一个连接套接字的连接器,被调试的程序所在的机器必须是一个与调试器相连的调试主机。由于受到内存和 CPU 不足的限制,要想让移动设备充当主机是不现实的。

为了进行远程调试,必须使用 Java Virtual Machine (JVM) V5.0 或更新版本,比如 IBM® J9 或 Sun Microsystem 的 Java SE Development Kit(JDK)。本文主要讨论远程调试,而不是每个 Eclipse 调试特性的细节。查看 参考资料 获得更多关于使用 Eclipse 进行调试的信息,并且可以找到上面提到的软件。

JPDA 简介

常用缩写词

  • JDI — Java 调试接口(Java Debug Interface)
  • JDT — Java 开发工具(Java Development Tools)
  • JDWP — Java 调试网络协议(Java Debug Wire Protocol)
  • JPDA — Java 平台调试器架构(Java Platform Debugger Architecture)
  • JVM — Java 虚拟机(Java Virtual Machine)
  • JVMDI — JVM 调试接口(JVM Debug Interface)
  • JVMTI — JVM 工具接口(JVM Tool Interface)
  • VM — 虚拟机(Virtual Machine)

Sun Microsystem 的 Java Platform Debugger Architecture (JPDA) 技术是一个多层架构,使您能够在各种环境中轻松调试 Java 应用程序。JPDA 由两个接口(分别是 JVM Tool Interface 和 JDI)、一个协议(Java Debug Wire Protocol)和两个用于合并它们的软件组件(后端和前端)组成。它的设计目的是让调试人员在任何环境中都可以进行调试。JPDA 不仅能够用于桌面系统,而且能够在嵌入式系统上很好地工作。

JVM Tool Interface (JVMTI) 规定必须为调试提供 VM(编辑注:从 Java V5 开始,将用 JVMTI 代替 Java V1.4 中的 JVMDI)。Java Debug Wire Protocol (JDWP) 描述调试信息的格式,以及在被调试的进程和调试器前端之间传输的请求,调试器前端实现 JDI,比如 Eclipse、Borland JBuilder 等。根据 Sun 的 JPDA 规范,被调试的程序常常称为 debuggee。JDI 是一个高级的接口,它定义用于远程调试的信息和请求。下面给出了调试器的架构。

清单 1. Java 平台调试器架构

				
             Components                      Debugger Interfaces

                 /    |--------------|
                /     |     VM       |
 debuggee -----(      |--------------|  <---- JVMTI - Java VM Tool Interface
                \     |   back-end   |
                 \    |--------------|
                 /           |
 comm channel --(            |  <------------ JDWP - Java Debug Wire Protocol
                 \           |
                 /    |--------------|
                /     |  front-end   |
 debugger -----(      |--------------|  <---- JDI - Java Debug Interface
                \     |      UI      |
                 \    |--------------|	    
	  

因此,任何第三方工具和基于 JPDA 的 VM 应该都能协调工作。通过这个客户机-服务器架构,您可以从运行该平台的本地工作站调试 Java 程序,甚至还可以通过网络进行远程调试。

在讨论调试场景之前,我们先了解 JPDA 规范中的两个术语:连接器和传输。连接器是一个 JDI 抽象,用来在调试器应用程序和目标 VM 之间建立连接。传输定义应用程序如何进行访问,以及数据如何在前端和后端之间传输。连接器 “映射” 到可用的传输类型和连接模式。在 Sun 的 JPDA 参考实现中,为 Microsoft® Windows® 提供了两个传输机制:套接字传输和共享内存传输。可用的连接器:

  • 连接套接字连接器
  • 连接共享内存连接器
  • 监听套接字连接器
  • 监听共享内存连接器
  • 启动命令行连接器

在调试器应用程序和目标 VM 之间建立连接时,有一端将用作服务器并监听连接。随后,另一端将连接到监听器并建立一个连接。通过连接,调试器应用程序或目标 VM 都可以充当服务器。进程之间的通信可以在同一个机器或不同的机器上运行。

要远程调试 Java 程序,难点不是在调试器的前端,而是远程 Java 后端。不幸的是,Eclipse 帮助系统中为这方面提供的信息并不多。事实上,JDI 和 JVMTI 是分别由 Eclipse 和 Java 运行时环境实现的。我们仅需要考虑 JDMP,因为它包含与 JVMTI 和 JDI 进行通信所需的信息。JDWP 包含许多参数,用于为远程 Java 应用程序调用所需的程序。以下是本文用到的一些参数。

-Xdebug
启用调试特性。
-Xrunjdwp:
在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。下面介绍一些特定的子选项。

从 Java V5 开始,您可以使用 -agentlib:jdwp 选项,而不是 -Xdebug-Xrunjdwp。但如果连接到 V5 以前的 VM,只能选择 -Xdebug-Xrunjdwp。下面简单描述 -Xrunjdwp 子选项。

transport
这里通常使用套接字传输。但是在 Windows 平台上也可以使用共享内存传输。
server
如果值为 y,目标应用程序监听将要连接的调试器应用程序。否则,它将连接到特定地址上的调试器应用程序。
address
这是连接的传输地址。如果服务器为 n,将尝试连接到该地址上的调试器应用程序。否则,将在这个端口监听连接。
suspend
如果值为 y,目标 VM 将暂停,直到调试器应用程序进行连接。

要获得每个调试设置的详细解释,请参考 JPDA 文档(参见 参考资料)。

清单 2 是一个示例,显示如何在调试模式下启动 VM 并监听端口 8765 的套接字连接。

清单 2. 作为调试服务器的目标 VM

				
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8765

清单 3 显示如何使用位于 8000 端口的主机 127.0.0.1 上的套接字连接运行中的调试器应用程序。

清单 3. 作为调试客户机的目标 VM

				
-Xdebug -Xrunjdwp:transport=dt_socket,address=127.0.0.1:8000

Eclipse 中的远程调试特性

 

Eclipse 是一个图形化 Java 调试器前端。JDI 在 org.eclipse.jdt.debug 包中实现。本文不详细讨论 JDI 实现。参见 参考资料 获得关于 Eclipse JDT 和 Java JDI 技术的信息。

我们首先应该知道要使用哪个 Eclipse 连接器。要了解 Eclipse 提供的远程连接类型,您可以转到 Eclipse 菜单并选择 Run > Debug Configurations...,在 Remote Java Application 中添加一个启动配置,然后从下拉列表中选择连接器。在 Ganymede 中共有两个连接器:

  • Socket Attach
  • Socket Listen

对于监听套接字的连接器,Eclipse VM 将是与远程 Java 应用程序连接的主机。对于连接套接字的连接器,目标 VM 将作为主机。这两种连接器对应用程序调试没有影响,用户可以任意选择。但根据经验,需要使用速度更快、更强大的计算机作为 VM 调试主机,因为需要计算的资源很多。

在调试 Java 应用程序之前,需要确保已经为远程应用程序启用所有调试选项。如果选项信息不可用,您将收到一个错误信息,比如 “Debug information is not available” 或 “Unable to install breakpoint due to missing line number”。您可以通过更改 Eclipse 菜单上的 Window > Preferences > Java > Compiler 来修改设置。

图 1. Eclipse 中的调试选项
Eclipse 中的调试选项

远程调试应用程序

 

现在,我们已经准备好远程调试应用程序。我们分步执行:

1. 使用简单类创建一个 Java 项目
我们为调试创建一个简单类。清单 4 给出了示例代码。清单 4. 调试示例代码
				
package com.ibm.developerWorks.debugtest;

public class test {

public static void main(String[] args) {
System.out.println("This is a test.");
}
}
2. 设置一个断点
在代码中设置一个断点。在这个例子中,我们在 System.out.println("This is a test."); 这行中设置断点。


图 2. 在 Eclipse 中设置断点
在 Eclipse 中设置断点

3. 从本地调试应用程序
在调试应用程序之前,确保已经为项目启用图 1 中描述的调试选项。从本地调试应用程序是没有必要的,但是这可以确保是否所有调试信息都可用。右键单击 Java 项目,并选择 Debug As,然后选择 Java Application(参见图 3)。如果应用程序在断点处停止执行,则表明调试信息正确显示。这时,可以继续使用这些调试特性,比如显示调试堆栈、变量或断点管理等等。


图 3. 从本地调试应用程序
从本地调试应用程序

4. 导出 Java 项目
我们将使用这个应用程序作为调试目标。右键单击 Java 项目,选择 Export,然后选择 Java,最后选择 JAR fileRunnable JAR file 导出项目。将在指定的位置生成 JAR 文件。注意,如果 Java 源代码与目标应用程序不匹配,调试特性将不能正常工作。
5. 手动运行 Java 应用程序
打开控制台手动启动应用程序,确保正确配置了 Java 运行时环境。


清单 5. 调用 Java 应用程序的示例

				
java -jar test.jar
6. 远程调试应用程序
将 JAR 文件复制到远程计算机或同一台计算机上的适当位置,调用调试服务器,然后为它添加一个客户机。简单的 Java 应用程序就可以充当调试服务器或客户机。您可以在 Eclipse 中选择 Socket AttachSocket Listen 连接类型,这取决于特定的配置。接下来的两个小节将学习如何将应用程序作为服务器或客户机运行。

作为调试服务器的目标 VM

下面这个示例远程调用 Java 应用程序作为调试服务器,并在端口 8000 监听套接字连接。目标 VM 将暂停,直到调试器连接。

清单 6. Eclipse 连接套接字模式下的 VM 调用示例

				
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address="8000" -jar 
     test.jar 

使用远程启动配置启动 Eclipse,并指定远程应用程序的目标 VM 地址。为此,单击 Run > Debug Configurations,然后在 Eclipse 菜单中双击 Remote Java Application。从最新创建的启动配置中为目标应用程序指定 IP 和端口。为了在同一台机器上运行远程应用程序,仅需将主机 IP 指定为 localhost 或 127.0.0.1。

图 4. 连接套接字连接的配置
连接套接字连接的配置

选择 Allow termination of remote VM 选项终止在应用程序调试期间连接的 VM。

图 5. Eclipse 中的 Terminate 按钮
Eclipse 中的 Terminate 按钮

作为调试客户机的目标 VM

第二个示例使用一个简单的 Java 应用程序作为调试客户机,并且调试器前端作为调试服务器。Eclipse 使用套接字监听模式连接类型进行监听。必须先启动调试前端来监听特定的端口。图 6 给出一个用于设置监听的示例配置。

图 6. 监听套接字连接的配置
监听套接字连接的配置

单击 Eclipse Debug 按钮,状态栏将显示消息 “waiting for vm to connect at port 8000...”。看到这个消息后,启动远程应用程序。清单 7 显示了如何将 Java 应用程序作为调试客户机并调用它,然后使用端口 8000 上的主机 127.0.0.1 的套接字将其连接到一个正在运行的调试器应用程序。

清单 7. Eclipse 监听套接字连接模式中的 VM 调用示例

				
    java -Xdebug -Xrunjdwp:transport=dt_socket,address=127.0.0.1:8000,suspend=y 
         -jar test.jar 

如果一切进行顺利,将会显示一个调试透视图帮助调试应用程序,并且远程 Java 应用程序将正常停止。这类似于步骤 3 中的本地调试(参见图 3)。在这里,您可以使用标准的调试特性,比如设置断点和值、单步执行等。

结束语

 

本文演示如何使用 Eclipse 内置的远程 Java 应用程序配置类型对应用程序执行远程调试。介绍了如何设置 Java 应用程序以调用远程调试,并帮助您理解 Eclipse 提供的连接器。最后,您还学习了如何将这些技术应用到项目中。

共26篇,第1/3页 首页 1 2 3 下一页 尾页