黑马程序员:Java培训、Android培训、iOS培训、.Net培训
JAVA线程-内存模型和volatile详解
一、单核内存模型
1、程序运行时,将临时数据存放到Cache中
2、将CPU计算所需要的数据从Cache中拷贝一份到H Cache中
3、CPU直接从H Cache中读取数据进行计算
4、CPU将计算的结果写入H Cache中
5、H Cache将最新的结果值涮入Cache中(何时写入不确定)
6、将Cache中结果数据写回程序(如果有需要,例如文件、数据库)
需要H Cache的原因:CPU的执行速度很快,而向Cache读取或写入数据则相对慢得多,因此,就需要H Cache来弥补。
二、多核内存模型
有2个线程:ThreadA和ThreadB,分别在不同的CUP内运行,并且执行如下代码:
i = 0; i = i + 1;。最后,我们希望i的值为2。
1、ThreadA和ThreadB分别读取i=0的值存入各自的H Cache中,此时H Cache中的i值都为0;ThreadA和ThreadB分别对i进行计算并得到的结果都为1;2个H Cache分别将结果写入Cache,最终,Cache中i的值为1。显然,这不是我们希望得到的结果。这就是著名的:缓存一致性问题。(对单核CPU也会出现同样的问题,只是单核CPU以线程调度的形式来分别执行)
2、缓存一致性问题的解决方法
1)总线加LOCK#锁(效率低下,不可取)
2)缓存一致性协议(这里不详述)
三、并发的三个概念
1、原子性
1)即一个操作或多个操作,要么全部执行并且执行的过程中不会被任何因素打断,要么不执行。
2)有如下代码:i = 9999999999;
假设为一个32位的变量赋值包括2个过程:为低16位赋值,为高16位赋值。现在,可能会发生:ThreadA将低16位数值写入之后,突然被中断,此时,ThreadB读取i的值就会得到错误的结果。
2、可见性
1)是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看到修改的值。
2)ThreadA和ThreadB分别执行如下代码:
ThreadA :int i = 0;
i = 10;
ThreadB :int j = i;
(1)当ThreadA执行到i=10时,首先将i的初始值加载到其CPU的H Cache中,然后赋值为10。那么,ThreadA的H Cache中i的值为10,但是,ThreadA却没有立即将H Cache中i的值马上写入Cache中。
(2)此时,ThreadB开始执行,然而Cache中i的值仍然为0,最终,不管怎样,j的值都为0。而不是我们希望j=10那样。
(3)这正是由于ThreadA对i修改后,ThreadB没有立即看到线程ThreadA对i修改的值。
3、有序性
1)即程序执行的顺序按照代码的先后顺序执行。
2)指令重排序:即处理器为了提高程序运行效率,可能会对输入代码进行优化,它不会保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
3)有如下代码:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a * a; //语句4
可能执行的顺序:1 2 3 4 或 2 1 3 4,不再可能有其它。这是因为处理器需要考虑指令之间的数据依赖性。
4)ThreadA和ThreadB分别执行如下代码:
ThreadA:context = loadContext(); //语句1
inited = ture; //语句2
ThreadB:while(!inited) sleep(); //语句3
doSomething(context); //语句4
如果处理器对ThreadA的指令进行重排,则ThreadB可能在context没有赋值的情况下执行doSomething(context),从而导致程序运行错误。
可见,原子性、可见性、有序性都不会影响单个线程对代码的执行结果。但是,会影并发执行的正确性。想要程序正确的并发执行,必须同时保证原子性、可见性、有序性。
四、Java确保并发执行的正确性
1、原子性
1)在Java中,对基本数据类型的变量的读取和赋值操作是原子性的。
2)下面哪些语句是原子操作的
x = 10; // 是原子操作
x = x + 1; // 不是:首先读取x值,在加1,最后赋值
x ++; // 不是:首先读取x值,在加1,最后赋值
y = x; // 不是:首先读取x值,然后赋值
3)如果实现更大范围的原子性,可使用synchronized和Lock来实现。
2、可见性
1)使用volatile来确保可见性:保证了修改的值会立即被更新到Cache并且使其它线程的H Cache中的volatile变量的值无效。
2)也可使用synchronized和Lock来确保可见性:保证了修改的值在锁被释放之前被更新到Cache并且使其它线程的H Cache中的相关变量的值无效。
3、有序性
1)使用volatile来确保真正的有序性:禁止对volatile变量进行指令重排序。
2)synchronized和Lock也能确保的有序性:以单线程执行同步代码块的方式来实现的。
五、volatile变量的详细论述
1、volatile关键字的两层含义:
1)保证可见性
2)禁止指令重排序,保证有序性
(1)当对volatile变量进行读取或写入操作时,在其前面对该volatile变量的操作早已完成并且结果已对当前操作可见,而当前操作的后序操作肯定还没有执行。
(2)进行指令优化时,不能将操作volatile变量前的指令放到volatile变量后执行,也不能将操作volatile变量后的指令放到volatile变量前执行。
注意:volatile关键字不保证原子性
3)例如:
x = 9; //语句1
y = 8; //语句2
volatile flag = ture; //语句3
i = 7; //语句4
k = 6; //语句5
尽管x,y,i,j之间不存在依赖性,但是,语句4和语句5不会被放到语句3之前执行,而语句1和语句2也不会被放到语句3之后执行。
可能执行的顺序有:1,2,3,4,5 或 2,1,3,4,5,或 1,2,3,5,4
或2,1,3,5,4
2、volatile与synchronized的区别
1)共同点
(1)volatile与synchronized都是同步机制的一部分
(2)都实现了可见性和有序性
2)区别
(1)synchronized实现了原子性,而volatile没有
(2)作用的对象不同:volatile作用的是变量,而synchronized作用的是语句块或方法。
(3)对线程的作用不同:volatile不会阻塞线程,而synchronized会阻塞线程,即volatile没有使用监视器,而synchronized使用了监视器。所以,volatile是一种比synchronized更轻量的弱化的同步机制。
六、volatile的正确使用
1、模式一:状态标记
1)没有使用volatile导致的并发问题
ThreadA :boolean stop = false; ThreadB :stop = ture;
while(!stop){
//A
doSomething();
}
并发可能不会正确执行:即ThreadA进入死循环
原因:1、ThreadA永远只读其H Cache中stop的值
2、ThreadB只将修改了stop的值保存到其H Cache中
3、即使ThreadA偶尔会从Cache中读取stop的值,如果Thread在A处阻
塞,而此时ThreadB修改了stop的值并且写入Cache,由于ThreadA没有看到Cache中stop值已经修改,即使重新执行,也可能会进入死循环。
2)使用volatile解决
ThreadA :boolean stop = false; ThreadB :stop = ture;
while(!stop){
//A
doSomething();
}
2、模式二:Double-check(在单例模式中的使用)
private volatile static Singleton instance;
public stratic Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){ instance = new Singleton();}
}
}
}
这是volatile与synchronized配合使用的经典案例。
3、模式三:开销较低的读-写锁策略
public class CheesyCounter{
//All mutative operations msut be done with the ‘this’lock held
@GuardedBy(“this”) private volatile int value;
public int getVulue(){return value;}
public synchronized int increment(){ return value++;}
}
1)volatile与synchronized配合使用的另一个经典案例
2)如果读操作远远超过写操作,可以结合使用锁和volatile变量来减少公共代码路径的开销,例如本例。
3)计数器必须使用synchronized来确保增量操作是原子的,同时使用volatile保证当前结果的可见性。
4)如果更新不频繁,读取的开销仅仅涉及volatile操作,这由于一个无竞争锁获取的开销。
网上有评论:本模式中value不使用volatile也能实现ThreadSave,因为increment()
使用了synchronized,真的这样吗?答案是否定的,如果ThreadA正进
行increment(),注意synchronized只实现了对increment()的互斥访问,而没有实现对value的互斥访问,而ThreadB也在进行getVulue(),那么ThreadB将会的到一个失效的value值,因为ThreadB不知道ThreadA正在对value进行修改。
4、模式四:一次性安全发布,发布不可变对象或线程安全的对象
public class Test{
public volatile FinalObject object;
public void CreateObject(){object = new FinalObject(…);} //ThreadA
public void doWork(){ //ThreadB
while(ture){ //轮询
if(object != null){doSomething(object);}
}
}
}
1)发布:使对象能够在当前线程作用域之外的代码中使用。
2)如果object引用不是一个volatile型,doWork()对object的引用可能得到一个不完全构造的的FinalObject。
3)必须注意:object本身必须是线程安全的。
4)volatile类型确保了发布形式的可见型,但如果object的状态在发布后可变,那么就需要额外的同步。
5)这个案例还展示出:在不使用阻塞的前提下,阻塞另一个线程的执行(尽管这不是真正的阻塞,但起到了阻塞的作用)。
网上评论:volatile不足以实现安全发布,原因在于object在构造过程中可能被中断。我们应当记得volatile有一个很重要的特性:禁止对volatile变量进行指令重排序。即中断指令要么在object在构造前执行,要么在object在构造后执行,不可能出现在构造过程中。
5、模式五:独立观察
public class UserManager{
public volatile String lastUser;
public boolean authenticate(String userName, String password){
boolean valid = passwordIsValid(user, password);
if(valid){
User u = new User(userName, password);
activeUsers.add(u);
lasstUser = user;
}
return valid;
}
………
}
1)本例展示了身份机制如何记忆最近一次登陆的用户的名字,并将反复使用lastUser引用来发布值,以供程序的其它部分使用。
2)该模式的另一个使用是收集程序的统计信息或定期(不定期)发布信息
3)这个模式要求:(1)被发布的值是有效不可变的-即值的状态在发布后不会更改(下一次发布已经是一个新的值,而不是在原有值的基础上的改变)
(2)使用发布值的代码需要清楚该值可能随时发生变化。
6、模式六:volatile-bean模式
@ThreadSafe
public class Person{
private volatile Stirng name;
private volatile int age;
public String getName(){return name;}
public int getAge(){return age;}
public void setName(String name){this.name = name;}
public void setAge(int age){this.age = age;}
}
1)原理:很多框架为易变数据的持有者(例如HttpSession)提供了容器,但是放入这些容器中的对象必须是线程安全的。
2)volatile-bean模式的所有成员都必须是volatile并且有效不可变,同时只能有getter和setter方法。
四、volatile变量的使用原则
1、写入变量不依赖此变量的值,或只有一个线程修改此变量
2、变量不与其它变量共同参与不变约束
3、访问变量不需要加锁