JMM模型:每个线程有一个本地内存,共享变量存储在主内存中。
Volatile:将一个变量声明为Volatile可以保证变量的可见性。即如果线程A写了一个变量的值,它可能只是写到了本地内存,而当线程B去读这个变量的时候,可能会读取不到刚刚线程A写的值。而如果将变量声明为Volatile,则可以在写的同时将本地内存中的共享变量值全部刷新到主内存中。这样线程B就可以读取到A线程写的值了。具体如下:
Volatile的内存语义:
当写一个Volatile变量时,JMM会把本地内存中的共享变量值刷新到主内存。
当读一个Volatile变量时,JMM会把线程对应的本地内存置为无效,然后从主内存中读取共享变量。
Sychronized锁也有同样的内存语义:
线程释放锁时,JMM会把本地内存中的共享变量值刷新到主内存。
线程获取锁时,JMM会把线程对应的本地内存置为无效,然后从主内存中读取共享变量。
同样的,ReentrantLock是通过Volatile变量实现的,因此也有同样的内存语义。CAS同时具有Volatile读和写的内存语义。
final域:线程A在构造函数中给一个普通变量的赋值,线程B随后读取这个对象引用并读取对象的普通变量的值。由于重排序规则,对普通变量的赋值可能出现在线程B读取对象引用和读取对象普通变量的值的后面。这样线程A所做的赋值操作对线程B就是不可见的。而如果在构造函数中对一个被声明为了final的变量赋值,则对它的赋值不允许被重排序到构造函数外,即线程B总是能读取到线程A对它赋予的值。
写final域的规则:JMM禁止编译器把final域的写重排序到构造函数之外。这个规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确的初始化了。
读final域的规则:在一个线程中,初次读取对象引用与初次读取对象包含的final域,这俩个操作不能被重排序。
对于final域为引用类型,对final域里面的变量在构造函数内的赋值也不能被重排序到构造函数外。
对一个类进行实例化有三个步骤:
1:分配对象的内存空间,2:初始化对象,3:设置引用指向内存空间
其中2和3是可以重排序的。但如果将其声明为Volatile变量,则可以禁止2和3的重排序。
如下程序是不安全的:
public class Instance{
private static Instance instance;
public static Instance getInstance(){
if(instance==null){
synchronized(Instance.class){
if(instance==null){
instance=new Instance();
}
}
}
return instance;
}
}
如下程序是安全的:
public class Instance{
private volatile static Instance instance;
public static Instance getInstance(){
if(instance==null){
synchronized(Instance.class){
if(instance==null){
instance=new Instance();
}
}
}
return instance;
}
}
java语言中的线程安全(根据安全程度排序):
1:不可变:对于被final域修饰的对象而言,只要其被构造出来了,则其外部可见状态永远不会改变。
2:绝对线程安全:不管运行时环境如何,都不需要任何同步措施。
3:相对线程安全:这是我们通常意义上的线程安全,需要保证这个对象单独的操作时线程安全的,我们在调用的时候不需要额外的保障措施。但对于一些特定顺序的连续调用,可能需要额外的同步手段。
4:线程兼容:对象本身不安全,可以通过额外同步手段来使用。
5:线程队列:怎么样都不可能线程安全。