我计划将线程安全相关问题用一个系列来记录一下。 其中包括线程安全,Java内存模型,对象共享,显示锁代码分析。这里偏重于记录核心概念股。 首先给出参考内容
什么是线程安全
对线程并行有一定了解的,肯定会有一个关于线程安全的概念。例如:多线程情况下程序执行无需额外动作,线程切换不会出现异常等等,诸如此类的理解。这些描述都没错,却不够精准,更没有指导意义,所以能算是定义。精准的线程安全的定义应该是: 当多个线程访问某个类时,这个类始终表现出正确的行为。其中「正确的行为」是指某个类的行为和规范完全一致。
按照这个定义来看,单线程肯定是线程安全的。另外类如果没有状态改变,那么多线程访问可以做到线程安全,所以无状态对象一定也线程安全
如何做到线程安全
在编码的过程,考虑当前自己写类,是否线程安全的时候,那么就考察是否做到了下面三个方面。
- 原子性(竞态条件)
- 可见性
- 有序性
下面详细介绍,以及如何做到 ###原子性
看下面的简单代码
public class Count{
private int value
public void increase(){
value++;
}
}
这个类肯定不是线程安全的。因为多个线程在调用increase方法,时候value的值在加加的计算过程中,其结果是依赖之前的状态的。这个时候就产生了竞态条件了。当执行的时候,可以依赖的状态已经被改变了,那么这个结果肯定错误了。一种常见的竞态条件类型就是:先检查后执行。例如下面的代码
if(condition){
dosomething()
}
其中的condition比方说是连接的状态,可能在dosomething的之前是处于可用的状态,就在成功过了条件判断后,准备dosomething的时候,连接却可能被其它线程关闭,这样很容易就出现异常了。这个就是典型的竞态条件。所有这里conditon与dosomething必须是紧密联系的,是为一体的,也就是所谓的原子性。
可见性
可见性的错误一般都很违背直觉。对于原子性来说,我们要防止某个线程在使用对象状态的时候,而另外一个线程在改变该状态。而对于可见性而言,我们是希望某个线程改变的状态,其它线程能够看到。
说到可见性,很难避免要了解CPU和内存模型。由于现在多核心甚至多CPU越来越普及,每个核心都有自己的缓存,为了加快计算速度。

从图中可以看出,一个核心倘若缓存了一个数据,那么没有一个告知机制的话,很容易就使用了失效数据。所有需要加锁来做到可见性

上图是非常经典的一副说明可见性的图。当线程A执行同步代码后,线程B随后进入由同一个锁保护同步代码。这种情况可以保证,在锁释放之前,A线程改变的变量值可以被B线程看到。这里简单说一下原来,进入同步代码块会失效当前cache重新从主存中读取,退出同步代码会将cache写入主存。那么这里有个思考,既然如此,是否意味随便来一个同步锁都能做到可见性呢?答案肯定不行,否则上面不会强调是同一个锁了。下面介绍锁实现的时候会详细讲解
通过volatile,锁和final可以保证可见性。对于volatile,java内存模型是通过修改变量后将新值同步回主内存,在读取变量前从主内存刷新变量值来做到可见性的。锁实现可见性,会在下面详细介绍。final能做到可见性是因为,被final修饰的字段,在构造器中一旦构造完成,那么其它线程就能看到final字段的值,当然这里必须保证是完全构造没有逃逸的情况。
有序性
有序性问题主要由「有指令重排序」和「工作内存和主内存同步延迟」引起的。指令重排序与可见性类似,是为了加快执行速度,编译器会按照一定规则来改变代码执行顺序。这个就可能导致多线程情况下出现问题。java中天然有序性可以认为,如果在本线程内观察,所有操作都是有序的,这里就是说「线程内表现为串行语义」。如果在一个线程中观察另外一个线程,所有操作都是无序的,这里指的就是「有序性问题」。 通过使用volatile和显示、隐式锁可以保证线程间的有序。volatile关键字包含了禁止指令重拍的语义。锁主要由「一个变量一个时刻只能有一个线程上锁」这个规则获得,它保证了线程只能串行的执行。
锁的问题
后续会详细解释显示锁的实现,这里主要是为了引出问题。
对于原子性和有序性,我没有太多的疑问。当时对于可见性,我还是有不少疑问的。其中两个困扰了我很久,第一个就是显示锁如何保证可见性的,第二个就是上面提到的问题,既然同步代码块可以失效和重读缓存,那么还为什么要同一个锁呢?经过查了很多资料,最终解决了我的疑惑。下面分别说一下
显示锁可见性的实现
我有这个疑惑的根本原因是我看看reenterlock的代码时候,发现实现清晰易懂,对于原子性和有序性的保证都能够推断得出。可是可见性却怎么不可见呢?反复看代码结合一些资料,猜测唯一可能就是在unsafe的CAS方法中。于是看了源码,果不其然。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
可以看到最终调用Atomic类的cmpxchg方法,继续往下追
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
在这里可以看到是用嵌入的汇编实现的, 有个CPU指令LOCK_IF_MP特别显眼,查了INTEL手册得知这个是如果多线程那么就上锁,而LOCK前缀操作会进行缓存写入主存,以及失效其它核心缓存的操作。于是焕然大悟。我相信隐式锁也是类似操作做的可见性的,但是由于隐私锁在字节码中可以看到MONITORENTER和MONITOREXIT的操作便没有深究罢了。未知是探究最大的动力
是个锁就能保证可见性吗?
答案肯定是不行的。虽然锁可以做到失效重载缓存的事情。但是没有办法做到正确的可见。对A上锁,你只能看到获得A之前的改变。相应的对B上锁,你能看到B之前的改变。那么如果你对B上锁,是否能看到A锁的改变呢?准确来说,如果能保证A锁释放,肯定发生在获得B锁时候,那么按道理来说应该是可以看到的。但实际上这个要怎么保证呢?那不就是对A上锁嘛!换句话说获得任何一个锁的时刻,可以看到这个是空前释放锁的改变值。其中这个就是使用volatile达到线程安全里面很重要的一点「状态改变不依赖之前的状态」,这里也有点像原子性啊。当然这里最主要涉及的是jvm模型中一个很重要概念:「先行发生(happens-before)」。这个下面肯定会在JVM内存模型中详细讲解