线程是Java的一大特色,从语言上直接支持线程,线程对于进程来讲的优势在于创建的代价很小,上下文切换迅速,当然其他的优势还有很多,缺点也是有的,比如说对于开发人员来讲要求比较高,不容易操作,但是Java的线程的操作已经简化了很多,是一个比较成熟的模型。很多时候,我们都用不到线程,但是当我们有一天不走运(或者走运)的时候,我们必须要面对这个问题的时候,应该怎么办呢?本文是我的学习笔记和一些总结,试图解决这个问题,引领还没有接触过Java 线程的开发人员进入一个Java线程的世界,其实很多东西在网路上已经有朋友总结过了,不过我感觉没有比较循序渐进,要么太基础,要么太高深,所以这边我由浅到深的总结一下。但是很显然,我的资历尚浅,能力也很有限,如果有什么错误还望不吝赐教!麻烦发送mail到:fantian830211@163.com 而且,这些大部份的都有源码,如果需要也可以发mail到这个邮箱,真的非常希望有人能指正我的错误! (一) 基本的API介绍 1. 如何创建一个可以执行的线程类 创建一个线程有两个办法:继承Thread类或者实现Runnable接口。 首先:继承Thread类 这里一般只需要我们来重写run这个方法。下面是代码: public class SimpleThread extends Thread { public SimpleThread() { start(); } @Override public void run() { while (true) { System.out.println(this); // Imply other thread can run now, but we cannot assume that it will // work well every time, actually , most of time we can get the same // result, but not to a certainty. // yield(); try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } 其次:实现Runnable接口,代码如下: Public class Inner implements Runnable { private Thread thread; public Inner(String name) { thread = new Thread(this, name); thread.start(); } public void run() { while (true) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } 2. 几个常用的API 这边介绍几个常见而且重要的的线程API,这边JDK文档有更加详细的说明,其实JDK的文档就是个很好的学习资料,常备很重要哦!
这边只是介绍了几个常用的API,但是非常重要,其他的API可以查看JDK的相关文档。但是在操作系统的概念中,很显然,对于一个线程应该还有别的状态,对,确实还有,但是Java在实现的映射的时候,也实现了这些方法,只是不赞成使用,下面的主题将讨论这些方法以及这些方法的替代方法。 3. 已经不赞成使用的方法 对于一些不应该再使用的东西,有时候被称为反模式antipattern。这些都是概念上的东西,对于我们开发人员来讲,需要做的就是写出好的代码。
4. 跟线程相关的关键字 跟线程相关的关键字我能够想到的就下面两个:
其实线程的使用不在于语言的API,而在于对操作系统的理解和一些常见的调度算法,其实个人理解经验比较重要,后边介绍到线程的实现模式和设计模式。其实我还是以前的想法:对于语言的学习,首先学习语法和API,然后学习如何使用这些API在语法的框架内编写出高效的程序。很显然,模式就是实现后边的重要方法。模式常见的分类有实现模式、设计模式和架构模式。这里限于本人的能力问题,没有理解到架构上面去,所以这里只是研究了前两个。 (二) 线程实现模式 实现模式这边主要参考自Effective Java这本书,至少分类是,但是很多内容应该会很不相同,当然还有Think in java。Effective Java是短小精悍的一本书,其中有太多的Java的关于实现模式的建议,但是这边把这本书的内容归类到实现模式,是我个人的想法,如果有什么不正确,万望指正。但是,个人认为这些概念性的东西仍然不会损害到我们需要讨论的问题的实质。 1. 共享数据同步 上面有提到过synchronized关键字,这个关键字保证一段代码同时只能有一个线程在执行,保证别人不会看到对象处于不一致的状态中。对象将从一种一致的状态转变到另一种一致的状态,后来的线程将会看到后一种状态。 在Java中,虚拟机保证原语类型(除double和long)的读写都是原子性的。即不需要同步,但是如果不对这样的数据读写进行同步,那么后果将很严重。可以参照Effective Java的解释,这里还要简单的提示意下,Effective Java中有提到double check这种方式,而且我的源代码中多次用到这种方法,单是需要提醒一下,如果用这种方式来实现singleton的话,就不可以了,因为这样有可能导致不完整的对象被使用,单是源码中的double check用的都是原语类型,所以OK。 这边的建议是如果修改原语类型或者非可变类的属性,可以同步或者使用volatile关键字。如果是其他对象,必须同步。关于尽量少使用同步,这边的建议是,我们这样的初学者在不知道如何优化的情况下就不要优化,我们要的是正确的程序,而不是快的程序。 2. wait方法的使用 wait方法是一个很重要的方法,前面有介绍过这个方法,不但可以使一个线程等待,而且可以作为实现suspend的替代方法的一个方法。 Wait方法的标准使用方式如下: synchronized (obj) { while (condition) wait(); } 这里,对应wait方法还有一个notify和notifyAll方法,到底我们应该如何使用这两个方法来唤醒等待的线程呢?很显然notifyAll的使用是最安全的,但是会带来性能的降低。这里又提到我们初学者,应该优先考虑这个方法,而不是notify。 3. 不要依赖线程调度器,管好自己的事情 Thread.yield这个方法并不能保证线程的公平运行,所以这个方法不应该依赖。还有就是线程的优先级,Java的线程优先级有10个等级,但是这个等级几乎没有什么用处,所以我们也不应该依赖这个优先级来控制程序,当然仍然可以优化一些服务,但是不能保证这些服务一定被优化了。我们应该尽量控制对critical resources的方法线程数,而不是用优先级或者yield来实现对资源的访问。 4. 不要使用线程组 线程组是一个过时的API,所以不建议使用。但是也不是一无是处,“存在即合理”嘛! (三) 线程设计模式 什么是模式呢?Martin Flower先生这样描述:一个模式,就是在实际的上下文中,并且在其他上下文中也会有用的想法。 这边的线程设计模式大部分参考自 1. Single Threaded Execution 这个模式在Java里说的话有点多余,但是这边还是先拿这个开胃一下。很明显,从字面的意思,就是说同一时刻只有一个线程在执行,Java里用synchronized这个关键字来实现这个模式。确实多余 L!看看UML吧!其实用这个图来描述有点不好。其实应该用别的图来描述会比较好!比如协作图。 2. Guarded Suspension 网上有一个比较好的描述方式:要等我准备好噢! 这里我们假设一种情况:一个服务器用一个缓冲区来保存来自客户端的请求,服务器端从缓冲区取得请求,如果缓冲区没有请求,服务器端线程等待,直到被通知有请求了,而客户端负责发送请求。 很显然,我们需要对缓冲区进行保护,使得同一时刻只能有一个服务器线程在取得request,也只能同一时刻有一个客户端线程写入服务。 用UML描述如下:
具体实现可以参看代码。 但是,这个模式有一点点瑕疵,那就是缓冲区没有限制,对于有的情况就不会合适,比如说您的缓冲区所能占用的空间受到限制。下面的Producer Consumer Pattern应该会有所帮助。 3. Producer Consumer Producer Consumer跟上面的Guarded Suspension很像,唯一的区别在于缓冲区,Guarded Suspension模式的缓冲区没有限制,所以,他们适用的场合也就不一样了,很显然,这个考虑应该基于内存是否允许。Producer Consumer的缓冲区就像一个盒子,如果装满了,就不能再装东西,而等待有人拿走一些,让后才能继续放东西,这是个形象的描述。可以参考下面的UML,然后具体可以参看源码。
4. Worker Thread Worker Thread与上面的Producer-consumer模式的区别在于Producer-consumer只是专注于生产与消费,至于如何消费则不管理。其实Worker Thread模式是Producer-consumer与Command模式的结合。这边简单描述一下Command pattern。用UML就和衣很清晰的描述Command pattern。
这个模式在我们的很多MVC框架中几乎都会用到,以后我也想写一个关于Web应用的总结,会提到具体的应用。其实Command pattern模式的核心就是针对接口编程,然后存储命令,根据客户短的请求取得相应的命令,然后执行,这个跟我们的Web请求实在是太像了,其实Struts就是这样做的,容器相当于Client,然后控制器Servlet相当于Invoker,Action相当于ICommand,那么Receiver相当于封装在Action中的对象了,比如Request等等。 上面描述过Command pattern之后,我们回到Worker模式。 这边看看worker的UML:
从图中可以看到,CommandBuffer这个缓冲区不仅仅能够存储命令,而且可以控制消费者WorkerThread。这就是Worker模式。下面的Sequence应该会更加明确的描述这个模式,具体可以参看代码。
5. Thread-Per-Message Thread-Per-Message模式是一个比较常用的模式了,如果我们有一个程序需要打开一个很大的文件,打开这个文件需要很长的时间,那么我们就可以设计让一个线程来一行一行的读入文件,而不是一次性的全部打开,这样从外部看起来就不会有停顿的感觉。这个模式Future模式一起学习。 6. Read-Write-Lock 考虑这样一种情况:有一个文件,有很多线程对他进行读写,当有线程在读的时候,不允许写,这样是为了保证文件的一致性。当然可以很多线程一起读,这个没有问题。如果有线程在写,其他线程不允许读写。如果要比较好的处理这种情况,我们可以考虑使用Read-Write-Lock模式。 这个模式可以如下描述:
其实这个模式的关键在于锁实现,这里有个简单的实现如下: public class Lock { private volatile int readingReaders = 0; @SuppressWarnings("unused") private volatile int writingWriters = 0; @SuppressWarnings("unused") private volatile int waitingWriters = 0; public synchronized void lockRead() { try { while (writingWriters > 0 || waitingWriters > 0) { wait(); } } catch (InterruptedException e) { // null } readingReaders++; } public synchronized void unlockRead() { readingReaders--; notifyAll(); } public synchronized void lockWrite() { waitingWriters++; try { while (writingWriters > 0 || readingReaders > 0) { wait(); } } catch (InterruptedException e) { // null } finally { waitingWriters--; } writingWriters++; } public synchronized void unlockWrite() { writingWriters--; notifyAll(); } } 其实在锁里还可以添加优先级之类的控制。 7. Future Future模式是Proxy模式和Thread-Per-Message模式的结合。考虑下面的情况: 比如我们的word文档,里头有很多图片在末尾,我们打开这个文档的时候会需要同时读取这些图片文件,但是很明显,如果刚刚开始就全部读取进来的话会消耗太多的内存和时间,使得显示出现停顿的现象。那么我们应该怎么做呢,我们可以做这样一个对象,这个对象代表需要读入的图片,把这个对象放在图片的位置上,当需要显示这个图片的时候,我们才真正的填充这个对象。这个就是Proxy模式了。当然Proxy不仅仅是这么个意思,Proxy的真正意思是我们之需要访问Proxy来操作我们真正需要操作的对象,以便实现对客户段的控制。 这边先简单描述一下Proxy模式:
当Client请求的时候,我们用Proxy代替RealObject载入,当Client真正需要getObject的时候,Proxy将调用RealObject的RealObject方法,获得真正的RealObjct。用Sequence来描述上面这段话:
下面回到Future模式,这个模式就是我们不需要真正对象的时候,首先生成一个Proxy对象来替代,然后产生一个线程来读取真正的对象,读取结束之后将这个对象设置到Proxy中,当真正需要这个对象的时候,我们可以从Proxy中取到。如下:
具体可以参看代码的实现。 8. Two-phase Termination Two-phase Termination模式就是让线程正常结束,也就是结束之前进行一些善后处理,释放掉该释放的资源,完成自己当前的任务。在Java语言中,有一个方法stop,这个方法会使当前线程结束,但是我们不应该使用这个方法,因为他将会导致灾难性的后果。那么我们应该怎么做呢?这里其实上面有实现过,就是使用设置标志的方法来替代stop方法。具体可以查看:已经不赞成使用的方法和代码。 9. Thread-Specific Storage Thread-Specific Storage模式的考虑是当资源的访问不需要线程的通信的时候,我们可以使用这个模式,这个模式的做法是每个线程有自己的一个区域,来存储自己的变量,然后需要的时候操作这个变量。在Java中,已经实现了ThreadLocal,我们可以用他来实现这个模式,这边有一个简单的实现: public class MyThreadLocal { @SuppressWarnings( { "unchecked", "unused" }) private Map storage = Collections.synchronizedMap(new HashMap()); @SuppressWarnings("unchecked") public Object get() { Thread current = Thread.currentThread(); Object obj = storage.get(current); if (obj == null && !storage.containsKey(current)) { obj = initValue(); storage.put(current, obj); } return obj; } @SuppressWarnings("unchecked") public void set(Object obj) { storage.put(Thread.currentThread(), obj); } public Object initValue() { return null; } } 10. Immutable 其实多线程的问题有一个很大的麻烦就是如何控制资源的同步,就是防止当前线程的中间状态被下一个线程看到,这个有两个办法可以实现,首先,就是同时只能有一个线程在访问,另外一个办法就是使得资源变成非可变类,既然是不变的,大家就可以随便访问了。 11. Balking 考虑这样一个情况:有一个比较好的洗手的地方,你可以按按钮来放水,其实它旁边还有一个传感器,可以感受到您的手过来了,应该放水,那么如果您已经按过按钮,水正在放,那么传感器的放水信号应该如何处理呢,很显然,需要丢弃这次放水请求。反过来也一样。 Sequence如下: 线程的学习笔记和一些总结大概就这么多了,想想这段时间的学习,花费了很多的时间,但是效果是很多东西只是从书本上看来的,实在是可惜没有办法真正的实践一下,所以这些东西其实应该有更深刻的理解。希望有这么一天!!!! |
|
来自: shaobin0604@1... > 《Java》