Java多线程编程的议论

Java多线程编程的讨论

        项目组的新进同事要求做一次关于Jav语言多线程开发相关的培训,我整理了一下思路,按照API的发展和变化列出了一份提纲,包含官方的介绍和自己的理解,不一定完全正确。限于篇幅和培训的目的,因而内容不会太全,只提到项目组在日常工作会遇到的一些议题,另外的一些比如惊群、虚假唤醒之类的话题则由于太深入而没有包括。

基础知识

        提到线程,不能不说一下进程。按照现代操作系统的理论,或者说实践现状,进程是资源的载体,线程是执行的载体;进程内部可以有多个线程,这些线程共享进程的地址空间,相互之间可以访问数据。这两点很重要。对于Java程序员来说,JVM就是平台,隔离了不同操作系统之间的差异。但是JVM也是运行在操作系统之上的普通应用,同样需要使用操作系统提供的API或者系统调用来完成一些基本操作,因此有意识的去了解一些操作系统相关的知识点,有助于以后的发展。

什么是多线程

        简单的问题通常不好回答。尤其在毕业几年之后,越来越有这种感觉。什么是多线程,别人问的和自己所理解的是不是同一回事,自己的理解正不正确,回答的方式和答案有没有满足提问人等等,都是大问题。还好现在不是参加学校里组织的操作系统课程考试,所以不需要使用那么学术化的方式作答,举个例子就可以混过去。比如一个程序的代码里实例化了多个线程对象,那么这个程序即是多线程的,听起来似乎很形象,也很好理解。

Java的多线程

        相对于C/C++,Java语言有一项优势即是多线程。这项优势是内建在语言内部的,Java在语言关键字中提供了synchronized来实现关键区保护和线程之间的同步,另外还在基础库中的提供了线程相关的基本类和方法,因而使用Java做多线程开发是相当容易的,首先编译器就可以帮忙完成部分代码有效性的检查。不得不说C/C++的多线程开发是很复杂的,从参数传递、API使用、跨平台等方面,都很麻烦,而且缺少美感。

Thread

        这是Java类库中提供的线程对象。从1.0时代延续至今,虽然看起来不那么时尚,但宝刀未老,为多线程编程提供了很多的灵活性。但Thread库的缺点也很明显。

1、Java语言只支持单继承,用户直接使用Thread类时并不那么方便,因为父类只有一个,一旦继承了Thread类,就没有办法继承其它类了;

2、Thread类中包含了很多基础的方法,同时也包含了不应当出现的方法,比如stop、resume、suspend等。这些方法被标记为@Deprecated,教科书中对这些方法的描述为随意使用可能会出现死锁等诡异的问题。为什么这些方法会有如此境遇,被定义出来却不允许程序员使用。其实回想一下基础知识中提到的“进程是资源的载体,线程是执行的载体”,原因就非常好理解了。比如锁是操作系统提供的内核对象,代码中申请使用锁对象,操作系统会在进程的信息块中记录下来(注意这个信息并没有记录在线程的头部),这样当进程退出时,操作系统可以把进程占有的资源全部释放掉。线程则不同,由于线程的信息块中缺少相关的信息,操作系统在线程退出之后无法回收资源,因而当线程被意外杀掉时,线程持有的锁可能依然处于锁定状态,这导致其它线程无法获取到锁,从而形成了死锁。

3、JDK1.5之前缺少现成的线程池实现,这导致应用中的线程数量不可控,而自行实现的线程池质量良莠不齐,Bug多多,往往需要花费大量时间定位解决。

。。。

Runnable

        Runnable是从1.0时代即存在的接口,一般需要配合Thread对象使用。Java语言限定类为单继承,但是允许类实现多个接口,使用Runnable给使用多线程编程提供了方便,消除了继承Thread类时带来的不便。

