Effective Java(五)

来源:互联网 发布:php扩展开发.pdf 编辑:程序博客网 时间:2024/06/06 01:34
本节主要介绍Effective Java中并发和序列化两章节的内容。
一.同步访问共享的可变数据
并发编程主要考虑的就是多线程安全问题,当多线程同时访问共享的可变数据时,如果没加适当的锁,必然会导致线程安全问题。简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。
二.避免过度同步
虽然并发编程我们需要注意线程安全问题,但是也不能过分使用同步锁,这样会大大降低程序性能,甚至可能产生死锁。书中介绍一点,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制。换句话说,在一个被同步的区域内部,不要调用被设计为要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。从包含该同步区域的类的角度来看,这样的方法是外来的。这个类不知道该方法会做什么事情,也无法控制它。根据外来方法的作用,从同步区域调用它会导致异常,死锁或者数据破坏。
通常,你应该在同步区域做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。
三.executor和task优先于线程
我们要尽量使用线程池,而不是直接操作Thread,线程池提供了更优雅的终止方式,并且还提供了更强大的功能,同时对线程可重复利用,使用线程池的好处如下
(1)降低资源消耗。通过重复利用已创建的线程减低线程创建和销毁造成的消耗。
(2)提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立刻执行。
(3)提高线程管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可进行统一分配,调优和监控。
四.并发工具优先于wait和notify
这节是我觉得比较有意思的章节,书中说自从java1.5版本,Java平台提供了更高级的并发工具,它们可以完成以前必须有wait和notify写代码完成的各项工作。Java提供的更高级的工具分为三类,Executor FrameWork,并发集合以及同步器。并发集合如常用的ConcurrentHashMap,BlockingQueue等,BlockingQueue可以实现生产者-消费者模式。同步器是一些使线程能够等待另一个线程的对象,常用的同步器有CountDownLatch和Semaphore。较不常用的有CyclicBarrier和Exchanger。
虽然书中说始终应该优先使用并发工具,而不是wait和notify,不过wait和notify还是很重要的,且一些面试会经常问道。
使用wait和notify时需要注意,wait永远要在循环里,永远不要在循环之外调用wait,因为循环会在等待之前和等待之后测试条件。在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程等待之前,notify方法已经被调用,则无法保证该线程从等待中苏醒过来。在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性是必要的。当条件不成立时,如果线程继续执行,则可能会破坏被锁保护的约束关系。当条件不成立时,有下面一些理由可使一个线程苏醒。
1.另一个线程已经获得了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。
2.条件并不成立,但是另一个线程可能恶意的调用了notify方法。
3.通知线程在唤醒等待线程时过度大方。例如只有某一些等待线程的条件已经被满足,但是通知线程仍然调用了notifyAll.
4.在没有通知的情况下,等待线程也可能(很少)会苏醒过来,这被称为"伪唤醒"。
一个相关的话题是,为了唤醒等待的线程,使用notify还是notifyAll?notify唤醒的是单个正在等待的线程,而notifyAll则是唤醒所有正在等待的线程。一种常见的说法是,应该总是使用notifyAll,它总会产生正确的结果。你可能会唤醒一些其他线程,但是不会影响程序的正确性,这些线程醒来之后,检查条件,发现条件不满足,继续等待。从优化的角度来看,如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从条件中唤醒,那么就应该使用notify。然而即使满足这样的条件,仍然推荐使用notifyAll.
五.慎用延迟初始化
延迟初始化是延迟到需要域的值时才将他初始化的这种行为。如果永远不需要这个值,这个域就永远不会初始化。对于延迟初始化的建议:除非绝对必要,否则就不要这么做。延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。根据延迟初始化的域最终需要初始化的比例,初始化这些域需要多少开销,以及每个域多久访问一次,延迟初始化实际降低了性能。
当有多个线程时,延迟初始化是需要技巧的。如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能产生严重的bug。
简而言之,对于大多数的域应该正常低进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,使用双重检查模式,对于静态域使用静态内部类的延迟初始化模式。
六.不要依赖线程调度器和不要使用线程组。
简单说就是线程调度器(Thread.yield)和优先级都不是很准确靠谱的,很不可控,程序的正确性不能对其有依赖。不要使用线程组,这个很少见到有使用的,书上介绍的线程组基本上一无是处,功能很弱,提供的几个功能中还存在缺陷,安全性也很弱,所以不要使用。
序列化部分
之前没工作的时候没有怎么使用过序列化,觉得用处不大,之后才发现服务化都是基于序列化实现的,本次只介绍Effective Java中的内容,后续会单独学习序列化再详细介绍。
七.谨慎的实现Serializable接口
要想使一个类的实例可以序列化,只需要实现Serializable接口,因为太容易,所以存在一种误解,"几乎所有类都应该实现Serializable接口"。本节介绍实现Serializable存在的问题。实现Serializable接口最大的代价是,一旦一个类被发布了,就大大降低了改变这个类的灵活性。如果一个类实现了Serializable接口,它的字节流编码就变成了它的导出API的一部分,一旦这个类被广泛使用,就必须永远支持这种序列化格式。(不过序列化方式一般很少会改动吧)
八.考虑使用自定义的序列化形式
这条主要说,只有当默认的序列化形式能够合理低描述对象的逻辑状态时,才能使用默认的序列化形式,否则就要设计一个自定义的序列化形式,通过它合理地描述对象的状态。书中举的例子是一个字符串列表,默认的序列化形式会不遗余力地镜像出列表的所有项,以及这些项的双向链接。而这种应该使用自定义序列化出:链表中包含字符串的数目,然后紧跟着字符串即可。
九.保护性的编写readObject方法
之前有个类Period里,要求startDate 早于endDate,参数保护性拷贝时介绍的。这条主要说通过序列化的方式可以打破这种约束,导致start晚于end,所以希望通过保护性编写readObject来防止这种现象。不过这种需要通过二进制来打破,我个人觉得作用不大。
十.考虑用序列化代理代替序列化实例
序列化代理模式比较简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确的表示外围类的实例的逻辑状态。这个私有的静态嵌套类被称为序列化代理,它应该有一个单独的构造器,参数类型就是外围类,这个构造器只从它的参数中复制数据,不需要进行一致性检查和保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式,外围类及其序列代理都必须声明实现Serializable接口。如下所示:
     
private static class SerializationProxy implements Serializable{           private final Date start;           private final Date end;          SerializationProxy(Period p){               this.start = p .start ;               this.end = p .end ;          }
         
在外围类中添加方法
     
private Object writePlace(){           return new SerializationProxy(this);     }

有了这个writePlace方法,序列化系统永远不会产生外围类的序列化实例,但是攻击者可能伪造,所以只要让readObject方法抛异常即可。

     
0 0