201711 Java安全编码标准 读书笔记 - xiaoxianfaye/Learning GitHub Wiki
- 第1章 概述
- Rule 00. 输入验证和数据净化(IDS)
- Rule 01. 声明和初始化(DCL)
- Rule 02. 表达式(EXP)
- Rule 03. 数值类型与运算(NUM)
- Rule 04. 字符与字符串(STR)
- Rule 05. 面向对象(OBJ)
- Rule 06. Methods (MET)
- Rule 07. 异常行为(ERR)
- Rule 08. 可见性和原子性(VNA)
- Rule 09. 锁(LCK)
- Rule 10. 线程APIs(THI)
- Rule 11. 线程池(TPS)
- Rule 13. 线程安全相关的其他规则(TSM)
- Rule 13. 输入输出(FIO)
- Rule 14. 序列化(SER)
- Rule 15. 平台安全性(SEC)
- Rule 16. 运行环境(ENV)
- Rule 17. Java本地接口(JNI)
- Rule 49. 其他(MSC)
这本书介绍了很多Java安全编码相关的规范,不可能完全记住。我想通过这篇笔记整理出我需要记住的关键条目,平时编码几乎不涉及的条目不在此范围内。每个条目会采用“不合规代码->问题描述->解决方法->合规代码”的顺序整理。
软件程序往往包含多个组件作为子系统,其中每个组件会在一个或多个受信域中运行。例如,一个组件可以访问文件系统,但不允许访问网络,而另一组件可以访问网络,但不能访问文件系统。非信任解耦(distrustful decomposition)及权限分离(privilege separation)[Dougherty 2009]是安全设计模式的例子,它意味着首先需要减少需要授权的代码的数量,这就要在设计系统时,使用不互信的组件,并保证组件在特定的权限下运行。
当软件组件需要遵循安全策略时,可以允许它们跨越受信的边界来传输数据,对于任何一个组件,它们不能指定其自身的受信级别。受信的边界应该由应用程序的部署者来确定,要完成这件事情,则需要一个全系统级别的安全策略的帮助。安全审核员可以使用这样的安全定义,通过安全定义来确定该软件能否充分支持应用系统安全目标的实现。
一个Java程序可以包含自行开发和第三方开发的代码,Java被设计成允许执行非受信代码;因此,第三方代码可以运行在它自己的受信域中。可以认为使用公共的API接口的第三方代码已经处在受信边界之中。任何跨越受信边界的数据都应接受验证,除非产生这些数据的代码本身已经能够提供权限有效性的保证。一个用户或使用者可以在数据流入受信边界时,省略对数据的验证,当然前提是其受信边界是适当的。在所有其他的情况下,进入数据必须接受验证。
当一个组件从超出该组件受信边界的外部数据源接收数据的时候,这些数据可能是恶意的,并且会导致注入攻击,如下图所示。
程序必须采取以下几个步骤,从而通过受信边界而收到的数据确保是适当的并且是没有恶意的。这些步骤包括:
-
验证(Validation):验证是指这样一个过程,该过程可以保证输入的数据处在预先设定好的有效的程序输入范围之内。这就要求这些输入符合类型和数值范围的要求,并且对各个类别和子系统来说,输入不会发生变化。
-
净化(Sanitization):在许多情况下,数据是从另一个受信域直接传递到组件当中来的。数据净化的过程是指,通过这个过程,可以确保数据符合接收该数据的子系统对数据的要求。数据净化也涉及如何确保数据符合安全性的要求,在这些要求中,需要考虑数据泄漏问题,或者需考虑在数据输出跨出受信边界的时候,敏感数据的暴露问题。净化可以通过消除不必要的字符输入来完成,比如对字符进行删除、更换、编码或转义操作。净化可能在数据输入(输入净化)之后或数据跨受信边界传输之前(输出净化)进行。数据净化和输入验证可能是并存的,并且是相互补充的。关于数据净化的详细情况说明,请参见规则IDS01-J。
-
标准化(Canonicalization)和归一化(Normalization):标准化的过程是指将输入最小损失地还原成等价的最简单的已知形式。归一化的过程是一个有损转换的过程,在这个过程中,输入数据会转化成用最简单的(可预期)形式来表现。标准化和归一化必须在数据验证之前进行,从而防止攻击者利用验证例程来除去不合法的字符,并由此构建一个不合法的(或者是有潜在恶意的)字符序列。关于这方面的更多信息,请参见规则IDS02-J。归一化应当在所有用户输入都收集完整之后进行。不要归一化那些不完整的数据,或者将经过归一化处理的数据和没有经过归一化处理的数据合并起来。
有一种情况需要特殊考虑,就是复杂的子系统接受代表特定操作命令或指令的字符串数据的问题。组件接受这些字符串数据的时候,在这些字符串数据中,可能包含的特殊字符会触发命令或动作,从而造成软件的安全漏洞。
以下是一些可以对命令进行解释或者对指令进行操作的组件的例子:
- 操作系统的命令解释器(见规则IDS07-J)
- 具备SQL兼容接口的数据库
- XML解析器
- XPath评估器
- 基于轻量级目录访问协议(Lightweight Directory Access Protocol, LDAP)的目录服务
- 脚本引擎
- 正则表达式(regex)编译器
在必须要把数据发送到处在另一个受信域的组件的时候,发送者必须确保数据经过适当的编码处理,从而在穿过受信边界的时候,满足数据接收者受信边界的要求。例如,尽管一个系统已经被恶意代码或数据渗透了,但如果系统的输出是经过适当转义和编码处理的,那么还是可以避免许多攻击的。
系统的安全策略需要确定哪些信息是敏感的。敏感数据可能包括用户信息,比如社会保障或信用卡号码、密码或私钥。在不同等级受信域的组件中共享数据的时候,我们称这些数据是跨越受信边界的。因为在Java环境中,允许在同一个程序中的处在不同受信域的两个组件进行数据通信,从而会出现那些跨受信边界的数据传输。所以,如果在域中存在一个授权用户,而该用户没有数据接收权限,那么系统必须保证这些数据不会发送给处于该域的组件。如果有可能的话,这种处理只简单地禁止数据传输,也有可能对穿越受信边界的数据进行处理,将敏感数据过滤掉,如下图所示。
在很多情况下,Java软件组件会输出敏感信息。以下规则可以帮助减轻敏感信息泄露的风险:
- ERR01-J 不能允许异常泄露敏感数据
- FIO13-J 不要在受信边界处记录敏感信息
- IDS03-J 不要记录未经净化的用户输入
- MSC03-J 不要硬编码敏感信息
- SER03-J 不要序列化未经加密的敏感数据
- SER04-J 不要序列化和反序列化绕过安全管理器
- SER06-J 在反序列化时,对私有的可变的组件进行防御性复制
在Java中,接口、类和类成员(如字段和方法)是访问受控的。这种访问受控由访问修饰符(public、private或protected)来标识,或者可以通过不使用访问控制符来标识(使用默认的访问控制,也称为package-private访问)。
Java的类型安全是指,在一个字段中被声明为private或者protected的时候,或者是在默认的保护情况下(package), 它是不允许被全局访问的。然而,有一些Java平台的设计会让这种保护失效,从而导致出现安全漏洞,比如对Java反射机制的错误使用。对于Java高手来说,这些漏洞的出现并不奇怪,因为它们在文档中都已做了详细的描述,但一不小心,还是会掉入陷阱。比如说,如果一个字段被声明为public,那么它可以被Java程序中的任意一段代码直接访问,也可以被Java程序中的任意一段代码修改(除非这个数据成员同样被声明为final)。很显然,敏感数据是不允许保存在public域中的,因为如果有人可以直接访问该程序运行所在的JVM的时候,就会招致问题。
下表展现了一个简化的关于访问控制规则的视图。x表示在这个地方允许特定的访问。例如,在“Class”中,如果标识了x,表示在同一类中声明的代码可以访问该成员。同样,“package”一栏中,如果标识了x,那么表示,这个成员可以被任何定义在同一个包中的类(或者子类)访问,并且该成员可以在由同一个类装载器载入的类(或者子类)中定义。而同一个类加载器的条件仅适用于package-private成员访问的情况。
访问控制关键字 | Class | Package | Subclass | World |
---|---|---|---|---|
Private | x | |||
None | x | x | x | |
Protected | x | x | x | |
Public | x | x | x | x |
不同包中的子类不能访问包可访问的数据成员,可以访问不同包的Protected的数据成员。
类和类成员只能给予可能的最低的访问权限,这样的话,恶意代码威胁系统安全的可能性就会大大减少。只要有可能,应当避免使用接口来开放那些包含(或调用)敏感代码的方法;接口应当只允许可以公开访问的方法,而这些方法往往是这个类公开的应用编程接口(API)的一部分。(注意,这和Bloch推荐的优先考虑API接口设计的看法相反 [Bloch 2008,Item 16])。存在一种例外情况,就是在实现一个不可修改的接口的时候,可以开放一个可变对象的公共的不可变部分(见规则OBJ04-J)。此外,值得注意的是,即使一个非final类的可见性是package-private,它仍然容易被错误使用,特别是当它包含公共方法的时候。对于那些执行了所有必要的安全检查,并对所有输入进行了净化的方法,它们同样是可以通过接口开放出来的。
对顶级类来说,protected的访问控制是无效的,虽然对嵌套类来说,可以声明它为protected。对那些非final的公共类的字段来说,它们是不能被声明为protected的,这是为了防止在另一个包中该类的子类的非受信代码可以访问该成员。此外,如果protected成员是这个类的API的一部分,则需要继续得到支持。规则OBJ01-J要求声明私有字段。
当一个类、接口、方法或者一个字段作为API的一部分进行发布的时候,比如说对网络服务接入点来说,它可以被声明为public。其他类和成员应当要么声明为package-private,要么声明为private。例如,如果一个类在安全性上的考虑并不是至关重要,它应该定义一个public静态工厂来实现实例控制,这可以通过使用private的构造器来实现。
效能(capability)指的是在授权中可以进行沟通而不会被忘记的标识。效能这个词是由Dennis和Van Horn[Dennis 1966]引入的。它指向的是一个数值,这个数值指向的对象拥有一系列的对象访问权限。在一个基于效能的操作系统中的用户程序,必须使用效能来访问对象。
每一个Java对象都有不会被遗忘的标识。因为Java的==操作符会对引用等进行测试,它会被用来测试这个标识。因为有这个不会被遗忘的标识,所以允许使用一个对象引用作为标记,从而作为某种形式的动作需要的授权的一个不会被遗忘的证据[Mettler 2010a]。
授权会包含在对象引用中,这些对象引用被作为效能来使用。授权指那些运行代码带来的任何效果,而这些运行代码也许会带来其他的副作用。授权不仅包括那些对于外部资源例如文件或者网络套接字的操作,而且包含了那些在程序不同部分进行共享的可变数据结构的操作[Mettler 2010b]。
对于那些方法会执行敏感操作的对象的引用而言,它们会允许持有者执行这些操作的效能(或者要求一个对象代表这些持有者执行这些操作)。所以,这样的引用自身必须要被视为敏感数据,并且不能泄露给非受信代码。
一个很有可能让人感到惊讶的泄露效能和数据的源头是内隐类,这些类可以访问它们包含的类的所有字段。Java字节码缺少内置的对内隐类的支持,因此,内隐类必须编译成带有样式名称的普通类,如OuterClass$InnerClass。因为内隐类必须能访问它们的包含类的私有字段,所以对这些字段的访问控制,就被转换到对字节码中的包的访问来进行。因而,手写的字节码可以访问这些名义上的私有字段(请以“Java字节码工程的安全方面”[Schonefeld2002]作为例子进行参考)。
以下是考虑效能的规则:
- ERR09-J 禁止非受信代码终止JVM
- MET04-J 不要增加被覆写方法和被隐藏方法的可访问性
- OBJ08-J 不要再嵌套类中暴露外部类的私有字段
- SEC00-J 不要允许特权代码块越过受信边界泄露敏感信息
- SEC04-J 使用安全管理器检查来保护敏感操作
- SER08-J 在从拥有特性的环境中进行反序列化之前最小化特权
拒绝服务攻击试图使计算机的资源不可获得,或者对需要使用该计算机资源的用户来说,会造成资源不足的情况。通常这种攻击是持续服务的服务器系统需要重点关注的,它与桌面应用程序有很大区别;然而,拒绝服务的问题可以出现在所有类别的应用上。
拒绝服务可能出现在,相对于输入数据需要的资源消耗来说,使用比例上更为巨大的资源。通过客户端软件检查资源是否被过度消耗,并希望用户来处理与资源相关的问题是不合理的。但是,存在这样的客户端软件,它们可以检查那些可能会导致持久拒绝服务的输入,比如将文件系统进行填充的操作。
《Java安全编码指南》(Secure Coding Guidelines for the Java Programming Language)[SCG 2009]中列举了可能的攻击的例子:
- 请求一个大的矢量图片,如SVG文件或者字体文件。
- “Zip炸弹”(Zip bomb),那些诸如ZIP、GIF或那些经过gzip编码的HTML内容,会因为解压而消耗大量的资源。
- “XML解析炸弹”,在解析的时候,在扩展XML所包含的实体时,有可能会使得XML文档急剧增长。可以通过设置XMLConstants.FEATURE_SECURE_PROCESSING功能并设置合理的限度值解决前面的问题。
- 过度使用的磁盘空间。
- 在一个散列表中插入了多个密钥,而这些密钥使用相同的散列码。这样会导致最差的性能(O(n2))而不是(O(n))。
- 发起许多连接,服务器为每个连接分配大量的资源(例如传统的洪水攻击)。
针对拒绝服务攻击并防止出现资源耗尽的规则,有以下几点:
- FIO03-J 在终止前移除临时文件
- FIO04-J 在不需要时关闭资源
- FIO07-J 不要让外部进程阻塞输入和输出流
- FIO14-J 在程序终止时执行正确的清理动作
- IDS04-J 限制传递给ZipInputStream的文件大小
- MET12-J 不要使用解析函数
- MSC04-J 防止内存泄露
- MSC05-J 不要耗尽堆空间
- SER10-J 在序列化时避免出现内存和资源泄露
- TPS00-J 使用线程池处理流量突发以实现降低性能运行
- TPS01-J 不要使用有限的线程池来执行相互依赖的任务
- VNA03-J 即使每一个方法都是相互独立并且四原子性的,也不要假设一组调用是原子性的。
某些拒绝服务攻击是通过试图引入并发问题来实现的,如线程死锁、线程饥饿和竞态。
针对拒绝服务攻击并防止出现并发问题的规则,有以下几点:
- LCK00-J 通过私有final锁对象可以同步那些与非受信代码交互的类
- LCK01-J 不要基于那些可能被重用的对象进行同步
- LCK07-J 使用相同的方式请求和释放锁来避免死锁
- LCK08-J 在异常条件下,保证释放已持有的锁
- LCK09-J 不要执行那些持有锁时会阻塞的操作
- LCK11-J 当使用那些不能对锁策略进行承诺的类时,避免使用客户端锁定
- THI04-J 确保可以终止受阻线程
- TPS02-J 确保提交至线程池的任务是可中断的
- TSM02-J 不要再初始化类时使用后台线程
其他预防拒绝服务攻击的规则如下:
- ERR09-J 禁止非受信代码终止JVM
- IDS00-J 净化穿越受信边界的非受信数据
- IDS06-J 从格式字符串中排除用户输入
- IDS08-J 净化传递给正则表达式的非受信数据
其他的规则会处理安全漏洞,这些安全漏洞会导致拒绝服务攻击,但它们自己并不足以导致拒绝服务:
- ERR01-J 不能允许异常泄露敏感信息
- ERR02-J 记录日志时应避免异常
- EXP01-J 不要解引用空指针
- FIO00-J 不要操作共享目录中的文件
- NUM02-J 确保除法运算和模运算中的除数不为0
序列化使得Java程序中对象的状态能被抓取并写入到字节流中[Sun 2004b]。这使得该对象的状态能够保存下来,所以可以在未来需要的时候恢复(通过反序列化)。序列化可以通过网络使用RMI的方式调用Java方法,这些对象被编组(序列化),在分布部署的虚拟机之间进行交换,然后进行解组(反序列化)。序列化还广泛使用在Java Beans中。
可以用以下方式序列化对象:
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("SerialOutput"));
oos.writeObject(someObject);
oos.flush();
可以用以下方式反序列化对象:
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("SerialOutput"));
someObject = (SomeClass) ois.readObject();
序列化能够捕捉对象中的所有非暂态字段,包括那些正常情况下不能读取的非公有字段,这通过Object类的Serializable接口来实现。序列化的值写入字节流之后,如果字节流是可读的,那么原先那些正常情况下不能访问的字段的值也就可以通过推导得出。此外访问,当反序列化一个类时,原来保存的值可能会被修改或者伪造。
引进一个安全管理器并不能防止正常情况下无法访问的字段被序列化和反序列化(如果字节流需要被存储或发送,那么必须给予它从文件或网络进行读写的权限)。网络流量(包括RMI)可以得到保护,但是,这需要使用SSL/TLS协议。
在序列化和反序列化对象时需要进行特殊处理的类可以实现有以下签名的方法[API 2006]:
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
如果一个Serializable类没有重写writeObject()方法的实现,这个对象会使用默认的方法来完成序列化,它会序列化所有的public、protected、package-private,以及private字段,除了transient字段。同样,当Serializable类没有重写readObject()方法的实现时,这个对象会反序列化所有的public、protected,以及private字段,除了那些transient字段。这个问题会在规则SER01-J中深入探讨。
可以在不同线程之间共享的内存称为共享内存(shared memory)或内存堆(heap memory)。本节使用变量(variable)这个名词来代表字段和数组元素[JLS2005]。在不同的线程中共享的变量称为共享变量。所有的实例字段、静态字段以及数组元素作为共享变量存储在共享内存中。局部变量、形式方法参数以及异常例程参数是从来不能在线程之间共享的,不会受到内存模型的影响。
在现代多处理器共享内存的架构下,每个处理器有一个或多个层次的缓存,会定期地与主存储器进行协同,如下图所示:
之所以开放对共享变量的数据写入会带来问题,是因为在共享变量中的数值是会被缓存的,而且把这些数值写入主存会有延迟。然后,其他的线程可能会读取这个变量的过时的数值。
更多的顾虑不仅在于并行执行的代码通常是交错的,同时也在于编译器或运行时系统会对执行语句进行重新排序来优化性能。这会导致执行次序很难从源代码中得到验证。不能记录可能的重排序,这是一个常见的数据竞态的来源。
举例来说,a和b是两个全局(共享)变量或实例域,而r1和r2是两个局部变量,r1和r2是不能被其他的线程读取的。在初始化状态下,令a=0,b=0。
线程1 | 线程2 |
---|---|
a = 10; | b = 20; |
r1 = b; | r2 = a; |
在线程1中,完成两个赋值:a=10和r1=b,这两个赋值是不相关的,所以编译器在编译的时候,运行时系统可以任意安排它们的执行次序。这两个赋值在线程2中也有可能是任意安排次序的。尽管可能看起来难以理解,但是Java的内存模型允许读取一个刚刚写入的数值,而这次写入显然与执行的次序有关。
以下是在实际赋值时可能的执行次序:
执行次序(时间) | 线程号 | 赋值操作 | 赋值 | 备注 |
---|---|---|---|---|
1 | t1 | a = 10; | 10 | |
2 | t2 | b = 20; | 20 | |
3 | t1 | r1 = b; | 0 | 读入b的初始值0 |
4 | t2 | r2 = a; | 0 | 读入a的初始值0 |
在这个次序中,r1和r2分别读取变量b和a的初始值,但它们实际上希望得到的是更新过的值:20和10,而以下是实际赋值时另一种可能的执行次序:
执行次序(时间) | 线程号 | 赋值操作 | 赋值 | 备注 |
---|---|---|---|---|
1 | t1 | r1 = b; | 20 | 读取后来写入的值(在第4步)20 |
2 | t2 | r2 = a; | 10 | 读取后来写入的值(在第3步)10 |
3 | t1 | a = 10; | 10 | |
4 | t2 | b = 20; | 20 |
在这个次序中,r1和r2读取b和a的值,b和a的值分别是在步骤4和步骤3赋予的,甚至是在对应的这些步骤被执行之前的那些语句赋予的。
如果能够明确代码可能的执行次序,那么对于代码的正确性,会有更大的把握。
当语句在一个线程中依次执行的时候,由于存在缓存,会使最新的数值没有在主存中体现。
《Java语言规范》(Java Language Specification JLS)中定义了Java内存模型(Java Memory Model, JMM),它为Java开发人员提供了一定程度的保障。JMM在定义的行为包括变量的读写、锁定和解锁的监视、线程的开始和会合。JMM对在程序中的所有动作定义了一种称为happens-before的部分次序化动作。它能保证一个线程执行时动作B可以看到动作A的执行结果,例如,可以说A和B的关系是一种happens-before的关系,A在B之前发生。
根据JSL17.4.5节对“Happens-before”的描述:
- 对监视器的解锁需要happens-before每一个接下来的对监视器的锁定。
- 对一个volatile域的写入需要happens-before每一个接下来的对该域的读取。
- 对一个线程的Thread.start()调用需要happens-before对这个启动线程的任何操作。
- 对一个线程的所有操作,需要happens-before从该线程Thread.join()引起的其他任何线程的正常返回。
- 任何对象的默认初始化需要happens-before该程序的任何其他操作(除了初始化的写入操作之外)。
- 一个线程对另一个线程的中断需要happens-before被中断线程检测到该中断。
- 一个对象的构造方法的结束需要happens-before这个对象的销毁器的开始。
在两个操作不存在happens-before关系的时候,JVM可以对它们的执行重新排序。当一个变量被至少一个线程写入,并且被至少一个线程读取的时候,如果这些读写不存在happens-before关系,数据的竞态会出现。正确同步的程序是不会出现数据的竞态的。JMM可以通过同步程序来保证其次序是一致的。次序一致是指任何执行结果都是一样的,比如当所有的线程按照任何特定的顺序对一个共享数据执行读写,这个序列中对每个线程的操作都是程序指定的顺序[Tanenbaum 2003]时,它们的执行结果都是同样的。换句话说:
- 每个线程都执行读和写操作,并把这些操作按照线程执行的次序进行排列(线程顺序)。
- 以某种方式安排这些操作,使它们在执行次序上是happens-before关系。
- 读操作必须返回最新写入的数据,在整个程序执行次序中,可以保证序列化的一致性。
- 这意味着任何线程都可以看到同样的对共享变量进行访问的次序。
如果程序的次序被遵从并且所有数据读取符合内存模型,那么对于实际的指令执行和内存读写次序来说,会有所不同。这使得开发人员可以理解他们编写的程序的语义,并且允许编译器开发者和虚拟机的实现有不同的优化方式[JPL 2006]。
这一系列并发原语可以帮助开发人员对多线程程序的语义有所理解。
如果声明一个共享变量为volatile,那么可以保证它的可见性并且限制对访问它的操作进行重新排序。比如递增该变量的时候,不能保证volatile访问这个操作组合的原子性。因而,当必须保证操作组合的原子性时,是不能够使用volatile的(可以参考CON02-J规则获取详细信息)。
声明一个volatile变量即建立一种happens-before关系,例如,当一个线程写入一个volatile变量后,随后读取该变量的线程总会看到这个写入线程。写入这个volatile变量之前执行的语句happens-before任何对这个volatile变量的读操作。
考虑两个线程执行以下语句的情况,如下图所示:
在这个例子中,语句3写入一个volatile变量,语句4(在线程2中)读取该volatile变量。这个读取可以得到语句3最新写入的数据(对同一个变量v)。
对volatile的读与写操作不能重新排序,要么依次读写它,要么使用非volatile变量。当线程2读取volatile变量的时候,它会得到所有的写入结果,而这些写入结果是在线程1写入该volatile变量之前发生的。由于需要相对有力的对volatile特性的保证,其性能开销几乎和同步是一样的。
在前面的例子中,并不能保证同一个程序中的两条语句按照它们在程序中出现的次序执行。如果在这两个语句之间不存在happens-before关系的话,它们可能会被编译器以任意的次序进行重排。
下表总结了所有对volatile和非volatile变量进行重排序的可能性。其中的load/store操作可以和read/write操作对应[Lea 2008]。
注意,如果在变量上加上volatile关键词的话,它会保证可见性和执行次序。也就是说,它仅应该适用于原始字段和对象引用。如果实际成员是一个对象引用本身,可以保证这一点;如果是一个对象,而它的引用是一个volatile类型,那么这一点是不能保证的。因而,声明一个对象引用是volatile不足以保证对所引用的成员的改变是可见的。这样的话,一个线程可能不能读取另一个线程对这个引用对象成员字段最新写入。此外,当一个引用是可变的并且不是线程安全,那么其他线程可能只能看到一个对象只是部分地被创建,或者这个对象会处在一个(临时)不一致的状态当中[Goetz 2007]。然而,当一个引用是不可变的时候,声明该引用是volatile已经足够保证该引用成员的可见性。
一个正确进行同步的程序,可以保证执行的一致次序,并且不会产生数据竞态情况。下面的例子通过使用一个非volatile变量x和一个volatile变量y来说明如何没有正确同步。
线程1 | 线程2 |
---|---|
x = 1; | r1 = y; |
y = 2; | r2 = x; |
在这个例子中,有两种序列上一致的执行次序。
执行次序(时间) | 线程号 | 语句 | 备注 |
---|---|---|---|
1 | t1 | x = 1 | 写入非volatile变量 |
2 | t1 | y = 2 | 写入volatile变量 |
3 | t2 | r1 = y | 读取volatile变量 |
4 | t2 | r2 = x | 读取非volatile变量 |
以及
执行次序(时间) | 线程号 | 语句 | 备注 |
---|---|---|---|
1 | t2 | r1 = y | 读取volatile变量 |
2 | t2 | r2 = x | 读取非volatile变量 |
3 | t1 | x = 1 | 写入非volatile变量 |
4 | t1 | y = 2 | 写入volatile变量 |
在第一种情况下,步骤1和步骤2总是发生在步骤3和步骤4之前,这是一种happens-before关系。然而,第二种顺序一致的执行情况在任何步骤之间都缺乏happens-before关系。因此,这个例子中存在数据的竞态。
正确的可见性可以保证多个线程在访问共享数据时,得到相互之间的结果,但是不能在每一个线程读写数据的时候、建立起次序。正确的同步可以实现正确的可见性,并且可以保证线程写以特定的次序访问数据。例如,从下面的代码可以看出,线程1的所有操作都在线程2的所有操作之前执行,这时保证了一个一致的执行次序。
class Assign {
public synchronized void doSomething() {
// If in Thread 1, perform Thread 1 actions
x = 1;
y = 2;
// If in Thread 2, perform Thread 2 actions
r1 = y;
r2 = x;
}
}
使用同步的时候,不需要声明变量y是volatile的。同步涉及取得锁、执行操作、释放锁等过程。在前面的例子中,doSomething()方法需要获得一个类对象Assign的内部锁。这个例子同样可以使用块同步来实现。
class Assign {
public void doSomething() {
synchronized (this) {
// If in Thread 1, perform Thread 1 actions
x = 1;
y = 2;
// If in Thread 2, perform Thread 2 actions
r1 = y;
r2 = x;
}
}
}
这两个例子都使用了内部锁。对象的内部锁也可以用作监视器。释放一个对象的内部锁总是在下一次获得该对象的内部锁之前发生,这是一种happens-before关系。
原子类 volatile变量对保证可见性很有用。但是,它不能保证原子性。同步可以保证原子性,但它们会产生上下文切换的额外开销,并且经常会出现锁竞争。java.util.concurrent.atomic包中的原子类提供了一种机制,可以在同时需要保证原子性时,在大多数环境下减少锁竞争。根据Goetz及其同事的研究,“在低和中等的竞争时,原子操作提供更好的可扩展性;在高竞争时,锁操作可以避免高竞争”[Goetz 2006a]。
atomic类开放了通用的功能接口,因而开发人员可以充分利用现代处理器的compare-swap指令提供的执行效率。例如,AtomicInteger.incrementAndGet()方法支持对一个变量的原子加,其他高层方法如 java.util.concurrent.atomic.Atomic*.compareAndSet()(其中Atomic可以是Integer、Long或Boolean类型)为开发者提供了一个简单的抽象接口,通过这个接口,同样可以方便地使用处理器级别的指令。
在java.util.concurrent辅助包中,倾向于使用volatile变量,而不是使用传统的诸如synchronized同步方法,如synchronized关键字和volatile变量,因为这些辅助包抽象了底层的细节,提供了一个更简单且更少错误的API,这样更容易扩展,并在一定的策略下可以加强其作用。
执行器框架 通过使用执行器框架,java.util.concurrent包提供了任务并发执行的机制。这些任务可以是由实现了Runnable或者Callable接口的类来封装的一个逻辑执行单元。这个执行器框架将任务提交与底层的任务管理和调度细节分离开。它还提供了线程池机制,通过这个线程池,在系统需要同时处理超过其处理能力的请求时,系统不致崩溃。
执行器框架的核心接口是Executor接口,它扩展自ExecutorService接口。ExecutorService接口提供了线程池的终止机制,并且可以获得任务的返回值。ExecutorService还被ScheduledExecutorService扩展了,这个ScheduledExecutorService接口提供了可以让运行中的任务周期性或延时执行。Executor类提供了若干工厂和辅助方法,通过这些方法可以提供Executor、ExecutorService和其他接口需要的通用配置。例如,Executors.newFixedThreadPool()方法可以返回确定大小的线程池,为在线程池中并发执行的任务数目确定一个上限,并在线程池满载时,维护一个任务队列。线程池的基本(实际)实现是由ThreadPoolExecutor类来完成的。这个类可以被实例化来定制任务执行策略。
显式锁 java.util.concurrent包中的ReentrantLock类提供了隐含锁所没有的功能特性。举例来说,调用ReentrantLock.tryLock()方法会立即返回持有锁的另一个线程对象。在JMM的定义中,获取或释放一个ReentrantLock对象与获取或释放一个隐含锁是一样的。
根据最低权限原则,系统的每一个程序和用户只应赋予能够完成它们操作所需要的最低权限[Saltzer 1974,Saltzer 1975] 。“构建安全的网站”(Build Security In Website)[DHS 2006]这篇文章补充说明了这个原则。以最低权限运行可以降低因为代码中的安全漏洞而带来的安全问题的严重性。
体现最低权限原则的规则如下所示:
- ENV03-J 不要赋予危险的权限组合
- SEC00-J 不要允许特权代码越过受信边界泄露敏感信息
- SEC01-J 不要在特权代码块中使用污染过的变量
定义权限的所谓安全策略必须有严格的控制。当Java程序有安全管理器时,通过默认的安全策略文件进行权限控制;然而,Java灵活的安全模型允许用户赋予应用更多的权限,这可以通过自定义安全策略来完成。
Java代码若想提高权限的话,需要使用代码签名。很多安全策略允许具备签名的代码以提高后的权限来执行。只有那些需要提升权限的代码需要签名,对其他代码来说,是不需要签名的(请参考规则ENV00-J)。
在同一个JAR文件内,已签名的代码会与未签名的类共存。建议将所有的特权代码打成一个包(详情请参考规则ENV01-J)。此外,根据安全策略,可以基于代码或签名器为代码赋予特定权限。
特权操作权限应该只提供给那些最少的需要特权的代码。Java的AccessController机制允许只有需要的代码可以获得权限提升。当一个类需要主张其权限时,在doPrivileged()代码块中,执行特权代码即可。AccessController机制需要与安全策略一起发挥作用。由于用户可能不清楚安全模型的相关细节,所以并不能根据它们的需求正确配置安全策略,在doPrivileged()代码块中出现的特权代码必须是最少的,从而避免出现安全漏洞。
SecurityManager是Java中定义安全策略的类。当一个程序在没有安装安全管理器的环境中运行时,它是不受限制的,它可以使用任何Java API提供的类和方法。当使用安全管理器时,它会明确哪些不安全和敏感的操作是允许的。任何违反安全策略的操作都会导致抛出SecurityException异常,代码可以向安全管理器查询某些动作是否允许。同时,安全管理器可以控制受信的Java API能够执行的功能。当非受信的代码不允许读取系统类的时候,应该赋予这些类最低的权限,以防止特定包中的那些受信类被它们使用。而accessClassInPackage权限只提供那些所需要的功能。
某一类应用中存在着一些预先设定好的安全管理器。applet安全管理器就能够管理所有的Java applet。它拒绝除了赋予重要权限的所有applet,以防止不经意的系统修改、信息泄露以及用户模拟。
安全管理器不仅仅只限于对客户端应用的保护。对于Web服务器,如Tomcat和WebSphere,可以将安全管理器用来隔离servlets和JSP(Java Server Page)代码中出现的恶意木马,以及保护敏感的系统资源,使其不能被随便访问。
对于在命令行状态下运行的Java应用,一个自定义的安全管理器会被设置一个特殊的标识。同样,也可以通过编程式的方法安装一个安全管理器。这样就可以建立一个默认的沙箱,通过安全策略来允许或者拒绝那些敏感操作。
在Java 2平台之前,安全管理器是一个抽象类。在Java 2平台之后,由于不作为抽象类出现,所以应用时不需要显式重写它的方法。如果编程式地创建安全管理器,代码需要有运行态的权限来运行createSecurityManager方法,从而实例化安全管理器并且通过运行setSecurityManager来安装SecurityManager。当已经安装安全管理器时,将会检查这些权限。当存在一个全局默认的安全管理器时,例如在虚拟宿主环境,或者在一个单独的宿主环境中,需要用一个定制的安全管理器来代替默认的安全管理器,从而拒绝以前那些能够通过的权限的情况。
安全管理器和AccessController类紧密相关。前者是一个访问控制中枢,而后者是访问控制算法的实际实现。安全管理器支持以下几种情况:
- 提供后向兼容性:历史遗留的代码通常包含许多定制化的安全管理器的实现,因为最开始安全管理器是被设计为抽象的。
- 设定自定义的策略:设计一个安全管理器的子类,以设定自定义的安全策略(例如,多层次、粗略或者细粒度的)。
在考虑实现和使用自定义的安全管理器的时候,对应于默认的安全管理器,在Java Security Architecture Specification[SecuritySpec 2008]中提到:
定制一个安全管理器(通过设计它的子类)应当是最后的手段,并且需要特别谨慎,我们鼓励在应用代码中使用AccessController。此外,一个定制的安全管理器,比如它会在调用标准的安全检查之前做当天的时间检查,它可以而且应该适当的利用AccessControllerwhenever所提供的算法。
许多Java SE API在进行敏感操作之前会通过调用安全管理器来进行默认的检查。例如,在调用者不具备读文件权限的时候,java.io.FileInputStream的构造函数会抛出SecurityException异常。因为SecurityException是RuntimeException的子类,一些API方法的声明并不需要声明它们抛出RuntimeException异常,但是一些API方法不能这样。例如,在java.io.FileReader中就没有声明它会抛出SecurityException异常。除非在API方法文档里特别指明,否则应该避免依赖对安全管理器是否存在进行的检查。
java.lang.ClassLoader类及其子类可以让新代码动态地加载到JVM中。每一个类都提供了装载它的ClassLoader的链接。此外,每一个类装载器类都有一个装载它的父类装载器,类装载器的顶端称为根类装载器。因为ClassLoader被设计成抽象的,所以它不能被实例化。所有的继承自SecureClassLoader的类装载器都继承自ClassLoader。SecureClassLoader对其方法进行的权限安全检查,在它的子类中也会同样进行。SecureClassLoader定义了getPermissions()方法,通过这个方法可以指明被类装载器装载的类所具有的权限。通过这种方式提供的保护机制,可以限制非受信的代码装载额外的类。
遗憾的是,被不同的类装载器载入的类总是不同的。为了那些非受信代码的安全性,package-private(即默认的)访问权限可以认为和private访问权限是一样的。
尽管作为一种相对安全的语言,Java语言及其类库还是在很大程度上存在着一些编程问题,从而造成系统安全漏洞。如果假设Java本身提供的功能特性可以减少一般的软件问题,并足够Java程序安全得不需要进行进一步检测,那么我们就大错特错了。因为任何在软件实现中出现的缺陷都会产生严重的安全影响,绷紧安全性这根弦是非常关键的,这样,当我们进行系统开发和部署时,就可以避免出现软件的安全漏洞问题。
为了减少因为编程错误所带来的安全漏洞的可能性,Java开发人员应当遵循本编码标准中的安全编码规则,并遵循其他适用的安全编码指南。
Input Validation and Data Sanitization
Prevent SQL injection
public void doPrivilegedAction(String username, char[] password) throws SQLException
{
Connection connection = getConnection();
if (connection == null)
{
// Handle error
}
try
{
String pwd = hashPassword(password);
String sqlString = "SELECT * FROM db_user WHERE username = '"
+ username +
"' AND password = '" + pwd + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sqlString);
if (!rs.next())
{
throw new SecurityException("User name or password incorrect");
}
// Authenticated; proceed
}
finally
{
try
{
connection.close();
}
catch (SQLException x)
{
// Forward to handler
}
}
}
问题描述:
这段代码存在SQL注入漏洞,注入点是在拼接SQL语句中未经数据净化的username参数。例如,允许攻击者注入validuser' OR '1'='1
。
解决方法:
使用PrepareStatement代替Statement。SQL语句不再使用字符串拼接方式,而是使用?占位符。通过使用PrepareStatement类的set*()方法,可以进行强类型检查。
public void doPrivilegedAction(String username, char[] password) throws SQLException
{
Connection connection = getConnection();
if (connection == null)
{
// Handle error
}
try
{
String pwd = hashPassword(password);
// Validate username length
if (username.length() > 8)
{
// Handle error
}
String sqlString = "SELECT * FROM db_user WHERE username=? AND password=?";
PreparedStatement stmt = connection.prepareStatement(sqlString);
stmt.setString(1, username);
stmt.setString(2, pwd);
ResultSet rs = stmt.executeQuery();
if (!rs.next())
{
throw new SecurityException("User name or password incorrect");
}
// Authenticated; proceed
}
finally
{
try
{
connection.close();
}
catch (SQLException x)
{
// Forward to handler
}
}
}
这段代码还验证了username参数的长度,防止如果提交一个长用户名的时候可能会出现的攻击。
Exclude unsanitized user input from format strings
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
public static void main(String[] args)
{
// args[0] should contain the credit card expiration date
// but might contain %1$tm, %1$te or %1$tY format specifiers
System.out.format(args[0] + " did not match! HINT: It was issued on %1$terd of some month", c);
}
}
问题描述:
将信用卡的失效日期作为输入参数并直接拼接在格式字符串中,存在信息泄露的问题。攻击者通过在输入字符串中包含%1$tm、%1$te或%1$tY
格式化控制符,就可以获得信用卡的失效日期信息。
解决方法:
排除用户输入字符串中的格式化控制符。
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
public static void main(String[] args)
{
// args[0] is the credit card expiration date
// Perform comparison with c,
// if it doesn't match, print the following line
System.out.format("%s did not match! HINT: It was issued on %terd of some month", args[0], c);
}
}
在这个合规方案中,虽然arg[0]还是可以包含一个或多个格式控制符,但会被忽略。
Sanitize untrusted data passed to the Runtime.exec() method
class DirList
{
public static void main(String[] args) throws Exception
{
String dir = System.getProperty("dir");
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("cmd.exe /C dir " + dir);
int result = proc.waitFor();
if (result != 0)
{
System.out.println("process error: " + result);
}
InputStream in = (result == 0) ? proc.getInputStream() : proc.getErrorStream();
int c;
while ((c = in.read()) != -1)
{
System.out.print((char) c);
}
}
}
这段代码通过Runtime.exec()方法调用Windows的dir命令实现在Windows操作系统上使用dir命令列出目录列表。
问题描述:
由于Runtime.exec()方法接受源于运行环境的未经净化的输入数据,这些代码会引起命令注入攻击。
攻击者可以通过以下命令利用该程序:
java -Ddir='dummy & echo bad' Java
该命令实际上执行的是两条命令:
cmd.exe /C dir dummy & echo bad
第一条命令会列出并不存在的dummy文件夹,并且在控制台上输出“bad”。
解决方法:
书中列出了三种方案。前两种方案是净化非受信的用户输入和限定用户选择,基本都属于白名单的方式,缺陷明显。第三种方案是避免使用Runtime.exec()。
当通过执行系统命令可以完成的任务可以用其他方式完成时,最好避免用执行系统该命令的方式。
在这个例子中,可以使用File.list()来提供目录列表,从而消除了命令或参数注入攻击发生的可能性。
import java.io.File;
class DirList
{
public static void main(String[] args) throws Exception
{
File dir = new File(System.getProperty("dir"));
if (!dir.isDirectory())
{
System.out.println("Not a directory");
}
else
{
for (String file : dir.list())
{
System.out.println(file);
}
}
}
}
Declarations and Initialization
Expressions
Do not ignore values returned by methods
public void deleteFile()
{
File someFile = new File("someFileName.txt");
// Do something with someFile
someFile.delete();
}
问题描述:
这段代码想要删除一个文件,但没有确认这个删除动作是否成功。
解决方法:
对delete()方法返回的boolean变量进行检查,并处理可能产生的错误。
public void deleteFile()
{
File someFile = new File("someFileName.txt");
// Do something with someFile
if (!someFile.delete())
{
// Handle failure to delete the file
}
}
public class Replace
{
public static void main(String[] args)
{
String original = "insecure";
original.replace('i', '9');
System.out.println(original);
}
}
问题描述:
这段代码忽略了String.replace()方法的返回值,并没有更新初始的字符串值。String.replace()方法不会修改这个String的状态,因为String对象是不可变的,但是它会返回一个新的字符串引用,而这个新的字符串包含了修改过的数据。
解决方法:
通过String.replace()方法的返回值正确更新original String对象。
public class Replace
{
public static void main(String[] args)
{
String original = "insecure";
original = original.replace('i', '9');
System.out.println(original);
}
}
Do not use the Object.equals() method to compare two arrays
public void arrayEqualsExample()
{
int[] arr1 = new int[20];
int[] arr2 = new int[20];
System.out.println(arr1.equals(arr2));
}
问题描述:
数组没有重写Object.equals()方法,equals()方法比较的是数组引用而不是数组的内容值。
解决方法:
使用Arrays.equals()方法比较两个数组的内容。
public void arrayEqualsExample()
{
int[] arr1 = new int[20];
int[] arr2 = new int[20];
System.out.println(Arrays.equals(arr1, arr2));
}
Do not use the equality operators when comparing values of boxed primitives
public class Wrapper
{
public static void main(String[] args)
{
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 1000;
Integer i4 = 1000;
System.out.println(i1 == i2);
System.out.println(i1 != i2);
System.out.println(i3 == i4);
System.out.println(i3 != i4);
}
}
运行结果:
true
false
false
true
问题描述:
这段代码试图使用==操作符比较两个Integer对象的值。然而,==操作比较的是对象引用,而不是对象的值。另外,Integer类缓存了介于-128~127的整型数值,可以使用==操作符比较,比较不在缓存范围内的其他值将会返回false。
解决方法:
使用equals()方法替代==操作符比较对象的值。
public class Wrapper
{
public static void main(String[] args)
{
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 1000;
Integer i4 = 1000;
System.out.println(i1.equals(i2));
System.out.println(!i1.equals(i2));
System.out.println(i3.equals(i4));
System.out.println(!i3.equals(i4));
}
}
运行结果:
true
false
true
false
Numeric Types and Operations
Characters and Strings
Object Orientation
Preserve dependencies in subclasses when changing superclasses
class Account
{
// Maintains all banking-related data such as account balance
private double balance = 100;
boolean withdraw(double amount)
{
if ((balance - amount) >= 0)
{
balance -= amount;
System.out.println("Withdrawal successful. The balance is : " + balance);
return true;
}
return false;
}
}
public class BankAccount extends Account
{
// Subclass handles authentication
@Override
boolean withdraw(double amount)
{
if (!securityCheck())
{
throw new IllegalAccessException();
}
return super.withdraw(amount);
}
private final boolean securityCheck()
{
// Check that account management may proceed
}
}
public class Client
{
public static void main(String[] args)
{
Account account = new BankAccount();
// Enforce security manager check
boolean result = account.withdraw(200.0);
System.out.println("Withdrawal successful? " + result);
}
}
Account类增加了overdraft()方法,BankAccount类不变,Client类增加对overdraft()方法的调用。
class Account
{
// Maintains all banking-related data such as account balance
private double balance = 100;
boolean overdraft()
{
balance += 300; // Add 300 in case there is an overdraft
System.out.println("Added back-up amount. The balance is :" + balance);
return true;
}
// Other Account methods
}
public class BankAccount extends Account
{
// Unchanged
}
public class Client
{
public static void main(String[] args)
{
Account account = new BankAccount();
// Enforce security manager check
boolean result = account.withdraw(200.0);
if (!result)
{
result = account.overdraft();
}
System.out.println("Withdrawal successful? " + result);
}
}
问题描述:
在这段代码中,Account类用来保存银行相关信息,但没有设置内在的安全机制。安全机制交由子类BankAccount代理。Client必须使用BankAccount子类。
后来,Account类增加了overdraft()方法,但BankAccount类不变,Client可以通过调用BankAccount对象的overdraft()方法绕过安全机制。
解决方法:
在BankAccount子类中Override overdraft()方法,并使得overdraft()方法失效,防止该方法被误用。
public class BankAccount extends Account
{
// ...
@Override boolean overdraft()
{
throw new IllegalAccessException();
}
}
Do not return references to private mutable class members
class MutableClass
{
private Date d;
public MutableClass()
{
d = new Date();
}
public Date getDate()
{
return d;
}
}
问题描述:
非受信的调用者能够通过调用getDate()方法获得私有的Date对象,从而改变MutableClass的内部数据。
返回类的内部可变成员的引用,会破坏应用的安全性。因为它既破坏了类的封装性,又使得类的内部状态可能遭到破坏(不管是无意的还是恶意的)。因此,应禁止返回内部可变类的引用。
解决方法:
返回Date对象的克隆副本。
public Date getDate()
{
return (Date)d.clone();
}
class MutableClass
{
private Date[] dates;
public MutableClass()
{
dates = new Date[20];
for (int i = 0; i < dates.length; i++)
{
dates[i] = new Date();
}
}
public Date[] getDates()
{
return (Date[])dates.clone();
}
}
问题描述:
getDates()方法返回一个Date对象的数组,而数组包含的Date对象的引用是可变的。对数组进行浅复制是不够的,因为攻击者可以修改数组中的Date对象。
解决方法:
返回深度复制后的Date对象数组。
class MutableClass
{
private Date[] dates;
public MutableClass()
{
dates = new Date[20];
for (int i = 0; i < dates.length; i++)
{
dates[i] = new Date();
}
}
public Date[] getDates()
{
Date[] copiedDates = new Date[dates.length];
for (int i = 0; i < dates.length; i++)
{
copiedDates = (Date) date[i].clone();
}
return copiedDates;
}
}
Defensively copy mutable inputs and mutable internal components
public final class MutableDemo
{
// java.net.HttpCookie is mutable
public void useMutableInput(HttpCookie cookie)
{
if (cookie == null)
{
throw new NullPointerException();
}
// Check whether cookie has expired
if (cookie.hasExpired())
{
// Cookie is no longer valid; handle condition by throwing an exception
}
doLogic(cookie);
}
}
问题描述:
这段代码包含一个TOCTOU漏洞。TOCTOU(Time-of-CHeck Time-of-Use)是指,一个属性值通过了校验和安全检查,但在使用前被修改了。cookie是一个可变输入,攻击者可以让它在初始检查(调用hasExpired())和实际使用(调用doLogic())之间过期。
解决方法:
复制可变输入,所有操作都基于这个副本进行。这样的话,攻击者对可变输入的改变不会影响副本。
public final class MutableDemo
{
// java.net.HttpCookie is mutable
public void useMutableInput(HttpCookie cookie)
{
if (cookie == null)
{
throw new NullPointerException();
}
// Create copy
cookie = (HttpCookie)cookie.clone();
// Check whether cookie has expired
if (cookie.hasExpired())
{
// Cookie is no longer valid; handle condition by throwing an exception
}
doLogic(cookie);
}
}
Do not expose private members of an outer class from within a nested class
class Coordinates
{
private int x;
private int y;
public class Point
{
public void getPoint()
{
System.out.println("(" + x + "," + y + ")");
}
}
}
class AnotherClass
{
public static void main(String[] args)
{
Coordinates c = new Coordinates();
Coordinates.Point p = c.new Point();
p.getPoint();
}
}
问题描述:
在这段代码中,内部类Point将外部类Coordinates的私有成员变量(x, y)坐标通过getPoint()方法暴露出去了,因此属于同一个包的AnotherClass可以访问到Coordinates的私有成员变量(x, y)。
解决方法:
使用private访问描述符隐藏内部类及其所包含的方法和构造函数。
class Coordinates
{
private int x;
private int y;
private class Point
{
private void getPoint()
{
System.out.println("(" + x + "," + y + ")");
}
}
}
class AnotherClass
{
public static void main(String[] args)
{
Coordinates c = new Coordinates();
Coordinates.Point p = c.new Point(); // Fails to compile
p.getPoint();
}
}
Compare classes and not class names
// Determine whether objects x and y have the same class name
if (x.getClass().getName().equals(y.getClass().getName()))
{
// Objects have the same class
}
问题描述:
在这段代码中,将对象x和y的类名进行比较。比较类的全名是不够的,因为不同的类装载器会装载具有相同全名的不同的类到一个JVM中。
在JVM中,如果两个类被同一个类装载器装载,并且有相同的全名,那么这两个类被认为是相同的类,并且因此有相同的类型。
解决方法:
比较对象x和y的类对象。
// Determine whether objects x and y have the same class
if (x.getClass() == y.getClass())
{
// Objects have the same class
}
方法
Methods that perform a security check must be declared private or final
public void readSensitiveFile()
{
try
{
SecurityManager sm = System.getSecurityManager();
if (sm != null)
{
// Check for permission to read file
sm.checkRead("/temp/tempFile");
}
// Access the file
}
catch (SecurityException se)
{
// Log exception
}
}
问题描述:
这段代码允许子类覆写readSensitiveFile(),从而绕过安全检查。
恶意的子类可以覆写进行安全检查的非final成员方法,从而绕过这些检查。这样的方法必须被声明为private或者final以防止被覆写。
解决方法:
将readSensitiveFile()方法声明为final或private的。
public final void readSensitiveFile()
{
try
{
SecurityManager sm = System.getSecurityManager();
if (sm != null)
{
// Check for permission to read file
sm.checkRead("/temp/tempFile");
}
// Access the file
}
catch (SecurityException se)
{
// Log exception
}
}
private void readSensitiveFile()
{
try
{
SecurityManager sm = System.getSecurityManager();
if (sm != null)
{
// Check for permission to read file
sm.checkRead("/temp/tempFile");
}
// Access the file
}
catch (SecurityException se)
{
// Log exception
}
}
Do not increase the accessibility of overridden or hidden methods
class Super
{
protected void doLogic()
{
System.out.println("Super invoked");
}
}
public class Sub extends Super
{
public void doLogic()
{
System.out.println("Sub invoked");
// Do sensitive operations
}
}
问题描述:
在这段代码中,恶意子类Sub覆写了父类的doLogic()方法,并增加了被覆写方法的可访问性。任何Sub的使用者都可以调用doLogic()方法。
增加被覆写方法和被隐藏方法的可访问性会导致恶意子类对这些受限制的方法拥有比预计更大的访问权限。因此,程序只能在必要的时候对方法进行覆写,并且必须尽可能地将方法声明为final以避免恶意扩展。如果不能将方法声明为final的,程序必须防止增加被覆写方法的可访问性。
解决方法:
将doLogic()方法声明为final,避免恶意覆写。
class Super
{
protected final void doLogic() // Declare as final
{
System.out.println("Super invoked");
}
}
Preserve the equality contract when overriding the equals() method
在JLS中描述了通常对equals()的使用契约,包含5点:
- 满足自反性(reflexive):对于任意引用x,x.equals(x)必须返回true。
- 满足对称性(symmetric):对于任意引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)才返回true。
- 满足传递性(transitive):对于任意引用x、y和z,如果x.equals(y)返回true且y.equals(z)返回true,则x.equals(z)返回true。
- 满足一致性(consistent): 只要用在equals()中比较的对象没有被修改,对于任意引用x和y,多次调用x.equals(y)总是 返回true或者总是返回false。
- 对任意非空引用x,x.equals(null)必须返回false
当覆写equals()方法的时候,绝对不要违反这些规则。
public final class CaseInsensitiveString
{
private String s;
public CaseInsensitiveString(String s)
{
if (s == null)
{
throw new NullPointerException();
}
this.s = s;
}
public boolean equals(Object o)
{
if (o instanceof CaseInsensitiveString)
{
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
}
if (o instanceof String)
{
return s.equalsIgnoreCase((String)o);
}
return false;
}
// Comply with MET09-J
public int hashCode() {/* ... */}
public static void main(String[] args)
{
CaseInsensitiveString cis = new CaseInsensitiveString("Java");
String s = "java";
System.out.println(cis.equals(s));
System.out.println(s.equals(cis));
}
}
运行结果:
true
false
问题描述:
cis.equals(s)返回true,但s.equals(cis)返回false,违反了对称性。CaseInsensitiveString类知道普通的字符串,但String类对大小写不敏感的字符串没有任何概念。因此,方法CaseInsensitiveString.equals()不应该尝试与String类的对象进行相互操作。
解决方法:
方法CaseInsensitiveString.equals()只对方法CaseInsensitiveString类的实例进行操作,从而保持对称性。
public final class CaseInsensitiveString
{
private String s;
public CaseInsensitiveString(String s)
{
if (s == null)
{
throw new NullPointerException();
}
this.s = s;
}
public boolean equals(Object o)
{
return o instanceof CaseInsensitiveString && ((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
}
// Comply with MET09-J
public int hashCode() {/* ... */}
public static void main(String[] args)
{
CaseInsensitiveString cis = new CaseInsensitiveString("Java");
String s = "java";
System.out.println(cis.equals(s));
System.out.println(s.equals(cis));
}
}
运行结果:
false
false
public class Card
{
private final int number;
public Card(int number)
{
this.number = number;
}
public boolean equals(Object o)
{
if (!(o instanceof Card))
{
return false;
}
Card c = (Card)o;
return c.number == number;
}
public int hashCode() {/* ... */}
}
class XCard extends Card
{
private String type;
public XCard(int number, String type)
{
super(number);
this.type = type;
}
public boolean equals(Object o)
{
if (!(o instanceof Card))
{
return false;
}
// Normal Card, do not compare type
if (!(o instanceof XCard))
{
return o.equals(this);
}
// It is an XCard, compare type as well
XCard xc = (XCard)o;
return super.equals(o) && xc.type == type;
}
public int hashCode() {/* ... */}
public static void main(String[] args)
{
XCard p1 = new XCard(1, "type1");
Card p2 = new Card(1);
XCard p3 = new XCard(1, "type2");
System.out.println(p1.equals(p2));
System.out.println(p2.equals(p3));
System.out.println(p1.equals(p3));
}
}
运行结果:
true
true
false
问题描述:
p1.equals(p2)返回true,p2.equals(p3)也返回true,但p1.equals(p3)返回false,违反了传递性。Card类不知道XCard类的实现,从而不能推断p2和p3的字段type有着不同的数值。
解决方法:
在这种情况下,在子类中增加一个数值或成员来扩展父类并同时保证equals()的契约性是不可能的。这时使用组合而不是继承可以达到预期的效果。在XCard类中增加一个private的card成员,并提供public的viewCard()方法。
class XCard
{
private String type;
private Card card; // Composition
public XCard(int number, String type)
{
card = new Card(number);
this.type = type;
}
public Card viewCard()
{
return card;
}
public boolean equals(Object o)
{
if (!(o instanceof XCard))
{
return false;
}
XCard cp = (XCard)o;
return cp.card.equals(card) && cp.type.equals(type);
}
public int hashCode() {/* ... */}
public static void main(String[] args)
{
XCard p1 = new XCard(1, "type1");
Card p2 = new Card(1);
XCard p3 = new XCard(1, "type2");
XCard p4 = new XCard(1, "type1");
System.out.println(p1.equals(p2));
System.out.println(p2.equals(p3));
System.out.println(p1.equals(p3));
System.out.println(p1.equals(p4));
}
}
运行结果:
false
false
false
true
Classes that define an equals() method must also define a hashCode() method
public final class CreditCard
{
private final int number;
public CreditCard(int number)
{
this.number = number;
}
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (!(o instanceof CreditCard))
{
return false;
}
CreditCard cc = (CreditCard)o;
return cc.number == number;
}
public static void main(String[] args)
{
Map<CreditCard, String> m = new HashMap<CreditCard, String>();
m.put(new CreditCard(100), "4111111111111111");
System.out.println(m.get(new CreditCard(100)));
}
}
问题描述:
这段代码用HashMap将信用卡号和字符串联系在一起,随后尝试通过信用卡的卡号获取卡号字符串。预期结果是"4111111111111111",但实际结果却是null。
造成这一错误行为的原因是CreditCard覆写了equals()方法,但没有覆写hashCode()方法。默认的hashCode()方法对不同的对象返回一个不同的数值,即便这些对象在逻辑上是相等的。这些不同的hash值会导致在哈希表的不同桶中查找对象,也就导致get()方法找不到需要的值。
覆写了Object.equals()方法的类必须同时覆写Object.hashCode()方法。java.lang.Object类要求:对任意两个使用equals()方法判断为相等的对象,对这两个对象调用它们的hashCode()方法时也必须产生相同的整数结果。
equals()方法被用于在对象实例间判断逻辑相等性。相应地,hashCode()方法也应当为所有相等的对象产生相同的值。不遵从这点通常会产生缺陷。
解决方法:
覆写equals()方法的同时覆写hashCode()方法,从而保证通过equals()方法判断为相等的两个实例拥有相同的哈希值。
public final class CreditCard
{
private final int number;
public CreditCard(int number)
{
this.number = number;
}
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (!(o instanceof CreditCard))
{
return false;
}
CreditCard cc = (CreditCard)o;
return cc.number == number;
}
public int hashCode()
{
int result = 17;
result = 31 * result + number;
return result;
}
public static void main(String[] args)
{
Map<CreditCard, String> m = new HashMap<CreditCard, String>();
m.put(new CreditCard(100), "4111111111111111");
System.out.println(m.get(new CreditCard(100)));
}
}
Exceptional Behavior
Prevent exceptions while logging data
try
{
// ...
}
catch (SecurityException se)
{
System.err.println(se);
// Recover from exception
}
问题描述:
这段代码将关键安全异常写入标准错误流。
为了记录日志,将这样的异常写到标准错误流是不合适的。首先,标准错误流可能会被耗尽或者被关闭,从而阻止了随后的异常记录。其次,标准错误流的信任级别也许不能满足在记录某些关键的安全异常或错误时不泄漏敏感信息的要求。如果在写入安全异常时发生I/O错误,catch块会抛出一个IOException,从而造成了关键的安全异常信息的丢失。最后,攻击者也会用其它无关紧要的异常伪装这个异常。
使用Console.printf()、System.out.print*()或者Throwable.printStackTrace() 输出安全相关异常同样违反了这条原则。
解决方法:
使用java.util.logging.Logger或其他合规的日志机制,例如log4j。
try
{
// ...
}
catch (SecurityException se)
{
logger.log(Level.SEVERE, se);
// Recover from exception
}
prior object state on method failure
class Dimensions
{
private int length;
private int width;
private int height;
static public final int PADDING = 2;
static public final int MAX_DIMENSION = 10;
public Dimensions(int length, int width, int height)
{
this.length = length;
this.width = width;
this.height = height;
}
protected int getVolumePackage(int weight)
{
length += PADDING;
width += PADDING;
height += PADDING;
try
{
if (length <= PADDING || width <= PADDING || height <= PADDING
|| length > MAX_DIMENSION + PADDING
|| width > MAX_DIMENSION + PADDING
|| height > MAX_DIMENSION + PADDING
|| weight <= 0 || weight > 20)
{
throw new IllegalArgumentException();
}
int volume = length * width * height;
length -= PADDING;
width -= PADDING;
height -= PADDING; // Revert
return volume;
}
catch (Throwable t)
{
MyExceptionReporter mer = new MyExceptionReporter();
mer.report(t); // Sanitize
return -1; // Non-positive error code
}
}
public static void main(String[] args)
{
Dimensions d = new Dimensions(8, 8, 8);
System.out.println(d.getVolumePackage(21));
System.out.println(d.getVolumePackage(19));
}
}
运行结果:
-1
1728 // 1728 (12x12x12) instead of 1000 (10x10x10)
问题描述:
在这段代码中,Dimensions类包含方形的3个整型属性:length、width和height。getVolumePackage()方法被设计为返回包括包装材料在内所需的总体积。包装材料在每个方向上增加了2个单位。在验证输入时排除了非正数的边长(不包括包装材料),所有维度的尺寸都要小于等于10。同时,物体的重量也作为一个参数传递给方法,它不能大于20个单位。
虽然代码会在没有异常的情况下恢复对象的最初状态,但是这些回滚操作在异常发生时并没有执行。因此,随后对getVolumePackage()的调用将会导致不正确的结果。
解决方法:
一般来说,即使发生异常情况,应该维护对象尤其是关乎安全的对象的状态的一致性。维护对象一致性的通用技术包括:
- 输入验证(例如对方法的参数进行验证)。
- 重新安排逻辑次序使得会导致异常发生的代码在对象状态被修改之前执行。
- 失败时使用回滚。
- 在对象的临时拷贝上完成需要的操作,只有当成功操作后才往源对象上提交变更 从根本上避免修改对象。
protected int getVolumePackage(int weight)
{
length += PADDING;
width += PADDING;
height += PADDING;
try
{
if (length <= PADDING || width <= PADDING || height <= PADDING
|| length > MAX_DIMENSION + PADDING
|| width > MAX_DIMENSION + PADDING
|| height > MAX_DIMENSION + PADDING
|| weight <= 0 || weight > 20)
{
throw new IllegalArgumentException();
}
int volume = length * width * height;
// Revert
length -= PADDING;
width -= PADDING;
height -= PADDING;
return volume;
}
catch (Throwable t)
{
MyExceptionReporter mer = new MyExceptionReporter();
mer.report(t); // Sanitize
// Revert
length -= PADDING;
width -= PADDING;
height -= PADDING;
return -1; // Non-positive error code
}
}
protected int getVolumePackage(int weight)
{
length += PADDING;
width += PADDING;
height += PADDING;
try
{
if (length <= PADDING || width <= PADDING || height <= PADDING
|| length > MAX_DIMENSION + PADDING
|| width > MAX_DIMENSION + PADDING
|| height > MAX_DIMENSION + PADDING
|| weight <= 0 || weight > 20)
{
throw new IllegalArgumentException();
}
int volume = length * width * height;
return volume;
}
catch (Throwable t)
{
MyExceptionReporter mer = new MyExceptionReporter();
mer.report(t); // Sanitize
return -1; // Non-positive error code
}
finally
{
// Revert
length -= PADDING;
width -= PADDING;
height -= PADDING;
}
}
protected int getVolumePackage(int weight)
{
try
{
// Validate first
if (length <= 0 || width <= 0 || height <= 0
|| length > MAX_DIMENSION || width > MAX_DIMENSION || height > MAX_DIMENSION
|| weight <= 0 || weight > 20)
{
throw new IllegalArgumentException();
}
}
catch (Throwable t)
{
MyExceptionReporter mer = new MyExceptionReporter();
mer.report(t); // Sanitize
return -1;
}
length += PADDING;
width += PADDING;
height += PADDING;
int volume = length * width * height;
length -= PADDING;
width -= PADDING;
height -= PADDING;
return volume;
}
protected int getVolumePackage(int weight)
{
try
{
// Validate first
if (length <= 0 || width <= 0 || height <= 0
|| length > MAX_DIMENSION || width > MAX_DIMENSION || height > MAX_DIMENSION
|| weight <= 0 || weight > 20)
{
throw new IllegalArgumentException();
}
}
catch (Throwable t)
{
MyExceptionReporter mer = new MyExceptionReporter();
mer.report(t); // Sanitize
return -1;
}
int volume = (length + PADDING) * (width + PADDING) * (height + PADDING);
return volume;
}
Do not complete abruptly from a finally block
class TryFinally
{
private static boolean doLogic()
{
try
{
throw new IllegalStateException();
}
finally
{
System.out.println("logic done");
return true;
}
}
}
问题描述:
这段代码中的return语句造成了finally块非正常结束,消除了IllegalStateException异常。
不要在finally块中使用return、break、continue或throw语句。当程序进入带有finally块的try块时,不管try块(或者任何相关的catch块)是否正常完成,finally块总是会执行的。导致finally块非正常结束的语句同样也会引发try块非正常结束,从而抑制了从try块或者catch块中抛出的任何异常。
依据Java语言规范,§14.20.2 “try-finally 和 try-catch-finally的执行”[JLS 2015]:
如果由于任何其它原因R造成try块非正常结束,那么finally块会被执行。接下来:
- 如果finally块正常结束,那么try语句由于R而仓促结束。
- 如果finally块由于原因S仓促结束,那么try语句就由于原因S而仓促结束(原因R会被丢弃)。
解决方法:
去除finally块中的return语句。
class TryFinally
{
private static boolean doLogic()
{
try
{
throw new IllegalStateException();
}
finally
{
System.out.println("logic done");
}
// Any return statements must go here;
// applicable only when exception is thrown conditionally
}
}
Do not let checked exceptions escape from a finally block
public class Operation
{
public static void doOperation(String some_file)
{
// ... Code to check or set character encoding ...
try
{
BufferedReader reader = new BufferedReader(new FileReader(some_file));
try
{
// Do operations
}
finally
{
reader.close();
// ... Other cleanup code ...
}
}
catch (IOException x)
{
// Forward to handler
}
}
}
问题描述:
这段代码在finally块中关闭reader对象,错误地假设finally块中的语句不会抛出异常,没有适当地处理可能发生的异常。
close()方法可能会抛出IOException异常。如果抛出这个异常,随后的其他清理语句将不会被执行。编译器之所以无法诊断出这个问题是因为任何IOException都会被外层的catch块所捕获。而且,close()操作抛出的异常会掩盖Do operations块执行期间抛出的异常,阻碍了可能的恢复处理。
在finally块中调用方法可能会抛出异常。没有捕获并处理这样的异常会导致整个try块仓促结束。而仓促结束会导致try块中抛出的任何异常都被丢失,从而阻碍了任何从特定问题中恢复的可能。
另外,由于这个异常改变了控制流程,finally块中异常抛出点后面的所有语句和表达式都不会被执行。
因此,程序必须正确地处理好从finally块中抛出的可检查异常。
允许可检查异常逃离finally块也违反了ERR04-J. 不要从finally语句块中非正常退出。
解决方法:
在finally块中,将close()方法的调用放在try-catch块里。从而可以处理潜在的IOException,并防止它的传递。
public class Operation
{
public static void doOperation(String some_file)
{
// ... Code to check or set character encoding ...
try
{
BufferedReader reader = new BufferedReader(new FileReader(some_file));
try
{
// Do operations
}
finally
{
try
{
reader.close();
}
catch (IOException ie)
{
// Forward to handle
}
// ... Other cleanup code ...
}
}
catch (IOException x)
{
// Forward to handler
}
}
}
public class Operation
{
public static void doOperation(String some_file)
{
// ... Code to check or set character encoding ...
// try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader(some_file)))
{
// Do operations
}
catch (IOException ex)
{
System.err.println("thrown exception: " + ex.toString());
Throwable[] suppressed = ex.getSuppressed();
for (int i = 0; i < suppressed.length; i++)
{
System.err.println("suppressed exception: " + suppressed[i].toString());
}
// Forward to handler
}
}
}
Java 7引入了try-with-resources新特性。它可以在错误发生时自动关闭特定的资源。
当doOperation()方法所在的try块抛出IOException异常或者在创建BufferedReader时产生任何 异常,它们都会被catch块捕获并打印“thrown exception”。如果在关闭reader时产生一个 IOException异常,它也能被catch块捕获并打印“thrown exception”。如果try块和关闭reader时都抛出IOException异常,catch块会捕获它们,并打印出try块的“thrown exception”,而关闭reader时抛出的IOException异常会被抑制,并在catch块中打印出“suppressed exception”。在所有这些情况下,reader都能被安全地关闭。
Do not throw RuntimeException, Exception, or Throwable
boolean isCapitalized(String s)
{
if (s == null)
{
throw new RuntimeException("Null String");
}
if (s.equals(""))
{
return true;
}
String first = s.substring(0, 1);
String rest = s.substring(1);
return (first.equals(first.toUpperCase()) && rest.equals(rest.toLowerCase()));
}
问题描述:
在这段代码中,如果向isCapitalized()方法传入null,会抛出RuntimeException异常。
方法不应抛出RuntimeException、Exception或者Throwable。处理这些异常需要捕获RuntimeException,违反了“ERR08-J. 不要捕获NullPointerException或者任何它的祖先类”。更有甚者,抛出RuntimeException可能会导致微妙的错误。例如,一个调用者不能通过检 查这个异常确定为何会抛出这个异常,也就不知道如何恢复。
方法可以抛出Exception或者RuntimeException子类的具体异常。为单个throw语句构造一个相关异常类是允许的。
解决方法:
抛出NullPointerException表明具体的异常情况。
boolean isCapitalized(String s)
{
if (s == null)
{
throw new NullPointerException();
}
if (s.equals(""))
{
return true;
}
String first = s.substring(0, 1);
String rest = s.substring(1);
return (first.equals(first.toUpperCase()) && rest.equals(rest.toLowerCase()));
}
注意:这里的null检查是多余的。如果移除它,当s为null时调用s.equals("")会抛出 NullPointerException。然而,null检查明确地表明了程序员的意图。更加复杂的代码可能需要明确的不可变性测试和恰当的throw语句。
Do not catch NullPointerException or any of its ancestors
boolean isName(String s)
{
try
{
String names[] = s.split(" ");
if (names.length != 2)
{
return false;
}
return (isCapitalized(names[0]) && isCapitalized(names[1]));
}
catch (NullPointerException e)
{
return false;
}
}
问题描述:
这段代码没有检查给定字符串是否为null,而是通过捕获NullPointerException并返回false来达到检测目的。
程序不应捕获java.lang.NullPointerException。一个运行期抛出的NullPointerException异常表明可能存在空指针问题,而它本该在应用代码中解决的(详情参见EXP01-J. 不要在需要一个对象的地方使用null)。
通过捕获NullPointerException而不对根本的原因进行处理是不合适的。有如下几个理由:
- 首先,捕获NullPointerException而不是简单地增加null检查明显增加了性能负担[Bloch 2008]。
- 其次,当一个try块中多个表达式都有可能抛出NullPointerException时,很难或者根本不可能判断哪个表达式引起了NullPointerException,因为NullPointerException的catch块会处理try语句块中任何位置抛出的NullPointerException。
- 再者,程序抛出NullPointerException后很少会处于一个可预料和可使用的状态。在捕获和记录(或更糟的是去抑制)第一次异常后继续尝试执行很少能NullPointerException成功。
解决方法:
明确检查String参数是否为null。
boolean isCapitalized(String s)
{
if (s == null)
{
return false;
}
if (s.equals(""))
{
return true;
}
String first = s.substring(0, 1);
String rest = s.substring(1);
return (first.equals(first.toUpperCase()) && rest.equals(rest.toLowerCase()));
}
注意:这里的null检查是多余的。如果移除它,当s为null时调用s.equals("")会抛出 NullPointerException。然而,null检查明确地表明了程序员的意图。更加复杂的代码可能需要明确的不可变性测试和恰当的throw语句。
Visibility and Atomicity
Ensure visibility when accessing shared primitive variables
final class ControlledStop implements Runnable
{
private boolean done = false;
@Override
public void run()
{
while (!done)
{
try
{
// ...
Thread.currentThread().sleep(1000); // Do something
}
catch(InterruptedException ie)
{
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public void shutdown()
{
done = true;
}
}
问题描述:
如果一个线程调用shutdown()方法设置标志,第二个线程可能不会观察到这个变化。因此,第二个线程会观察到done仍然是false,然后错误地调用sleep()方法。编译器和运行期编译器(JITs)如果确定done的值从未被同一个线程修改,有可能会对代码进行优化,导致一个无限循环。
在一个线程中读取一个共享的基础类型变量有可能得不到最近一次由另一个线程写入变量的值。因此,该线程有可能读取到共享变量的一个旧值。为了确保最近一次更新的可见性,要么变量被声明为volatile,要么变量的读写必须是同步的(使用synchronized关键字)。
解决方法:
有三种合规方案:
- volatile:将done标志声明为volatile,从而确保对其数值的写入对其他线程是可见的。
- AtomicBoolean:将done标志声明为java.util.concurrent.atomic.AtomicBoolean类型。这个原子类型可以保证对其数值的写入对其他线程是可见的。
- synchronized:使用对象锁保证更新对其他线程是可见的。
final class ControlledStop implements Runnable
{
private volatile boolean done = false;
@Override
public void run()
{
while (!done)
{
try
{
// ...
Thread.currentThread().sleep(1000); // Do something
}
catch(InterruptedException ie)
{
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public void shutdown()
{
done = true;
}
}
final class ControlledStop implements Runnable
{
private final AtomicBoolean done = new AtomicBoolean(false);
@Override
public void run()
{
while (!done.get())
{
try
{
// ...
Thread.currentThread().sleep(1000); // Do something
}
catch(InterruptedException ie)
{
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public void shutdown()
{
done.set(true);
}
}
final class ControlledStop implements Runnable
{
private boolean done = false;
@Override
public void run()
{
while (!isDone())
{
try
{
// ...
Thread.currentThread().sleep(1000); // Do something
}
catch(InterruptedException ie)
{
Thread.currentThread().interrupt(); // Reset interrupted status
}
}
}
public synchronized boolean isDone()
{
return done;
}
public synchronized void shutdown()
{
done = true;
}
}
注意:
- 对象锁会导致线程阻塞并且可能引入竞争。volatile修饰的共享变量不会阻塞线程。
- synchronized在volatile关键字或者java.util.concurrent.atomic.Atomic*类型不能使用的场景是一个更加安全的替代方案,比如当一个变量的新值依赖它的当前值时。
Ensure visibility of shared references to immutable objects
// Immutable Helper
public final class Helper
{
private final int n;
public Helper(int n)
{
this.n = n;
}
}
final class Foo
{
private Helper helper;
public Helper getHelper()
{
return helper;
}
public void setHelper(int num)
{
helper = new Helper(num);
}
}
问题描述:
如果一个线程调用setHelper()方法修改了helper引用,第二个线程可能会观察到旧的引用。
解决方法:
有三种合规方案:
- volatile:将helper引用声明为volatile,从而确保对引用的修改对其他线程是可见的。
- AtomicBoolean:将helper引用声明为java.util.concurrent.atomic.AtomicReference类型。这个原子类型可以保证对引用的修改对其他线程是可见的。
- synchronized:使用对象锁保证更新对其他线程是可见的。
final class Foo
{
private volatile Helper helper;
public Helper getHelper()
{
return helper;
}
public void setHelper(int num)
{
helper = new Helper(num);
}
}
final class Foo
{
private AtomicReference<Helper> helperRef = new AtomicReference<>();
public Helper getHelper()
{
return helperRef.get();
}
public void setHelper(int num)
{
helperRef.set(new Helper(num));
}
}
final class Foo
{
private Helper helper;
public synchronized Helper getHelper()
{
return helper;
}
public synchronized void setHelper(int num)
{
helper = new Helper(num);
}
}
Ensure that compound operations on shared variables are atomic
final class Flag
{
private boolean flag = true;
public void toggle()
{
flag = !flag;
}
public boolean getFlag()
{
return flag;
}
}
问题描述:
在多线程的环境下,因为flag值的读取、取反、回写,这段代码会产生数据竞争,从而得到意料之外的结果。
考虑两个线程调用toggle()的情况,对flag操作两次是希望它恢复初始值。然而,下面的场景却得到了意料之外的结果。
时间 | flag | 线程 | 动作 |
---|---|---|---|
1 | true | t1 | 读取flag的当前状态到临时变量中,为true |
2 | true | t2 | 读取flag的当前状态到临时变量中,仍然为true |
3 | true | t1 | 将临时变量设置为false |
4 | true | t2 | 将临时变量设置为false |
5 | false | t1 | 将临时变量的值写入flag |
6 | false | t2 | 将临时变量的值写入flag |
因此,t2的调用结果并不会反映在flag中,这个程序的执行看起来就像调用了toggle()方法一次而不是两次。
复合操作由多个离散操作组成,包括前后缀表达式++和--,复合表达式*=、/=、%=、+=、-=、<<=、>>=、>>>=、^=和|=等。对于共享变量的复合操作必须是原子的,以避免数据竞争(data race)和竞争环境(race condition)。
final class Flag
{
private volatile boolean flag = true;
public void toggle()
{
flag = !flag;
}
public boolean getFlag()
{
return flag;
}
}
问题描述:
将flag声明为volatile并不能解决这个问题。因为声明一个变量为volatile并不能保证对变量进行的复合操作是原子性的。
解决方法:
有四种合规方案:
- synchronized
- volatile-read, synchronized-write
- read-write lock
- AtomicBoolean
final class Flag
{
private boolean flag = true;
public synchronized void toggle()
{
flag = !flag;
}
public synchronized boolean getFlag()
{
return flag;
}
}
这个方案使用对象锁保护了对flag的读和写。此外,synchronized保证了这些变化对所有线程来说都是可见的。
现在只有两种执行顺序是可能的。其中一种如以下场景所示:
时间 | flag | 线程 | 动作 |
---|---|---|---|
1 | true | t1 | 读取flag的当前状态到临时变量中,为true |
2 | true | t1 | 将临时变量设置为false |
3 | false | t1 | 将临时变量的值写入flag |
4 | false | t2 | 读取flag的当前状态到临时变量中,为false |
5 | false | t2 | 将临时变量设置为true |
6 | true | t2 | 将临时变量的值写入flag |
第二个执行顺序涉及同样的操作,只是t2在t1之前开始和结束。
final class Flag
{
private volatile boolean flag = true;
public synchronized void toggle()
{
flag = !flag;
}
public boolean getFlag()
{
return flag;
}
}
注意:
- 在getFlag()方法中,如果执行了其他操作,就不能使用这样的方式,除非采用了同步的方式来返回volatile字段的值。
- 除非读性能很关键,否则这样的方法比同步方法要差不少。
final class Flag
{
private boolean flag = true;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void toggle()
{
writeLock.lock();
try
{
flag = !flag;
}
finally
{
writeLock.unlock();
}
}
public boolean getFlag()
{
readLock.lock();
try
{
return flag;
}
finally
{
readLock.unlock();
}
}
}
读写锁允许多个读取和一个写入访问共享状态,但两者不能同时进行。
final class Flag
{
private AtomicBoolean flag = new AtomicBoolean(true);
public void toggle()
{
boolean temp;
do
{
temp = flag.get();
}
while (!flag.compareAndSet(temp, !temp));
}
public AtomicBoolean getFlag()
{
return flag;
}
}
使用AtomicBoolean类的compareAndSet()更新flag。
Do not assume that a group of calls to independently atomic methods is atomic
final class Adder
{
private final AtomicReference<BigInteger> first;
private final AtomicReference<BigInteger> second;
public Adder(BigInteger f, BigInteger s)
{
first = new AtomicReference<BigInteger>(f);
second = new AtomicReference<BigInteger>(s);
}
public void update(BigInteger f, BigInteger s)
{
first.set(f);
second.set(s);
}
public BigInteger add()
{
return first.get().add(second.get());
}
}
问题描述:
在这段代码中,用线程安全的AtomicReference封装了BigInteger对象的引用。尽管AtomicReference是一个能够被原子性更新的对象引用,然而,那些包含了多于一个原子引用的操作却并不是原子性的。例如,一个线程调用update()方法,而另一个线程调用add(),可能会导致add()方法把新的first加到旧的second上去,从而得到意料之外的结果。
解决方法:
将update()和add()方法声明为synchornized以保证原子性。
final class Adder
{
private final AtomicReference<BigInteger> first;
private final AtomicReference<BigInteger> second;
public Adder(BigInteger f, BigInteger s)
{
first = new AtomicReference<BigInteger>(f);
second = new AtomicReference<BigInteger>(s);
}
public synchronized void update(BigInteger f, BigInteger s)
{
first.set(f);
second.set(s);
}
public synchronized BigInteger add()
{
return first.get().add(second.get());
}
}
final class KeyedCounter
{
private final Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
public void increment(String key)
{
Integer old = map.get(key);
int oldValue = (old == null) ? 0 : old.intValue();
if (oldValue == Integer.MAX_VALUE)
{
throw new ArithmeticException("Out of range");
}
map.put( key, oldValue + 1);
}
public Integer getCount(String key)
{
return map.get(key);
}
}
问题描述:
在这段代码中,定义了KeyedCounter类,这个类并不是线程安全的。虽然HashMap被封装到synchronizedMap()中,但整个递增操作并不是原子性的。
解决方法:
有两种合规方案:
- 使用一个内部的私有锁对象来保证原子性,同步increment()和getCount()方法中的语句。
- 使用ConcurrentHashMap类提供的方法进行原子操作。
final class KeyedCounter
{
private final Map<String, Integer> map = new HashMap<String, Integer>();
private final Object lock = new Object();
public void increment(String key)
{
synchronized (lock)
{
Integer old = map.get(key);
int oldValue = (old == null) ? 0 : old.intValue();
if (oldValue == Integer.MAX_VALUE)
{
throw new ArithmeticException("Out of range");
}
map.put(key, oldValue + 1);
}
}
public Integer getCount(String key)
{
synchronized (lock)
{
return map.get(key);
}
}
}
final class KeyedCounter
{
private final ConcurrentMap<String, AtomicInteger> map = new ConcurrentHashMap<String, AtomicInteger>();
private final Object lock = new Object();
public void increment(String key)
{
AtomicInteger value = new AtomicInteger();
AtomicInteger old = map.putIfAbsent(key, value);
if (old != null)
{
value = old;
}
synchronized (lock)
{
if (value.get() == Integer.MAX_VALUE)
{
throw new ArithmeticException("Out of range");
}
value.incrementAndGet(); // Increment the value atomically
}
}
public Integer getCount(String key)
{
AtomicInteger value = map.get(key);
return (value == null) ? null : value.get();
}
// Other accessors ...
}
注意:本合规的解决方案仍然需要同步,因为不同步,防止溢出和自增的检查将不是原子的,所以两个线程调用increment()仍然会导致溢出。同步块更小同时不包含对新值的查找和叠加,所以相对于前面的合规解决方案对性能的影响更小。
Ensure atomicity when reading and writing 64-bit values
class LongContainer
{
private long i = 0;
void assignValue(long j)
{
i = j;
}
void printLong()
{
System.out.println("i = " + i);
}
}
问题描述:
在这段代码中,如果一个线程重复调用assignValue()方法,并且另一个线程重复地调用printLong()方法,printLong()方法可能偶然打印一个i的值,该值既不是0,也不是参数j的值。
在Java语言的内存模型中,对于一个非volatile的long或者double类型的数值的单个写操作,会被处理成两个独立的写操作,每个操作对应32位数值。这可能导致一个线程看到了一次写入的64位数值的低32位和另一次写入的64位数值的高32位。
解决方法:
声明i为volatile。对于volatile的long和double类型的数值的读写永远是原子性的。
class LongContainer
{
private volatile long i = 0;
void assignValue(long j)
{
i = j;
}
void printLong()
{
System.out.println("i = " + i);
}
}
Lock
Use private final lock objects to synchronize classes that may interact with untrusted code
public class SomeObject
{
public synchronized void changeValue()
{
// ...
}
public static SomeObject lookup(String name)
{
// ...
}
}
// Untrusted code
String name = // ...
SomeObject someObject = SomeObject.lookup(name);
if (someObject == null)
{
// ... handle error
}
synchronized (someObject)
{
while (true)
{
Thread.sleep(Integer.MAX_VALUE); // Indefinitely lock someObject
}
}
问题描述:
在这段代码中,将一个SomeObject类的实例暴露给非受信代码。这段非受信代码想要获得一个基于对象监视器的锁,并基于此产生一个不确定的延迟,从而防止同步的changeValue()方法得到同一个锁。
解决方法:
线程安全的公有类跟非受信代码交互时,必须使用private final锁对象。
现有的使用内置同步的类必须被重构为使用基于这样的锁对象的块同步。在本合规的解决方案中,调用changeValue()基于一个private final对象实例获取了锁,这个锁对于类外部的调用者是不可访问的。
public class SomeObject
{
private final Object lock = new Object(); //private final lock object
public void changeValue()
{
synchronized (lock) // Locks on the private Object
{
// ...
}
}
}
public class SomeObject
{
public static synchronized void changeValue()
{
// ...
}
}
// Untrusted code
synchronized (SomeObject.class)
{
while (true)
{
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay someObject
}
}
问题描述:
在这段代码中,将SomeObject类对象(class object)暴露给非受信代码。这段非受信代码想要获得一个基于类对象监视器的锁,并基于此产生一个不确定的延迟,从而防止同步的changeValue()方法得到同一个锁。
解决方法:
线程安全的公有类跟非受信代码交互时,必须使用private final锁对象。
现有的使用基于类对象的内置同步的类必须被重构为使用private static final对象的块同步。
public class SomeObject
{
private static final Object lock = new Object();
public void changeValue()
{
synchronized (lock) // Locks on the private Object
{
// ...
}
}
}
Do not synchronize on objects that may be reused
private int count = 0;
private final Integer lock = count;
public void doSomething()
{
synchronized (Lock)
{
// ...
}
}
问题描述:
这段代码使用装箱(boxed)Integer对象实现锁。装箱类型对于一定范围内的整数数值会使用相同的实例。当对象数值能被一个字节表示时,包装对象会被重用。使用装箱Integer对象的内置锁是不安全的。
一般来说,包含一个装箱数值的任何数据类型的锁都是不安全的。
解决方法:
使用new操作(new Integer(value))来创建的对象是唯一的而不是被重用的。
当显式地创建一个Integer对象时,这个对象只有唯一的一个引用。而且,它自己的内置锁不但区别于其他Integer对象,并且区别于具有同一个数值的经过装箱的整数不同。
private int count = 0;
private final Integer lock = new Integer(count);
public void doSomething()
{
synchronized (lock)
{
// ...
}
}
private final String lock = "LOCK";
public void doSomething()
{
synchronized (lock)
{
// ...
}
}
问题描述:
String文本是常量并且会被自动intern。
intern的意思是,如果在池中已经存在一个等于这个String对象的字符串(一般使用equals(Object)方法进行等于的判断),则返回池中的字符串。否则,这个字符串对象会被加入池中,并且返回指向这个字符串对象的引用。
因此一个被intern的String对象在Java虚拟机中的行为类似一个全局变量。基于String常亮的锁有重用性问题。
解决方法:
基于非intern的字符串实例实现锁。
一个字符串实例和一个字符串文本是不同的。一个实例具有唯一的一个引用,并且它自己的内置锁和其他的字符串对象或文本不同。
private final String lock = new String("LOCK");
public void doSomething()
{
synchronized (lock)
{
// ...
}
}
Do not synchronize on the intrinsic locks of high-level concurrency objects
private final Lock lock = new ReentrantLock();
public void doSomething()
{
synchronized(lock)
{
// ...
}
}
问题描述:
这段代码基于ReentrantLock实例的内置锁实现同步,而不是ReentrantLock封装的可重入的互斥锁进行同步。
解决方法:
使用Lock接口提供的lock()和unlock()方法。
private final Lock lock = new ReentrantLock();
public void doSomething()
{
lock.lock();
try
{
// ...
}
finally
{
lock.unlock();
}
}
Synchronize access to static fields that can be modified by untrusted code
public final class CountHits
{
private static int counter;
public void incrementCounter()
{
counter++;
}
}
问题描述:
这段代码没有以同步的方式访问static的counter字段。
既能修改静态字段又会被非受信代码调用的方法对静态字段必须以同步的方式访问。
解决方法:
使用一个static final private的锁来保护counter字段。
public final class CountHits
{
private static int counter;
private static final Object lock = new Object();
public void incrementCounter()
{
synchronized (lock)
{
counter++;
}
}
}
Do not use an instance lock to protect shared static data
public final class CountBoxes implements Runnable
{
private static volatile int counter;
// ...
private final Object lock = new Object();
@Override
public void run()
{
synchronized (lock)
{
counter++;
// ...
}
}
public static void main(String[] args)
{
for (int i = 0; i < 2; i++)
{
new Thread(new CountBoxes()).start();
}
}
}
问题描述:
这段代码试图使用一个非静态锁对象来保护对静态counter字段的访问。当两个Runnable
的任务启动时,他们创建了两个锁对象的实例,对每个实例分别进行锁定。
不应该使用实例锁来保护静态共享变量,因为实例锁在两个或者多个实例的情况下是无效的。如果不使用一个静态的锁对象,会导致共享状态在并发进入时是不被保护的。
另外,与非受信代码交互的类的锁对象必须是private和final的。
public final class CountBoxes implements Runnable
{
private static volatile int counter;
// ...
private final Object lock = new Object();
public synchronized void run()
{
counter++;
}
// ...
}
问题描述:
在这段代码中,方法同步使用了内置锁,这个内置锁是与每一个类实例对象相关联的,而不是与类本身相关联的内置锁。所以,使用不同的Runnable实例构建的线程会感知到不一致的counter的值。
解决方法:
使用一个static final private的静态锁对象来保护counter字段,保证递增操作的原子性。
public class CountBoxes implements Runnable
{
private static int counter;
// ...
private static final Object lock = new Object();
public void run()
{
synchronized (lock)
{
counter++;
// ...
}
}
// ...
}
Ensure actively held locks are released on exceptional conditions
public final class Client
{
private final Lock lock = new ReentrantLock();
public void doSomething(File file)
{
InputStream in = null;
try
{
lock.lock();
in = new FileInputStream(file);
// Perform operations on the open file
lock.unlock();
}
catch (FileNotFoundException x)
{
// Handle exception
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException x)
{
// Handle exception
}
}
}
}
}
问题描述:
这段代码使用一个ReentrantLock保护一个打开的文件资源。但是对打开的文件执行操作发生异常的情况下不会释放锁。当异常被抛出,控制流转移到catch块,unlock()不会被调用。
解决方法:
在finally块中释放之前获取的锁。
public final class Client
{
private final Lock lock = new ReentrantLock();
public void doSomething(File file)
{
InputStream in = null;
try
{
lock.lock();
in = new FileInputStream(file);
// Perform operations on the open file
lock.unlock();
}
catch (FileNotFoundException x)
{
// Handle exception
}
finally
{
lock.unlock();
if (in != null)
{
try
{
in.close();
}
catch (IOException x)
{
// Handle exception
}
}
}
}
}
Thread APIs
Do not invoke Thread.run()
public final class Foo implements Runnable
{
@Override
public void run()
{
// ...
}
public static void main(String[] args)
{
Foo foo = new Foo();
new Thread(foo).run();
}
}
问题描述:
这段代码直接在当前线程上下文中直接调用run()。新创建的线程不会启动,因为启动新线程的时候错误的使用了run()方法。
解决方法:
调用start()方法启动一个新线程。
public final class Foo implements Runnable
{
@Override
public void run()
{
// ...
}
public static void main(String[] args)
{
Foo foo = new Foo();
new Thread(foo).start();
}
}
Notify all waiting threads rather than a single thread
public final class ProcessStep implements Runnable
{
private static final Object lock = new Object();
private static int time = 0;
// Do Perform operations when field time reaches this value
private final int step;
public ProcessStep(int step)
{
this.step = step;
}
@Override
public void run()
{
try
{
synchronized (lock)
{
while (time != step)
{
lock.wait();
}
// Perform operations
time++;
lock.notify();
}
}
catch (InterruptedException ie)
{
// Reset interrupted status
Thread.currentThread().interrupt();
}
}
public static void main(String[] args)
{
for (int i = 4; i >= 0; i--)
{
new Thread(new ProcessStep(i)).start();
}
}
}
问题描述:
Object.notify()方法一次只能唤醒一个线程。除非正好唤醒了所需要的下一步的执行线程,否则将发生死锁。
解决方法:
调用notifyAll()方法来通知等待中的线程。准备好的线程可以执行它的任务,其他条件判断为假(循环表达式为true)的线程继续等待。
public final class ProcessStep implements Runnable
{
private static final Object lock = new Object();
private static int time = 0;
// Do Perform operations when field time reaches this value
private final int step;
public ProcessStep(int step)
{
this.step = step;
}
@Override
public void run()
{
try
{
synchronized (lock)
{
while (time != step)
{
lock.wait();
}
// Perform operations
time++;
lock.notifyAll(); // Use notifyAll() instead of notify()
}
}
catch (InterruptedException ie)
{
// Reset interrupted status
Thread.currentThread().interrupt();
}
}
public static void main(String[] args)
{
for (int i = 4; i >= 0; i--)
{
new Thread(new ProcessStep(i)).start();
}
}
}
Always invoke wait() and await() methods inside a loop
synchronized (object)
{
if (<condition does not hold>)
{
object.wait();
}
// Proceed when condition holds
}
问题描述:
这段代码在if代码中调用wait()方法,没有检查接到通知后的后置条件。如果通知是意外或恶意的,线程将会过早地被唤醒。
wait()方法必须在检查条件断言的循环中被调用。注意条件断言是循环的条件表达式的非。
解决方法:
在while循环内调用wait()方法。在调用wait()方法的前后都对条件进行检查。
synchronized (object)
{
while (<condition does not hold>)
{
object.wait();
}
// Proceed when condition holds
}
Thread Pools
Use thread pools to enable graceful degradation of service during traffic bursts
class Helper
{
public void handle(Socket socket)
{
// ...
}
}
final class RequestHandler
{
private final Helper helper = new Helper();
private final ServerSocket server;
private RequestHandler(int port) throws IOException
{
server = new ServerSocket(port);
}
public static RequestHandler newInstance() throws IOException
{
return new RequestHandler(0); // Selects next available port
}
public void handleRequest()
{
new Thread(new Runnable() {
public void run() {
try
{
helper.handle(server.accept());
}
catch (IOException e)
{
// Forward to handler
}
}
}).start();
}
}
问题描述:
这段代码演示了thread-per-message模式。RequestHandler类提供了一个public静态工厂方法,调用者可以获得一个RequestHandler 实例。随后调用handleRequest()方法来处理每一个请求。
Thread-Per-Message策略无法提供优雅的服务降级。线程被创建,处理正常进行,直到稀缺资源耗尽。
解决方法:
使用一个固定的线程池来限制并发执行线程的数目。提交到线程池的任务存储在内部队列中。将任务存储在队列中可防止系统在尝试响应所有传入请求时负荷太重,并允许系统通过提供固定最大数量的并发客户端来优雅地降级。
// class Helper remains unchanged
final class RequestHandler
{
private final Helper helper = new Helper();
private final ServerSocket server;
private final ExecutorService exec;
private RequestHandler(int port, int poolSize) throws IOException
{
server = new ServerSocket(port);
exec = Executors.newFixedThreadPool(poolSize);
}
public static RequestHandler newInstance(int poolSize) throws IOException
{
return new RequestHandler(0, poolSize);
}
public void handleRequest()
{
Future<?> future = exec.submit(new Runnable() {
@Override
public void run()
{
try
{
helper.handle(server.accept());
}
catch (IOException e)
{
// Forward to handler
}
}
});
}
// ... Other methods such as shutting down the thread pool
// and task cancellation ...
}
Ensure that tasks executing in a thread pool do not fail silently
final class PoolService
{
private final ExecutorService pool = Executors.newFixedThreadPool(10);
public void doSomething()
{
pool.execute(new Task());
}
}
final class Task implements Runnable
{
@Override
public void run()
{
// ...
throw new NullPointerException();
// ...
}
}
问题描述:
Task.run()方法会抛出运行期异常,例如NullPointerException,任务意外终止,但任务并未通知应用程序,从而缺少恢复机制。结果就是NullPointerException被忽略了。
线程池中的所有任务必须提供这样一种机制:如果它们异常终止,需要通知应用程序。做不到这一点不会导致资源泄漏,因为池中的线程仍然被会重复使用,但这使得故障诊断非常困难甚至不可能。
在应用程序级别处理异常的最好方法是使用异常处理器(exception handler)。异常处理器可以执行诊断操作,清理和关闭Java虚拟机,或者只是记录故障的详细信息。
解决方法:
有三种合规方案:
- ThreadPoolExecutor Hooks
- Uncaught Exception Handler
- Future and submit()
final class PoolService
{
// The values have been hard-coded for brevity
ExecutorService pool = new CustomThreadPoolExecutor(
10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
// ...
}
class CustomThreadPoolExecutor extends ThreadPoolExecutor
{
// ... Constructor ...
public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) \
{
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void afterExecute(Runnable r, Throwable t)
{
super.afterExecute(r, t);
if (t != null)
{
// Exception occurred, forward to handler
}
// ... Perform task-specific cleanup actions
}
@Override
public void terminated()
{
super.terminated();
// ... Perform final clean-up actions
}
}
任务特定的恢复或清理操作可以通过覆写java.util.concurrent.ThreadPoolExecutor 类的afterExecute()钩子来实现。当任务成功完成run()方法中的所有语句或者由于异常而停止时,将调用此钩子。
final class PoolService
{
private static final ThreadFactory factory = new ExceptionThreadFactory(new MyExceptionHandler());
private static final ExecutorService pool = Executors.newFixedThreadPool(10, factory);
public void doSomething()
{
pool.execute(new Task()); // Task is a runnable class
}
public static class ExceptionThreadFactory implements ThreadFactory
{
private static final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
private final Thread.UncaughtExceptionHandler handler;
public ExceptionThreadFactory(Thread.UncaughtExceptionHandler handler)
{
this.handler = handler;
}
@Override
public Thread newThread(Runnable run)
{
Thread thread = defaultFactory.newThread(run);
thread.setUncaughtExceptionHandler(handler);
return thread;
}
}
public static class MyExceptionHandler extends ExceptionReporter implements Thread.UncaughtExceptionHandler
{
// ...
@Override
public void uncaughtException(Thread thread, Throwable t)
{
// Recovery or logging code
}
}
}
为线程池设置一个Uncaught Exception Handler。一个ThreadFactory参数在线程池构造期间被传入。工厂负责创建新线程并为其设置Uncaught Exception Handler。
final class PoolService
{
private final ExecutorService pool = Executors.newFixedThreadPool(10);
public void doSomething()
{
Future<?> future = pool.submit(new Task());
// ...
try
{
future.get();
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt(); // Reset interrupted status
}
catch (ExecutionException e)
{
Throwable exception = e.getCause();
// Forward to exception reporter
}
}
}
调用ExecutorService.submit()方法提交任务,得到一个Future对象。当通过ExecutorService.submit()方法提交任务时,抛出的异常不会到达Uncaught Exception Handler,这是因为抛出的异常作为了返回状态的一部分,被封装在ExecutionException中并由Future.get()重新抛出。
Thread-Safety Miscellaneous
Do not override thread-safe methods with methods that are not thread-safe
class Base
{
public synchronized void doSomething()
{
// ...
}
}
class Derived extends Base
{
@Override
public void doSomething()
{
// ...
}
}
问题描述:
在这段代码中,Derived类中的非同步方法覆写了Base类中的同步doSomething()方法。
Base类的doSomething()方法可以被多个线程安全地使用,但这并不适用于Derived子类的实例。这种变成错误可能很难诊断,因为接受Base实例的线程也可以接受其子类的实例。 因此,客户端可能不知道它们是在一个线程安全的类的非线程安全的子类的实例上做了操作。
禁止使用非线程安全的方法覆写线程安全的方法。
解决方法:
有两种合规方案:
- 子类提供同步方法
- 使用private final锁对象
class Base
{
public synchronized void doSomething()
{
// ...
}
}
class Derived extends Base
{
@Override
public synchronized void doSomething()
{
// ...
}
}
class Base
{
public synchronized void doSomething()
{
// ...
}
}
class Derived extends Base
{
private final Object lock = new Object();
@Override
public void doSomething()
{
synchronized (lock)
{
// ...
}
}
}
Input Output
Detect and handle file-related errors
File file = new File(args[0]);
file.delete();
问题描述:
Java的文件操作相关方法一般通过返回值而不是抛出异常表示失败。因此,如果程序忽略文件操作的返回值,那么将会检测不到操作失败的情况。Java程序必须检查文件I/O操作方法的返回值。
解决方法:
有两种合规方案:
- 检查返回值
- 调用Java 7的java.nio.file.Files.delete()方法删除文件
File file = new File("file");
if (!file.delete())
{
System.out.println("Deletion failed");
}
Path file = new File(args[0]).toPath();
try
{
Files.delete(file);
}
catch (IOException x)
{
System.out.println("Deletion failed");
// Handle error
}
在Java 7文档中,定义了Files.delete()会抛出以下异常:
异常 | 原因 |
---|---|
NoSuchFileException | 文件不存在 |
DirectoryNotEmptyException | 文件是一个不为空的目录,所以不能删除 |
IOException | 一个I/O错误发生 |
SecurityException | 在安装了默认的提供器和安全管理器的情况下,调用SecurityManager.checkDelete(String)方法检查文件是否有删除访问权限 |
Remove temporary files before termination
class TempFile
{
public static void main(String[] args) throws IOException
{
File f = File.createTempFile("tempnam",".tmp");
FileOutputStream fop = null;
try
{
fop = new FileOutputStream(f);
String str = "Data";
fop.write(str.getBytes());
fop.flush();
}
finally
{
// Stream/file still open; file will
// not be deleted on Windows systems
f.deleteOnExit(); // Delete the file when the JVM terminates
if (fop != null)
{
try
{
fop.close();
}
catch (IOException x)
{
// Handle error
}
}
}
}
}
问题描述:
这段代码使用deleteOnExit()方法保证临时文件在Java虚拟机终止时删除。然而在Windows上deleteOnExit()有缺陷。在相关的数据流或RandomAccessFile文件关闭之前调用deleteOnExit()会导致JVM不能删除文件。
解决方法:
使用Java 7的NIO2包中的若干方法穿件临时文件。它使用createTempFile()方法创建一个不可预期的名字。代码使用try-with-resources结构打开文件,无论是否发生异常,文件都会被自动关闭。最后使用Java 7的DELETE_ON_CLOSE选项打开文件,在关闭文件的时候自动删除文件。
class TempFile
{
public static void main(String[] args)
{
Path tempFile = null;
try
{
tempFile = Files.createTempFile("tempnam", ".tmp");
try (BufferedWriter writer = Files.newBufferedWriter(tempFile, Charset.forName("UTF8"), StandardOpenOption.DELETE_ON_CLOSE))
{
// Write to file
}
System.out.println("Temporary file write done, file erased");
}
catch (FileAlreadyExistsException x)
{
System.err.println("File exists: " + tempFile);
}
catch (IOException x)
{
// Some other sort of failure, such as permissions.
System.err.println("Error creating temporary file: " + x);
}
}
}
Release resources when they are no longer needed
public int processFile(String fileName) throws IOException, FileNotFoundException
{
FileInputStream stream = new FileInputStream(fileName);
BufferedReader bufRead = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = bufRead.readLine()) != null)
{
sendLine(line);
}
return 1;
}
问题描述:
这段代码打开并使用一个文件,但没有显式地关闭这个文件。
解决方法:
有两种合规方案:
- 显示地释放资源
- 使用Java 7的try-with-resources
try
{
final FileInputStream stream = new FileInputStream(fileName);
try
{
final BufferedReader bufRead = new BufferedReader(new InputStreamReader(stream));
String line;
while ((line = bufRead.readLine()) != null)
{
sendLine(line);
}
}
finally
{
if (stream != null)
{
try
{
stream.close();
}
catch (IOException e)
{
// Forward to handler
}
}
}
}
catch (IOException e)
{
// Forward to handler
}
try (FileInputStream stream = new FileInputStream(fileName);
BufferedReader bufRead = new BufferedReader(new InputStreamReader(stream)))
{
String line;
while ((line = bufRead.readLine()) != null)
{
sendLine(line);
}
}
catch (IOException e)
{
// Forward to handler
}
Distinguish between characters or bytes read from a stream and -1
FileInputStream in;
// Initialize stream
byte data;
while ((data = (byte) in.read()) != -1)
{
// ...
}
问题描述:
这段代码将read()方法返回的int类型的值直接转换为byte类型,并且将其与-1比较,检查是否到达了流的末尾。如果read()方法遇到文件的字节是0xFF,转换成byte类型的值是0xFF,在跟-1进行比较前,byte类型会被提升为int类型,byte类型的0xFF变成0xFFFF,跟-1(0xFFFF)相等。因此如果读到0xFF字节,循环将提前结束。
解决方法:
使用int类型的变量获取输入方法的byte返回值。
FileInputStream in;
// Initialize stream
int inbuff;
byte data;
while ((inbuff = in.read()) != -1)
{
data = (byte) inbuff;
// ...
}
FileReader in;
// Initialize stream
char data;
while ((data = (char) in.read()) != -1)
{
// ...
}
问题描述:
这段代码将read()方法返回的int类型的值直接转换为char类型,并且将其与-1比较,检查是否到达了流的末尾。当读到文件尾时,read()方法返回-1(0xFFFF),转换成char类型的值是0x0000FFFF,和-1(0x0000FFFF)进行比较永远返回false。
解决方法:
使用int类型的变量获取输入方法的char返回值。
FileReader in;
// Initialize stream
int inbuff;
char data;
while ((inbuff = in.read()) != -1)
{
data = (char) inbuff;
// ...
}
Do not rely on the write() method to output integers outside the range 0 to 255
class ConsoleWrite
{
public static void main(String[] args)
{
System.out.write(Integer.valueOf(args[0]));
System.out.flush();
}
}
问题描述:
这段代码没有对用户输入的数值进行校验。任何不在0~255范围的数值都会被截断。
例如,write(303)在ASCII系统中打印出/。因为会使用303的低8位比特,而忽略了高24位(303 % 256 = 47,47代表ASCII中的/),结果是输入整除256后剩余的余数。
Java.io.OutputStream流中的write()方法,接收一个范围是0~255的整型参数。因为整型类型可能超出这个范围,没有检测范围的话可能导致参数的高位部分被截断。
解决方法:
有两种合规方案:
- 仅在输入整数在正确的范围内时输出对应的字符
- 使用DataOutputStream.writeInt()方法
class FileWrite
{
public static void main(String[] args) throws NumberFormatException, IOException
{
// Perform range checking
int value = Integer.valueOf(args[0]);
if (value < 0 || value > 255)
{
throw new ArithmeticException("Value is out of range");
}
System.out.write(value);
System.out.flush();
}
}
class FileWrite
{
public static void main(String[] args) throws NumberFormatException, IOException
{
DataOutputStream dos = new DataOutputStream(System.out);
dos.writeInt(Integer.valueOf(args[0].toString()));
System.out.flush();
}
}
Serialization
Enable serialization compatibility during class evolution
class GameWeapon implements Serializable
{
int numOfWeapons = 10;
public String toString()
{
return String.valueOf(numOfWeapons);
}
}
问题描述:
实现Serializable接口而不覆盖Serializable接口方法的类会使用默认的序列化格式。在类更改的情况下,该类的旧版本的用户生成的字节流将与新实现不兼容。因此,依赖于默认序列化格式的可序列化类在演化过程中不能保持其序列化方面的兼容性。
在这段代码中,GameWeapon类包含了一个可序列化字段numOfWeapons,并且使用了默认的序列化格式。任何对该类内部表示的改变都会破坏现存的序列化格式。
为了实现可序列化类的兼容演进,开发人员必须使用自定义的序列化格式。
解决方法:
有两种合规方案:
- 显示地定义serialVersionUID
- 通过serialPersistentFields来使用自定义的序列化
public class GameWeapon implements Serializable {
private static final long serialVersionUID = 24L;
int numOfWeapons = 10;
public String toString()
{
return String.valueOf(numOfWeapons);
}
}
类中显式地声明了serialVersionUID字段,这个serialVersionUID包含了一个在这个类的版本中独一无二的数值。对具有相同的类名和版本ID的序列化对象,JVM会忠实地对其进行 反序列化。
class WeaponStore implements Serializable
{
int numOfWeapons = 10; // Total number of weapons
}
public class GameWeapon implements Serializable
{
WeaponStore ws = new WeaponStore();
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("ws", WeaponStore.class)};
private void readObject(ObjectInputStream ois) throws IOException
{
try
{
ObjectInputStream.GetField gf = ois.readFields();
this.ws = (WeaponStore) gf.get("ws", ws);
}
catch (ClassNotFoundException e)
{ /* Forward to handler */
}
}
private void writeObject(ObjectOutputStream oos) throws IOException
{
ObjectOutputStream.PutField pf = oos.putFields();
pf.put("ws", ws);
oos.writeFields();
}
public String toString()
{
return String.valueOf(ws);
}
}
Serializable应该仅由那些稳定的类来实现。一个维护原始的序列化格式并且允许类进行演化的方法是通过serialPersistentFields来使用自定义的序列化。static和transient 标识符指定了哪些字段不应当被序列化,而serialPersistentFields字段指定了哪些字段应当被序列化。它使得类不需要在类实现中定义序列化字段,将当前实现与整个逻辑解耦。新的字段可以很容易地增加进来,而不会破坏各个版本之间的兼容性。
Do not deviate from the proper signatures of serialization methods
public class Ser implements Serializable
{
private final long serialVersionUID = 123456789;
private Ser()
{
// initialize
}
public static void writeObject(final ObjectOutputStream stream) throws IOException
{
stream.defaultWriteObject();
}
public static void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException
{
stream.defaultReadObject();
}
}
问题描述:
这段代码展示了一个使用private构造函数的Ser类,意味着这个类的外部代码不能创建这个类的实例。这个类实现了java.io.serializable接口并且定义了public的readObject()和writeObject()方法。因此,非受信代码可以使用readObject()得到重组对象,并且可以使用writeObject()写入流。
那些需要在对象序列化和反序列化中进行特殊处理的类必须使用以下的方法签名来实现特殊的方法[API2006]:
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
对于任何序列化类,这些方法要声明为private的。
解决方法:
将readObject()和writeObject()方法声明为private,并且通过将其声明为非static来限制它们的可访问性。
private void writeObject(final ObjectOutputStream stream) throws IOException
{
stream.defaultWriteObject();
}
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException
{
stream.defaultReadObject();
}
class Extendable implements Serializable
{
private Object readResolve()
{
// ...
}
private Object writeReplace()
{
// ...
}
}
class Extendable implements Serializable
{
protected static Object readResolve()
{
// ...
}
protected static Object writeReplace()
{
// ...
}
}
问题描述:
这段代码将readResolve()和writeReplace()方法声明为private。
序列化的类同样也会实现readResolve()和writeReplace()方法。序列化规范[Sun2006]中对readResolve()和writeReplace()的方法描述如下:
- 对于可序列化和可外部化的类,readResolve()方法允许一个类在返回给调用者之前替换/ 解析从流读取的对象。通过实现readResolve()方法,一个类可以直接控制它自己的反序 列化实例的类型和实例。
- 对于可序列化和可外部化的类,writeReplace()方法允许对象的类在对象写入之前在流中提名(nominate)自己的替换。通过实现writeReplace()方法,一个类可以直接控制它自己的序列化实例的类型和实例。
可以为readResolve()和writeReplace()方法加入任何访问指定符。然而,如果这些方法被声明为private,那么扩展类就不能调用和覆写他们。同样,如果这些类被声明为静态的,扩展类也不能覆写这些方法,它们只能将其隐藏。
偏离这些方法签名,会导致在序列化或者反序列化的时候这些方法不会被调用。这样的方法,特别是当其在被声明为public的时候,则可能会被非受信代码访问。
不像其他的接口,Serializable没有定义需要的接口方法签名。接口仅允许公有的字段和方法,而readObject()、readObjectNoData()和writeObject()方法必须要声明为私有。同样,Serializable不能防止readResolve()和writeReplace()接口被声明为static、public或者private。因此,Java的序列化机制不能让编译器识别出任何这些方法的不正确的方法签名。
解决方法:
将readResolve()和writeReplace()方法声明非static、protected,以便被子类继承。
class Extendable implements Serializable
{
protected Object readResolve()
{
// ...
}
protected Object writeReplace()
{
// ...
}
}
Do not invoke overridable methods from the readObject() method
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException
{
overridableMethod();
stream.defaultReadObject();
}
public void overridableMethod()
{
// ...
}
问题描述:
readObject()方法中不能调用任何可覆写方法。
因为父类的反序列化发生在子类反序列化之前,所以从readObject()方法中调用可覆写方法会允许覆写方法可以读取子类被完全创建之前的状态。
解决方法:
从readObject()方法中移除对可覆写方法的调用。
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException
{
stream.defaultReadObject();
}
Platform Security
Do not use reflection to increase accessibility of classes, methods, or fields
class FieldExample
{
private int i = 3;
private int j = 4;
public String toString()
{
return "FieldExample: i=" + i + ", j=" + j;
}
public void zeroI()
{
this.i = 0;
}
public void zeroField(String fieldName)
{
try
{
Field f = this.getClass().getDeclaredField(fieldName);
// Subsequent access to field f passes language access checks
// because zeroField() could have accessed the field via
// ordinary field references
f.setInt(this, 0);
// Log appropriately or throw sanitized exception; see EXC06-J
}
catch (NoSuchFieldException ex)
{
// Report to handler
}
catch (IllegalAccessException ex)
{
// Report to handler
}
}
public static void main(String[] args)
{
FieldExample fe = new FieldExample();
System.out.println(fe.toString());
for (String arg : args)
{
fe.zeroField(arg);
System.out.println(fe.toString());
}
}
}
问题描述:
在这段代码中,私有的字段i和j可以使用反射通过一个Field对象来修改。此外,任何
类也都可以使用反射通过zeroField()方法来修改这些字段。
反射不应当用来提供对类、方法和字段的访问,除非这些类、方法和数据成员本身已经不 通过反射就可以访问。例如,使用反射来访问或修改字段是不允许的,除非那些字段本身就可以被其他的方式访问和修改,例如通过getter和setter方法。
解决方法:
有两种合规方案:
- 当你需要使用反射时,需确认直接的调用者通过声明自己为private和final,从而不会被恶意代码调用。
- 当一个类必须要使用反射来对字段进行访问的时候,它同时必须使用一个非反射的接口来进行同样访问。
class FieldExample
{
// ...
private void zeroField(String fieldName)
{
// ...
}
}
class FieldExample
{
// ...
public void zeroField(String fieldName)
{
// ...
}
public void zeroI()
{
this.i = 0;
}
public void zeroJ()
{
this.j = 0;
}
}
Runtime Environment
Do not trust the values of environment variables
String username = System.getenv("USER");
问题描述:
这段代码试图使用一个环境变量获取用户名。
首先,这会产生可移植性问题。例如,Windows提供的用户名是一个称为USERNAME的环境变量,而UNIX可能是USER、LOGNAME或两者兼而有之。其次,攻击者可以执行这个程序将用户环境变量设置为任何值。
因为环境变量对所有程序可见,所以他们的影响更大, 而不仅仅是对当前的Java程序。它们可能有细微不同的语义,如在不同的操作系统中是否区分大小写。因此,环境变量更容易产生意想不到的副作用。如果可能最好使用系统属性。
解决方法:
使用user.name系统参数获取用户名。Java虚拟机(JVM)基于这个系统参数来初始化正确的用户名,即使当USER环境变量被设置为不正确的值或没有设置。
String username = System.getProperty("user.name");
Java Native Interface
Miscellaneous
Do not use an empty infinite loop
public int nop()
{
while (true)
{
}
}
问题描述:
这段代码实现了一个闲置的任务,这个任务会连续地执行一个循环,却在这个循环中不执行任何指令。一个优化编译器或JIT可能会去掉这个while循环。
解决方法:
有两种合规方案:
- 在while循环中调用thread.sleep()。
- 在while循环中调用thread.yield()。
循环体包含语义上有意义的操作,因此不会被优化掉。
public final int DURATION=10000; // In milliseconds
public void nop() throws InterruptedException
{
while (true)
{
// Useful operations
Thread.sleep(DURATION);
}
}
public void nop() throws InterruptedException
{
while (true)
{
Thread.yield();
}
}
Generate strong random numbers
import java.util.Random;
// ...
Random number = new Random(123L);
//...
for (int i = 0; i < 20; i++)
{
// Generate another random integer in the range [0, 20]
int n = number.nextInt(21);
System.out.println(n);
}
问题描述:
这段代码使用了java.util.Random类。这个类为每一个种子值产生相同的序列。因此,
数字的序列是可以预测的。
伪随机数生成器(Pseudorandom Number Generator,PRNG)使用确定的数学算法产生一个具有良好统计特性的数字序列。然而,生成的数字序列并不具有真正的随机性。PRNG通常开始于一个算术种子值,算法使用这个种子生成一个输出值和一个新的种子,新的种子用于生成下一个值,如此循环。
Java API中提供了java.util.Random类实现PRNG。如果两个java.util.Random类的实例使用相同的种子,会在所有Java实现中生成相同的数字序列。
攻击者可以通过侦察一些易受攻击的目标获得种子的值,可以构建一个查找表估算未来的种子值。因此,java.util.Random类不能用于安全敏感的应用程序或保护敏感数据。使用更安全的随机数发生器,如java.security.SecureRandom类。
解决方法:
有两种合规方案:
- 使用java.security.SecureRandom类生产高质量的随机数。
- 使用Java 8中引入的SecureRandom.getInstanceStrong()方法,使用加强的RNG算法。
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
// ...
public static void main (String args[])
{
SecureRandom number = new SecureRandom();
// Generate 20 integers 0..20
for (int i = 0; i < 20; i++)
{
System.out.println(number.nextInt(21));
}
}
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public static void main (String args[])
{
try
{
SecureRandom number = SecureRandom.getInstanceStrong();
// Generate 20 integers 0..20
for (int i = 0; i < 20; i++)
{
System.out.println(number.nextInt(21));
}
}
catch (NoSuchAlgorithmException nsae)
{
// Forward to handler
}
}
Do not leak memory
public class Leak
{
static Vector vector = new Vector();
public void useVector(int count)
{
for (int n = 0; n < count; n++)
{
vector.add(Integer.toString(n));
}
// ...
for (int n = count - 1; n > 0; n--)
{ // Free the memory
vector.removeElementAt(n);
}
}
public static void main(String[] args) throws IOException
{
Leak le = new Leak();
int i = 1;
while (true)
{
System.out.println("Iteration: " + i);
le.useVector(1);
i++;
}
}
}
问题描述:
在这段代码中,vector对象会产生内存泄漏。删除vector中的元素的条件被错误的写成“n > 0”而不是“n >= 0”。因此,每次调用这个方法都少删除了一个元素,导致可用堆空间很快被耗尽。
解决方法:
有两种合规方案:
- 改变循环条件为n >= 0纠正错误。将清理代码封装在finally块中,即使代码会抛出异常仍然会执行。
- 如有可能,更倾向于使用标准语言的语义。使用vector.clear()方法删除所有的元素。
public void useVector(int count)
{
int n = 0;
try
{
for (; n < count; n++)
{
vector.add(Integer.toString(n));
}
// ...
}
finally
{
for (n = n - 1; n >= 0; n--)
{
vector.removeElementAt(n);
}
}
}
public void useVector(int count)
{
try
{
for (int n = 0; n < count; n++)
{
vector.add(Integer.toString(n));
}
// ...
}
finally
{
vector.clear(); // Clear the vector
}
}
Do not modify the underlying collection when an iteration is in progress
class BadIterate
{
public static void main(String[] args)
{
List<String> list = new ArrayList<String>();
list.add("one");
list.add("two");
Iterator iter = list.iterator();
while (iter.hasNext())
{
String s = (String)iter.next();
if (s.equals("one"))
{
list.remove(s);
}
}
}
}
问题描述:
这段代码使用ArrayList的remove()方法从一个ArrayList中删除一个元素,同时迭代遍历ArrayList,产生的行为是未知。
解决方法:
Iterator.remove()方法删除迭代器从底层集合返回的最后一个元素,其行为完全是确定的,所以它可以在遍历一个集合时被安全的调用。
// ...
if (s.equals("one"))
{
iter.remove();
}
// ...