一、线程同步是什么?
资源共享的两个原因是资源紧缺和共建需求。线程共享CPU 是从资源紧缺的维度来考虑的,而多线程共享同一变量,通常是从共建需求的维度来考虑的。
在多个线程对同一变量进行写操作时,如果操作没有原子性,就可能产生脏数据。 所谓原子性是指不可分割的一系列操作指令,在执行完毕前不会被任何其他操作中断,要么全部执行,要么全部不执行。如果每个线程的修改都是原子操作,就不存在线程同步问题。有些看似非常简单的操作其实不具备具备原子性,典型的就是i++ 操作,它需要分为三步,即 ILOAD → IINC → ISTORE 。另一方面,更加复杂的CAS (Compare And Swap) 操作却具有原子性。
计算机的线程同步,就是线程之间按某种机制协调先后次序执行,当有一个线程在对内进行操作时,其他线程都不可以对这个内存地址进行操作,知道该线程完成操作。 实现线程同步方式有很多,比如同步方法、锁阻塞队列等。
二、Volatile
从happen before 了解线程操作的可见性。把happen before 定义为方法hb(a,b),表示a happen before b。如果hb(a,b)且hb(b,c),能够推导出hb(a,c)。
CPU 在处理信息时会进行指令优化,分析哪些区数据动作可以合并进行,哪些存数据动作可以合并进行。CPU 拜访一趟遥远的内存,一定会到处看看,是否可以存取合并,以提高执行效率。指令重排示例代码如下:
@Override
public void run(){
(第1处)
int x =1;
int y = 2;
int z = 3;
(第2处)
x = x + 1;
(第3处)
int sum = x +y + z;
}
happen before 是时钟顺时针的先后,并不能保证线程交互的可见性,在第2处和第3处都是写操作,不会进行指令重排,但是前三行时不互斥的,并且第1处的操作如果放在z=3 赋值操作之后,明显是效率最大化的处理方式. 所以指令重排的最大可能使把第1处和第2处串联依次执行.
happen before 并不能保证线程交互的可见性.那么什么是可见性呢? 可见性是指 某些线程修改共享变量的指令对其他线程来说都是可见的,它反映的是指令执行的实时透明度.
每个线程都有独占的内存区域,如操作栈、本地变量表等。线程本地内存保存了引用变量在堆内存中的副本,线程对变量的所有操作都在本地内存区域中进行,执行结束后再同步到堆内存中去。这里必然有一个时间差,在这个时间差内,该线程对副本的操作,对于其他线程都是不可见的。
当使用volatile 修饰变量时,意味着任何对此变量的操作都会在内存中进行,不会产生副本,以保障共享变量的可见性,局部阻止了指令重排的发生。由此可知,在使用单例设计模式时,即使用双检锁也不一定会拿到最新的数据。
如下代码在高并发场景中会存在问题:
public class LazyInitDemo {
private static TransactionService service =null;
public static TransactionService getTransactionService(){
if(service == null){
synchronized (this){
if (service == null){
service = new TransactionService();
}
}
}
return service ;
}
// other methods and fields ...
}
使用者在调用getTransactionService()时,有可能会得到初始化未完成的对象。究其原因,与Java虚拟机的编译优化有关。对Java编译器而言,初始化TransectionService 实例和将对象地址写到service 字段并非原子操作,且这两个阶段的执行顺序是未定义的。 假设某个线程执行 new Transactionservice() 时,构造方法还未被调用,编译器仅仅为该对象分配了内存空间并设为默认值,此时若另一个线程调用 getTransactionService() 方法,由于service != null ,但是此时service对象还没有被赋予真正有效的值,从而无法取到正确的service 单例对象. 这就是著名的双重检查锁定(Double-checked Locking) 问题,对象引用在没有同步的情况下进行读操作,导致用户可能会获取为构造完成的对象.对于此问题,一种较为简单的解决方案是用volatile 关键字修饰目标属性(适用于JDK5 及以上版本) ,这样service就限制了编译器对它的相关读写操作,对它的读写操作进行指令重排,确定对象实例化之后才返回引用.
锁也可以确保变量的可见性,但是实现方式和volatile 略有不同,线程在得到锁时读入副本,释放时