“ 在深入理解Java虚拟机一书的高效并发部分中提到:按照线程安全的安全程度由强至弱来排序,可以将Java语言中各种操作共享数据分为5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立(这种划分也是Brian Goetz在IBM developWorkers中发表的一篇论文提出的)。”
线程安全应该是我们在Java学习过程中听到较多的一个名称,在我自己看来线程安全就是对象在被多个线程的访问和操作的情况下,结果仍然是我们所预期的那样。但是在深入理解Java虚拟机一书中作者认为这种理解不能说不对,但是无法从中获取到任何有用的信息,作者提出《Java Concurrency In Practice》(JAVA并发编程实践)中对线程安全有一个比较恰当的定义:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。这种定义我个人看来是增加了场景和条件的描述,书中也说到这种定义比较严谨,那么在Java语言中线程安全具体是如何体现的?
01
—
不可变
在JDK1.5以后,不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要采取任何线程安全的保障措施,在书中的第12章谈到final关键字带来的可见性时提到这一点:只要一个不可变的对象被正确的构建出来(没有发生this引用逃逸的情况)那其外部的可见状态永远也不会发生改变,永远也不会看到它在多个线程之中处于不一致的状态。"不可变"带来的安全性是最简单和最纯粹的。
PS:this引用逃逸指的是对象还没有构造完成,它的this引用就被发布出去了,此时会出现某些线程中看到该对象的状态是没初始化完的状态,而在另外一些线程看到的却是已经初始化完的状态。(这里推荐一个文章的链接:https://www.cnblogs.com/straybirds/p/8640748.html)
Java语言中如果共享数据是一个基本数据类型,但么只要在定义时是哟final关键字修饰它就可以保证它是不可变的,如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生影响才行。就好比String类型,调用substring()方法,replace()和concat()这些方法并不会影响他原来的值,只会返回一个新构造的字符串对象。
PS:下面是源码:我可以看到它返回的是新构造的字符串,或者是this
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
保证对象行为不影响自己状态的途径有很多种,其中最简单就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的。
个人觉得字节写不可变类的情况并不是很多,只有在写一些不希望被扩展的工具类的时候,会考虑用final,其他时候我基本上没有用过。
02
—
绝对线程安全
绝对的线程安全完全满足Brain Goetz 给出的线程安全定义,这个定义其实很严格,一个类要达到"不管运行时环境如何,调用者都不要任何额外的同步措施"通常需要付出很大的,甚至有时候是不切实际的代价。在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。比如Hashtable 或者 Vector ,我们通常会说他是线程安全的容器,但是即使它所有的方法都被修饰成同步,也不意味这调用他的时候永远都不要同步手段了。看一下测试代码:
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
});
t.start();
t1.start();
while (Thread.activeCount() > 20) ;
}
}
PS:书中测评结果是数组越界,但是我本地测试十几次这个代码,都没出现过数组越界,我以为是JDK1.8的原因,改成1.6之后也没有数组越界,百度搜索也没有想要的答案,但是大家统一认为,虽然vector的注解也说vector是线程安全的,但是实际上要真正达成线程安全,还需要以vector对象为锁,来进行操作。
同时我百度得到绝对线程安全类有:Random 、ConcurrentHashMap、Concurrent集合、atomic
03
—
相对线程安全
相对的线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用段使用额外的同步手段来保证调用的正确性。
在Java语言中,大部分的线程安全类都属于这种类型,例如Vector,HashTable,Collections的synchronizedCollection()方法包装的集合等。
04
—
线程兼容
线程兼容指的是对象本身并不是线程安全的,但是可以通过调用端正确地使用同步手段来保证对象在并发环境中可以安全的使用,我们平常说的一个类不是线程安全的,绝大多数都指的是这一种情况。Java API中大部分的类都是属于线程兼容的,比如ArrayList和HashMap等等。
PS:在平时的工作中,我们要考虑的线程安全多是这种情况,我们自己创建的类并不是线程安全的,但是在访问其共享资源的时候,我们就要考虑并发是不是会导致线程不安全,如果存在不安全,我们就需要使用一些手段保证其线程安全。
05
—
线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。由于Java语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应该尽量避免。
一个线程对立的例子是Thread类的suspend方法和resume方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试恢复线程,如果并发进行的话,无论是否进行了同步,目标线程都是存在死锁的风险,如果suspend中断的线程就是即将要执行resume的线程,肯定会发生死锁,所有这两个方法在JDK中声明废弃了。常见的线程对立的操作还有System.setIn,System.setOut,System.runFinalizersOnExit等。