设计原则
在编程中,我们必须要假设一点,就是其他程序员会以各种稀奇古怪的方式来使用我们编写的类。
-
不暴露一切不需要暴露的字段。向外界提供本对象所引用的内部对象的引用时一定要小心。必要的话克隆一份(保护性副本)给他而不是直接给出引用。
-
使用工厂方法而非构造函数来初始化对象。防止this引用在构造过程中逸出。
新手程序员常犯的一个错误就是在构造函数里完成一堆事情而且也不注意先后顺序。会导致构造函数没有完成前,别的线程已经对这个对象可见了(比如提前将本实例的引用写入到别的对象中)。推荐是构造函数绝对不对外发布任何本实例的引用。对外发布由工厂方法完成。工厂方法完成调用构造函数,发布对象和后续处理(如启动新的线程等等)。 -
使用栈封闭(仅通过局部对象生成和使用对象)或者ThreadLocal的方式达成线程封闭的要求(即保证某个对象永远只能被一个线程访问到。
-
不可变对象一定是线程安全的。
-
使用JAVA监视器模式———把对象的所有可变状态都封装起来。仅允许通过提供的公开方法来执行。所有的公开方法都是由synchronized关键字修饰的。典型例子是Vector和Hashtable。这个会带来另外一个好处,就是如果在代码中有人改变了这个状态,你至少可以方便在这里设断点,从而具体定位是那个线程以如何的堆栈访问到了这个点。
-
如果某个对象不是线程安全的,那么就必须确保该个对象只能又单个线程来访问(线程粉笔)。或者,使用一个锁来保证不会出现并发问题。
常见的问题及其解决方案
-
死锁
常见的产生死锁的情况有相互排斥(一个线程永远占有共享资源),循环等待,部分分配等。 -
活锁
活锁一般是来自于对死锁不正确的处理引起的。比如两个人在一个很宅的胡同里。 一次只能并排过两个人。 两人比较礼貌,都要给对方让路。 结果一起要么让到左边,要么让到右边,结果仍然是谁也过不去。 类似于原地踏步或者震荡状态。 -
线程饥饿死锁
对应一个单线程话的Executor,一个任务将另一个任务提交到相同的Executor中,并等待新提交的任务的结果,这总会引发死锁。第二个任务滞留在工作队列中,直到第一个任务完成,但是第一个任务不会完成,因为它在等待第二个任务的完成。简单来说,释放锁的任务因为缺乏线程资源而无法执行。已经执行的线程因为没有获取锁所以阻塞了。