MENU

Java - 并发的小知识

July 9, 2020 • Read: 2046 • 后端

1. Java并发知识

1.1 共享变量的内存可见性

Java内存模型规定,将所有的变量都存放在主内存,当线程使用变量时,会把主内存里的变量赋值到自己的工作内存(也叫工作空间),线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

JMM.png

由于自身存在工作内存,导致一个线程操作共享变量的结果对另一个线程不可见,这就是内存的不可见问题。

1.2 synchronized关键字

synchronized块是Java提供的一种原子性内置锁,是一种排它锁,也是可重入锁。线程执行进入synchronized代码块前会自动获取内部锁,其他线程访问改代码块就会被阻塞。由于Java线程与操作系统的原生线程是一一对应的,所以阻塞会导致从用户态切换到内核态执行阻塞,这是很耗时的操作,而synchronized的使用就会导致上下文切换。

1.3 volatile关键字

使用锁可以解决共享变量的内存可见性问题,但是锁太笨重,会导致线程上下文切换和线程的重新调度的开销,所以Java还提供了弱形式的同步,就是使用volatile关键字,其保证写入的值会立刻刷新回主内存,读取共享变量时,也会重新从主内存获取最新值。但是volatile关键字并不保证操作的原子性。因为获取-计算-写入三步操作,这三步操作不是原子性的。

  • volatile只保证可见性,不保证操作的原子性
  • volatile变量禁止指令重排序优化
例子
private volatile int value;
public void inc() {
    // 这条语句编译成汇编就有4条语句,更不要说具体的机器码指令
    ++value
}
/**
* getfield 这条指令取过来是保证value的值是正确的
* lconst_1 
* ladd 执行这条和上条指令的时候,可能value的值被其他的线程修改了,这样栈顶的数据就是过期的数据
* putfield 可能就把较小的value,同步回主内存
*/

1.4 伪共享

什么是伪共享

由于CPU与主内存的运行速度差问题,所以会在CPU和主内存之间添加一级或多级高速缓冲存储器(Cache),在Cache内部是按行存储的,每一行叫做Cache行。Cache行是Cache与主内存进行数据交换的单位。

当CPU访问某个变量时,首先回去Cache行里面是否有对应的变量,有则直接取出,否则就去主内存去获取该变量,然后把该变量所在区域的一个Cache行大小复制到Cache中。由于存放到Cache行的是一个内存块,所以可能把多个变量放到一个Cache行中。当多个线程同时修改一个缓存航里面的多个变量时,由于一个Cache行同时只能有一个线程操作,所以相比将每个变量放到单独一个缓存行,性能就会下降,这就是伪共享

为何出现伪共享

由于Cache和内存交换数据的单位是缓存行,当CPU访问的变量没有在缓存行找到时,根据程序运行的局部性原理(空间、时间局部性),所以同一个程序的变量可能被放在同一个缓存行。

注意:在多线程下访问同一个缓存行的多个变量时才会出现伪共享

如何避免伪共享

JDK 8之前一般通过字节填充来避免。

// 假设一个缓存行的大小刚好64字节
// 由于类对象的的字节码对象头占用8字节,加上这7*8字节,共64字节。
public final class Test {
     public volatile long value = 0L;
     public long p1, p2, p3, p4, p5, p6;
}

JDK 8提供了sun.isc.Contended注解,它是rt包下的一个注解,默认情况只用于Java核心类,如果用户类路径下需要使用,则需要添加JVM参数: -XX:-RestrictContended。

比如在LongAdder类,由于里面有个volatile修饰的Cell类数组,由于原子性数组的元素的内存地址连续的,所以多个元素经常共享缓存行。Cell类就用了这个注解修饰:@sun.isc.Contended

@sun.misc.Contended static final class Cell{
    ....
}

参考资料

Java并发编程之美(书籍)

Last Modified: October 8, 2020
Leave a Comment