Java线程编程

Java程序开发中有大量的可复用资源,可复用的公用类;有很多中间件已经帮助我们解决了多线程的问题,所以很多开发人员是不需要深入的涉及到这个话题的,而要自己去开发一个服务端的应用的时候,多线程的编程、调试、发布、问题跟踪就变得无可避免了。
多线程编程需要涉及到两个方向,一个是使用Java语言进行程序设计,必须了解语言本身对于线程的支持和其编程模型;另外一个是运行时的运行状况,更多的是基于现有的硬件、先有的操作系统的配置问题。所有的应用都必须运行一定的硬件配置环境、一定的操作系统之上,所以线程的运行时状态无可避免的要在应用设计阶段进行考虑了。
1.对于共享可变数据的互斥(什么时候使用关键字synchronized):Java虚拟机通过对象锁来实现互斥,达到多个线程在同一个共享数据上独立而互不干扰地工作。

1
2
3
4
5
private int sequence = 0;

public synchronized int getSequence(){
return sequence++;
}

线程间通讯,保证共享对象对于多线程访问是从一种一致的状态跃迁到另外一种一致的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Worker implements Runnable{
private boolean running = false;

public void run(){
while(isRunning()){
//
}
}

public synchronized boolean isRunning(){
return this.running;
}

public synchronized void stop(){
this.running = false;
}

public synchronized void startRequest(){
this.running = true;
}
}

2.协作(什么时候使用wait和notify):主要是用于多线程为了同一个目标而共同协作性的工作,应用需要设计多线程的协作工作机制。一般性地是,线程需要反复检查某一个数据结构,在等待某些条件的发生,又避免忙等(busy-wait)的时候。
使用wait()是需要仔细阅读以下javadoc了,wait(0)的情况很特殊:线程一直处于等待,直到被唤醒(notify()或者notifyAll())。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Object monitor = new Object();

public void run(){

synchronized(monitor){
while(){
try{
obj.wait()
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

Java使用的同步机制是监视器,为了更好的使用同步机制,就需要虚拟机是怎么使用监视器的机制的?
Java监视器主要分成了三个区域,入口区、监视区、等待区,所有进入到监视器开始处的线程首先都是进入到入口去,(可能阻塞)等待成为监视区的持有者;监视区域同时只能被一个线程持有并执行,只有当一个线程执行完了监视区域动作(该线程将释放监视区域并推出监视器)或者执行了等待命令(该线程将释放监视区域进入到等待区,直到执行了唤醒命令才能重新持有监视区域)才能释放监视区域;只有那些持有监视区域的线程执行了等待命令之后就会进入到等待区。所以基本可以了解了监视区域的各个功能、线程进出的时机动作。而且分析互斥和协作情况下线程和监视器的工作状态基本也就清晰了。
1.互斥情况下,多线程将会在等待区阻塞的等待持有监视区域,因为同时只会有一个线程执行监视区域(代码即指令),监视区域主要是共享数据或者共享资源。(例外就是虚拟机实现不是基于时间片的,那么监视器就会用来协调多线程的执行策略,将不仅仅是共享数据或者共享资源了。)
2.协作情况下,监视器主要协调多个线程之间共同工作,即是,一个已经持有监视区域的线程,通过执行等待命令,释放监视区域进入到等待区,那么该线程阻塞并一直持续暂停状态,只有监视区域持有线程执行了唤醒该线程命令并释放监视区域之后该线程才能重新持有监视区域,直到该线程再次释放监视区域。

运行阶段,Java线程的线程模型又是什么样的呢,究竟需要应用设计开发人员需要注意什么呢?
这主要涉及到Java的内存模型(Memory Model)、JVM实现等,其中我正在使用的虚拟机是Sun基于Solaris系统的,其线程模型参考《Threading》。Java的线程在Java2都是操作系统的线程,并没有了”green threads”的概念了,每一个线程的申请在向JVM申请资源的同时也是在向操作系统申请资源,对于各家JVM在不同的操作系统之上的thread stack size大小也有一些差别,其中Sun JVM可以通过-XX:ThreadStackSize(-Xss)参数进行设置(在默认情况下,”Thread Stack Size (in Kbytes). (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]”).
在我们设计并发要求比较高的服务端应用的时候,线程将会变得相当宝贵了,而且就针对操作系统而言,一个进程所能申请的线程的数量也是限制的,所以如何使用和调度多线程是一个重要的环节。在系统运行阶段,进行调优的时候不可避免的要面对多线程的资源相关的几个关注点:thread stack size、thread local heap、garbage collection affects、intimate shared memory。

进行多线程的设计编码,要极力追求简洁原则,尽力将多线程的调度和编码和业务相关的逻辑进行解耦,这样多线程处理模块被抽象出来,可复用度变高,对于分析、调试、问题跟踪多线程的设计编码就不再变得臃肿复杂;避免复杂、过多的条件判断等待、wait-notify,线程意外退出或者形成死锁。

在今天的编程生活中,我们也要面对和解决多线程编程的调试、单元测试、跟踪等方面的问题。解决这些问题也是我们的乐趣之一:)

参考:
《Effective Java 中文版》
《深入Java虚拟机》
http://java.sun.com/docs/hotspot/threads/threads.html
http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
http://www.theserverside.com/tt/knowledgecenter/knowledgecenter.tss?l=ConcurrencyTestingJavaApps