没有被正确同步的含义是什么
没有正确同步的代码对于不同的人来说可能会有不同的理解。在Java内存模型这个语义环境下,我们谈到“没有正确同步”,我们的意思是:
- 一个线程中有一个对变量的写操作
- 另外一个线程对同一个变量有读操作
- 而且写操作和读操作没有通过同步来保证顺序
当这些规则被违反的时候,我们就说这个变量上有一个“数据竞争(data race)”。一个数据竞争的程序就是一个没有正确同步的程序。
同步有几个方面的作用。最广为人知的就是互斥:
一次只有一个线程能够获得一个监视器,因此,在一个监视器上面同步意味着一旦一个线程进入到监视器保护的同步块中,其他的线程都不能进入到同一个监视器保护的块中间,直到第一个线程退出了同步块。
但是同步的含义比互斥更广。同步保证了一个线程在同步块之前或者在同步块中的一个内存写入操作以可预知的方式对其他相同监视器的线程可见。
- 当我们退出了同步块,我们就释放了这个监视器,这个监视器有刷新缓冲区到主内存的效果,因此该线程的写入操作能够为其他线程所见。
- 在我们进入一个同步块之前,我们需要获取监视器,监视器有使本地处理器缓存失效的功能,因此变量会从主存重新加载,于是其他线程对共享变量的修改对当前线程来说就变得可见了。
依据缓存来讨论同步,可能听起来这些观点仅仅会影响到多处理器的系统。但是,重排序效果能够在单一处理器上面很容易见到。对编译器来说,在获取之前或者释放之后移动你的代码是不可能的。当我们谈到在缓冲区上面进行的获取和释放操作,我们使用了简述的方式来描述大量可能的影响。
新的内存模型语义在内存操作(读取字段,写入字段,锁,解锁)以及其他线程的操作(start和join)中创建了一个部分排序,在这些操作中,一些操作被称为在其他操作之前发生(where some actions are said to happen before other operations)。当一个操作在另外一个操作之前发生,第一个操作保证能够排到前面并且对第二个操作可见。排序规则如下:
- 线程中的每个操作都发生在该线程中在线程顺序上后续的每个操作之前(Each action in a thread happens before every action in that thread that comes later in the program's order)
- 解锁一个监视器的操作发生在随后对相同监视器进行锁的操作之前(An unlock on a monitor happens before every subsequent lock on that same monitor.)
- 对volatile字段的写操作发生在后续对相同volatile字段的读取操作之前(A write to a volatile field happens before every subsequent read of that same volatile.)
- 线程上调用start()方法发生在这个线程启动后的任何操作之前(A call to start() on a thread happens before any actions in the started thread.)
- 一个线程中所有的操作都发生在从这个线程join()方法成功返回的任何其他线程之前。(All actions in a thread happen before any other thread successfully returns from a join() on that thread.)(注:意思是其他线程等待一个线程的join()方法完成,那么,这个线程的所有操作发生在其他线程中的所有操作之前)
这意味着,任何内存操作:
- 这个内存操作在退出一个同步块前对一个线程是可见的
- 对任何线程在它进入一个呗相同的监视器保护的同步块后都是可见的
因为所有内存操作发生在释放监视器之前和释放监视器发生在获取监视器之前
其他如下模式的实现被一些人用来强制实现一个内存屏障是没有用的:
synchronized (new Object()){}
这段代码其实不会执行任何操作,编译器会把它完全移除掉,因为编译器知道没有其他的线程会使用相同的监视器进行同步。要看到其他线程的结果,你必须为一个线程建立happens before关系。
重点注意
对两个线程来说,为了正确建立happends before关系而在相同监视器上面进行同步是非常重要的。以下观点是错误的:当线程A在对象X上面同步的时候,所有东西对线程A可见,线程B在对象Y上面进行同步的时候,所有东西对线程B也是可见的。释放监视器和获取监视器必须匹配。(也就是说要在相同的监视器上面完成这两个操作),否则,代码就会存在“数据竞争”。