(2)
volatile
这个关键字告诉编译器不要对这个属性或者值进行优化,也就是为了保证这个变量的同步性,如果这个值被更新,其他线程应该可以马上
访问到最新的值,而不是“脏值”。其实这个关键字是同步互斥的一个模型,但是现在没有实现。
synchronized
给对象或者Class加锁,分为对象锁和Class锁。
对象锁只是加在当前对象上,对别的对象没有影响,这种加锁一般都是把这个关键字用在方法和同步块上面。
Class锁就是加在这个Class上面,所有的其他对象想访问这个Class的对象的任何同步方法,必须获得这个锁。这种锁一般把这个关键字
用在静态方法中,或者显示的这样实现:
synchronized (AClass.class) {
while (isSuspended) {
wait();
}
}
一般我们很少用Class锁。
这里简单提一下monitor,个人感觉这里把monitor认为是一把锁也可以。网络上有朋友解释的比较好:在java中,每个对象只有一个
相应的monitor,一个mutex,而每一个monitor都可以有多个“doors”可以进入,即,同一个monitor中被守护的代码可以在不同的地方,
因为同一个对象可以出现在不同的代码段,只要mutex锁定的对象是同一个,每个入口都用Synchronized关键字表明,当一个线程通过
了Synchronized关键字,它就所住了该monitor所有的doors
其实线程的使用不在于语言的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先生这样描述:一个模式,就是在实际的上下文中,并且在其他上下文中也会有用的想法。
这边的线程设计模式大部分参考自林信良先生的《设计模式》,还有一些网路的文章,这些都是前辈们在使用线程时候的经验,
非常值得我们借鉴。还有就是林信良先生的设计模式非常通俗易懂,是入门级选手的最佳选择。关于线程的模式应该还有别的,
只是我这边现在只能总结这么多了,能力有限。这边用大量的UML来描述这些模式,但是由于我的UML学的不好,而且工具用的
不怎么熟,画的图应该会有些问题,当做草图来看就好了。
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 {
** volatile int readingReaders = 0;
@SuppressWarnings("unused")
** volatile int writingWriters = 0;
@SuppressWarnings("unused")
** 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();
}