重排序
- java编译器和java虚拟机会在编译或者执行程序时改变程序的执行顺序以优化程序的性能。
- 重排序的语句是没有依赖的语句。
- 单线程的程序一般不会因重排序导致程序出现运行错误,因为有很多规范对其做出了限制。
- 但是,在多线程程序中,有时会发生明显是由于重排序导致的运行错误。所以在编写多线程程序时,我们要注意多个线程对共享变量的访问,对其加以限制,比如synchronized或者volatile。
可见性
- 线程通常是将共享内存中的实例复制到独有的缓存中,对该实例的更改不能及时更新到共享内存中,即共享变量的更改对其他线程不可见。(使用缓存是为了提高性能)
- 可见性:用volatile修饰的变量,被更改后立刻会对其他线程可见。
共享内存
- 共享内存又称堆内存,可以用new在堆内存中分配存储空间,即所有实例(new出来的)都保存到堆内存中,数组元素也保存在堆内存中。
- 所有线程都可以访问堆内存中的实例。在java内存模型中,只有可以被多个线程访问的共享变量才会发生问题。所以当线程要访问共享内存中的实例时,要保证线程安全。
- 局部变量、方法的形参以及catch语句中的异常处理器的参数都保存到所属线程的专有栈空间中。所以其他线程不会访问他们。天然具有线程安全性。
- 例如,People p = new People(),其中实例引用p(实例在堆内存中的地址)保存在栈内存中,但new People()这个实例对象保存在堆内存中。这样分开保存也有利于java的内存回收,比如,当p指向新的引用时,老的new People()就可以被回收。
synchronized
synchronized具有“线程的互斥处理”和“同步处理”两种功能。
- 线程的互斥处理:只有一个线程能够获得实例的锁,即只有一个线程能够访问被synchronized修饰的方法。
- 同步处理:线程A在进行unlock M操作之前的所有写入操作对下一个lock M的线程B都是可见的。
- 线程在unlock操作之前,写入缓存中的内容会被强制地写入到共享内存中
- 线程在lock操作之前会让缓存中的内容失效,然后共享内存中的最新内容会被强制重新读取到缓存中。
- 总结:只要被synchronized保护起来的可以被多个线程读写的共享字段,就可以避免这些共享字段受到重排序和可见性的影响。
volatile
volatile具有“同步处理”和“对long和double的原子操作”两种功能
- 同步处理:某个线程对volatile字段的写操作立即对其他线程可见。
- 对volatile字段赋值的语句的位置很重要。
- volatile不会对线程进行互斥处理。
- 访问volatile字段会产生性能开销
- 对long和double的原子操作:java规范无法确保对long或者double的赋值操作的原子性。但当它们被volatile修饰后,就可以保证赋值操作的原子性。
final
- final字段只能被初始化一次,声明时or构造函数中。
- 在构造函数处理完成之前,可能会看到final字段的值是默认的初始值(0,false或者null)。
- final字段初始化之后,任何时候,他的值对其他线程都是可见的。
- 将常量字段声明为final,java内存模型可以确保final字段在构造函数执行结束之后可以被正确地看到,不需要再通过synchronized或volatile同步。