先介绍下多进程多线程在linux几种通信方式
java设计上则是基于共享内存来实现进程,线程的通信
JAVA内存屏障指令 | 作用描述 |
---|---|
Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存),先于Store2及所有后续存储指令的存储。 |
Load1;LoadStore;Store2 | 确保Load1数据装载先于Store2及所有后续存储指令的存储。 |
Store1;StoreLoad;Load2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Load2及所有后续装载指令的装载。 |
Load1;LoadLoad;Load2 | 确保Load1数据的装载,先于Load2及所有后续装载指令的装载。 |
特殊的是StoreLoad,会使该屏障之前的所有内存访问指令(装载和存储指令)完成之后,才执行该屏障之后的内存访问指令;是一个”全能型”的屏障,它同时具有其他三个屏障的效果
class Count{
int a = 0;
public synchronized void writer(){// 1
a++; //2
} //3
public synchronized void reader(){// 4
int i = a; //5
} //6
}
volatile Object instance;
instance = new Object();
//相应汇编代码
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
0x01a3de24: lock addl $0×0,(%esp)
,其实就是插入了内存屏障导致的结果,lock表示volatile变量写时被缓存锁定了(MESI协议),作用如下int a = 0; volatile boolean v = false;
线程A
a = 1; //1
v = true; //2
线程B
v = true; //3
System.out.println(a);//4
7.1 final写内存语义:
public class Example {
int i; //普通类型
final int j; // 引用类型
public Example () { // 构造函数
i = 0; j = 1;
}
public static void writer () { // 写线程A执行
obj = new Example ();
}
public static void reader () { // 读线程B执行
Example object = obj; // 读对象引用
int a = object.i; // 读普通域
int b = object.j; // 读final域
}
}
7.2 final读内存语义
7.3 当使用final修饰引用对象或者数组时,final只保证在构造器返回之前对引用对象的操作先于构造器返回之后的操作
public class Example {
final int[] intArray; // intArray 是引用类型
public Example () { // 构造函数
intArray = new int[1];
intArray[0] = 1; //此操作对获取该对象引用的线程是可见的
}
}
public class Instance { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (Instance.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
} // 8
} // 9
return instance; // 10
} // 11
}
代码第7行instance=new Singleton();
创建了一个对象。这一行代码可以分解为如下的3行伪代码
memory = allocate(); // A1:分配对象的内存空间
ctorInstance(memory); // A2:初始化对象
instance = memory; // A3:设置instance指向刚分配的内存地址
假如2和3之间重排序之后的顺序如下
memory = allocate(); // A1:分配对象的内存空间
instance = memory; //A3:instance指向刚分配的内存地址,此时对象还没有被初始化
ctorInstance(memory); // A2:初始化对象
假如发生A3、A2重排序,线程是不保障赋值和初始化对象两步骤操作结果会一起同步到主存。
因此第二个线程执行到if (instance == null);// 4:第一次检查
时,可能会得到一个刚分配的内存而没初始化的对象(此时没有加锁,锁的happens-before规则不适用)
9.2.1 在锁内使用volatile修饰instance,volatile禁止指令重排序,并且保障变量的内存可见性:private volatile static Instance instance;
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
// 这里将导致InstanceHolder类被初始化
return InstanceHolder.instance ;
}
}
9.2.2 使用类加载器的全局锁,在执行类的初始化期间,JVM会去获取一个锁;这个锁可以同步多个线程对同一个类的初始化,每个线程都会试图获取该类的全局锁去初始化类
设想变量A和B没有关联,却刚好在同一缓存行;然后A被CPU-X处理,B被CPU-Y处理;因为CPU-X对A的缓存更新而导致B的缓存失效;CPU-Y要处理B,则要读取更新后的缓存行(B实际是没被更新),造成没必要的内存读取开销。这就是伪共享
填充字节,将对应的变量填充到缓存行的大小。如下面定义的类,声明额外的属性
public final static class FilledLong {
/**value 加 p1 - p6;加对象头8个字节正好等于一缓存行的大小 */
//markWord + klass (32位机,64位是16字节) 8字节
public volatile long value = 0L; // 8字节
public long p1, p2, p3, p4, p5, p6; //48字节
}
使用jdk的注解@Contended修饰变量,jvm会自动将变量填充到缓存行的大小。注意的是需要加入启动参数 -XX:-RestrictContended