Callable/Future

        使用Thread或者Runnable时存在一些困难,比如无法准确获知线程何时执行完毕,线程执行的结果或者执行中抛出的异常。Thread类虽然提供了join方法可以用来等待线程结束,但使用时多有不便或者易出Bug;而且对于程序中大量存在的多线程应用,等待结束时处理的代码相对又比较近似,容易滋生冗余或者重复的代码。到JDK1.5时,问题有所改观,新类库的出现使得上述问题有了解决的可能,Callable和Future一起提供了线程代码执行结束后获取结果的便利途径。Callable类的执行方法定义了返回值和抛出的异常,而Future则提供了获取Callable的返回值或者异常的方法定义。

FutureTask

        FutureTask类的出现完美的解决了线程执行结束后,调用者如何获取到通知。这个类中定义了done方法,线程在执行完成之后,done方法会被自动回调,调用者可以将任务结束之后的操作放在这个方法里,真正实线程任务的异步处理。

线程池

        JDK1.5开始,Java提供了预定义的线程实现,由此方便了多线程应用的开发,使得Java程序员可以聚集在应用类问题的解决,不需要特别关注底层的实现。

Executor

        Executor一方面定义了线程池应当实现的基本能力和语义,另外间接屏蔽了Thread和Runnable在使用层次的依赖关系。

ExecutorService

        ExecutorService更进一步定义了线程池对池内线程进行操作的行为,比如shutdown行为以及检查线程池状态的方法;另外丰富了向线程池提交任务的方式,并且提供了Callable和Future之间关联的途径,如submit方法。

Executors

        对于普通应用来说,工具类Executors中工厂方法构造的线程池可以满足大多数场景下的应用,一般不需要定制。

Exectors的背后

        显然,JDK提供的预定义线程池可以满足多数应用的需求,但定制需求仍然不可避免。比如期望线程池中的线程拥有特别的标识,以方便定位问题,或者线程池中活跃线程的数量、最大线程数量、任务队列满时的处理策略等等需要特别的定制。如下是线程池实现类ThreadPoolExecutor的一个构造函数,由此可以看出ThreadPoolExecutor类拥有强大的定制能力,足以满足前述需求。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
        理解ThreadPoolExecutor背后的实现原理,我建议阅读JDK1.6版本ThreadPoolExecutor的源码,重点是execute相关方法的源码。看过之后,相信对于线程池类如何选择任务队列、核心线程数量、最大线程数量、线程工厂、任务队列拒绝策略等等会有相对深入的理解。JDK1.7中ThreadPoolExecutor类的实现发生了变化,总体来说复杂度上升,难度相应有所提升,可能需要花费更长的时间来理解。

多线程的挑战

       对于经验不足的程序员来说,前述的多线程的API似乎很复杂,这其实是一种假象。多线程编程的困难其实和API的使用关系不大,而在很大程度上和数据共享有关系。前面讲过,进程内部的多个线程共享相同的地址空间,因而可以访问到相互的数据,这是好事;但是相同的数据多个线程都可以修改,那么怎么样才能保证多个线程对数据修改之后仍然可以保证数据的结果是正确的。这不但和编码有关,很多时候还是设计层面需要慎重考虑的重要大事。Java语言通过内建的关键字synchronized来解决这个问题。

单线程VS多线程

        To be or not to be, it is a question.

        单线程和多线程,一对欢喜冤家。什么时候适宜使用单线程,什么时候使用多线程,如何取舍是一个大问题。从代码开发角度讲,单线程的代码在开发、维护、问题处理时有非常大的优势,因为消除了数据共享的问题,不存在并发修改的可能,也不需要关注线程切换引入的消耗,这样规避了很多问题,比如nginx即是完全的单线程应用。对于设计良好的多线程应用,可以充分利用硬件资源,提高处理吞吐量,提升系统的处理能力,提供良好的外部体验。那么该如何选择呢?我也不知道,这个话题可能需要具体问题具体分析。我只是想说明工具只有在合适的地方被合理的应用,才会发挥最大的效用。

其它

        多线程对JDK、程序员的影响巨大,下面的几个话题也很有意思,虽然实现原理复杂,但好在使用上相对要简单一些。

        StringBuilder VS StringBuffer

        ArrayList VS Vector

        HashMap VS ConcurrentHashMap


。。。。

2013年9月7日夜