黑马-----内存模型和volatile详解

时间:2024-01-04 17:52:38

黑马程序员:Java培训、Android培训、iOS培训、.Net培训

JAVA线程-内存模型和volatile详解

一、单核内存模型

黑马-----内存模型和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来弥补。

二、多核内存模型

黑马-----内存模型和volatile详解

有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、访问变量不需要加锁