线程安全—相关介绍
线程安全?
《Java并发编程实战(Java Concurrency In Practice)》的作者Brian Goetz为“线程安全”做出了一个比较恰当的定义:“当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。
《Java并发编程之美》的作者翟陆续的定义如下:"线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题",其中共享资源是指该资源被多个线程所持有或者说多个线程都可以去访问该资源。
出现线程不安全的原因是什么?
多个线程之间存在着共享数据,那么就有可能出现线程的安全问题。是不是说多个线程共享了资源,都会产生线程安全问题呢?答案是否定的,如果多个线程都是只读取共享资源,而不去修改,那么就不会存在线程安全问题。只有当至少一个线程修改共享资源时候才会存在线程安全问题。
Java语言中的线程安全
在Java语言中,线程安全具体是如何体现的?有哪些操作是线程安全的?按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
在Java语言里面(特指JDK 5以后,即Java内存模型被修正之后的Java语言),不可变 (Immutable)的对象一定是线程安全的。Java语言中,如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰 它就可以保证它是不可变的。如果共享数据是一个对象,由于Java语言目前暂时还没有提供值类型的 支持,那就需要对象自行保证其行为不会对其状态产生任何影响才行,不妨类比java.lang.String类的对象实例,它是一个典型的不可变对象,用户调用它的 substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
保证对象行为不影响自己状态的途径有很多种,最简单的一种就是把对象里面带有状态的变量都 声明为final,这样在构造函数结束之后,它就是不可变的,例如java.lang.Integer 构造函数,它通过将内部状态变量value定义为final来保障状态不变。
在Java类库API中符合不可变要求的类型,除了上面提到的String之外,常用的还有枚举类型及 java.lang.Number的部分子类(AtomicInteger和AtomicLong是可变的),如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。
2.绝对线程安全
我们可以通过Java API中一个不是“绝对线程安全”的“线程安全类型”来看看这个语境里的“绝对”究竟是什么 意思。 如果说java.util.Vector是一个线程安全的容器,相信所有的Java程序员对此都不会有异议,因为它的add()、get()和size()等方法都是被synchronized修饰的,尽管这样效率不高,但保证了具备原子性、 可见性和有序性。不过,即使它所有的方法都被修饰成synchronized,也不意味着调用它的时候就永远都不再需要同步手段了。具体示例如下
运行结果如下
很明显,尽管这里使用到的Vector的get()、remove()和size()方法都是同步的,但是在多线程的环境 中,如果不在方法调用端做额外的同步措施,使用这段代码仍然是不安全的。因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号i已经不再可用,再用i访问数组就会抛出一个 ArrayIndexOutOfBoundsException异常。如果要保证这段代码能正确执行下去,我们不得不把 removeThread和printThread的定义代码这样。假如Vector一定要做到绝对的线程安全,那就必须在它内部维护一组一致性的快照访问才行,每次对其中元素进行改动都要产生新的快照,这样要付出的时间和空间成本都是非常大的。
3.相对线程安全
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安 全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需 要在调用端使用额外的同步手段来保证调用的正确性。 在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包装的集合等。示例同上。
4.线程兼容
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对 象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。Java类 库API中大部分的类都是线程兼容的,如与前面的Vector和HashTable相对应的集合类ArrayList和 HashMap等。
5.线程对立
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java 语言天生就支持多线程的特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害 的,应当尽量避免。 一个线程对立的例子是Thread类的suspend()和resume()方法。如果有两个线程同时持有一个线程对 象,一个尝试去中断线程,一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同 步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,那就 肯定要产生死锁了。也正是这个原因,suspend()和resume()方法都已经被声明废弃了。常见的线程对立 的操作还有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。