假设线程A将某个值写入到了字段x中,而线程B读取到了该值。称其为“线程A向x的写值对线程B是可见的”。“是否是可见的”这个性质就称为可见性。
在单线程程序中,无需在意可见性。这是因为,线程总是可以看见自己写入到字段中的值。
但是,在多线程程序中必须注意可见性。这是因为,如果没有使用synchronized或volatile正确地进行同步,线程A写入到字段中的值可能并不会立即对线程B可见。
示例
Java内存模型可能会导致Runner线程永远在while循环中不停地循环。也就是说,代码中的程序可能会失去生存性。
1 | public class Main { |
原因是,向字段quit写值的线程(主线程)与读取字段quit的线程(Runner)是不同的线程。主线程向quit写入的true这个值可能对Runner线程永远不可见。
如果以“缓存”的思路来理解不可见的原因可能会有助于理解。主线程向quit写入的这个true值可能只是被保存在主线程的缓存中。而Runner线程从quit读取到的值,仍然是在Runner线程的缓存中保存着的值false,并没有任何变化。
代码是未正确同步的程序。不过如果将quit声明为volatile字段,就可以实现正确同步的程序。
共享内存与操作
在Java内存模型中,线程A写入的值并不一定会立即对线程B可见。下图展示了线程A和线程B通过字段进行数据交互的情形。
共享内存(shared memory)是所有线程共享的存储空间,也被称为堆内存(heap memory)。因为实例会被全部保存在共享内存中,所以实例中的字段也存在于共享内存中。此外,数组的元素也被保存在共享内存中。也就是说,可以使用new在共享内存中分配存储空间。
局部变量不会被保存在共享内存中。通常,除局部变量外,方法的形参、catch语句块中编写的异常处理器的参数等也不会被保存在共享内存中,而是被保存在各个线程持有的栈中。正是由于它们没有被保存在共享内存中,所以其他线程不会访问它们。
在Java内存模型中,只有可以被多个线程访问的共享内存才会发生问题。
上图一共展示了以下6种操作:
(1) normal read操作
(2) normal write操作
(3) volatile read操作
(4) volatile write操作
(5) lock操作
(6) unlock操作
这里,(3)~(6)的操作是进行同步的同步操作。进行同步的操作具有防止重排序,控制可见性的效果。
normal read/normal write操作表示的是对普通字段(volatile以外的字段)的读写。这些操作是通过缓存来执行的。因此,通过normal read读取到的值并不一定是最新的值,通过normal write写入的值也不一定会立即对其他线程可见。
volatile read/volatile write操作表示的是对volatile字段的读写。由于这些操作并不是涌过缓存来执行的,所以通过volatile read读取到的值一定是最新的值,通过volatile write写入的值也会立即对其他线程可见。
lock/unlock操作是当程序中使用了synchronized关键字时进行互斥处理的操作,lock操作可以获取实例的锁,unlock操作可以释放实例的锁。
之所以在normal read/normal write操作中使用缓存,是为了提高性能。