直入主题吧。
– 先说一下内存模型
cpu执行程序中,程序的临时数据是保存在主内存中的(也就是我们的内存条RAM),cpu执行指令的速度比主内存的读写速度快很多,如果数据都及时对主内存去读写操作,执行速度会大大降低,因此cpu中就有了高速缓存。当程序执行时,会将运算需要的数据从主存复制一份到cpu的高速缓存当中,那么cpu进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
例如:i = i+1; 当cpu执行这段代码的时候,先从主内存中拷贝i的值到高速缓存中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主内存中。
- 抛出一个问题:
1.假设电脑两个cpu核心,每个核心创建一个线程,同时执行i=i+1这行代码,i的值最终是2吗?
答案:不确定。a:假设i初始值是0,如果cpu1中的thread1执行,先从主内存中取出i放入自己的高速缓存中,然后执行+1操作,然后将结果存入cpu1高速缓存中,然后刷入主内存中,此时主内存中i=1. 同理cpu2中的thread2也这样执行,cpu2中i初始值为0,执行完输入主内存中,i=1. 这样最终结果i还是1. 这就称之为缓存一致性问题。
-
解决缓存一致性问题:
1.在总线加lock#锁.(所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存. 通过总线锁可以保证原子性) 。缺点:加总线锁的方式,虽然解决了问题,但是会阻塞其他cpu执行,效率低。
2.缓存一致性协议。(当cpu写数据时,如果发现操作的变量是共享变量,即在其他cpu缓存中也存在该变量的副本,会发出信号通知其他cpu将该变量的缓存行置为无效状态,当收到其他cpu发过来的确认信息之后再写数据。当其他cpu需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从主内存重新读取(爱思考的童鞋心里有疑惑了,再次获取的就是更新之后的数据吗)。)
-
继续抛出问题: 缓存一致性问题中,其他cpu重新从主内存中获取数据一定是更新之后的吗? 答案是 不一定。 因为需要写入数据的cpu,刷新主内存的时机是不确定的。这种情况只能通过加锁(例如java中synchroized,lock,volatile等等)
– 并发编程的三个概念
1.原子性:类似事务
2.可见性:导致缓存一致性的问题
3.有序性:禁止指令重排
– JAVA内存模型
起因:内存模型是一套共享内存系统中多线程读写操作行为的规范,这套规范屏蔽了底层各种硬件和操作系统的内存访问差异,解决了 CPU 多级缓存、CPU 优化、指令重排等导致的内存访问问题,从而保证 Java 程序(尤其是多线程程序)在各种平台下对内存的访问效果一致。
在每一个线程中,都会有一块内部的工作内存。这块工作内存保存了主内存共享数据的拷贝副本。并且线程之间不能访问其他线程的工作内存。(tips:线程的工作内存,其实是对cpu寄存器和高速缓存的抽象描述,它不是真实存在的)
原子性
a.在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作。
b.synchronized和Lock来保证原子性。**
复制代码
例子:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
结论:只有语句1是原子性的。
语句1直接将10赋值给i,线程执行这个语句的会直接将数值10写入到工作内存中.
语句2包含两步操作,先读取i的值,然后将i的值写入工作内存,这两步都是原子操作,合在一起就不是了。
语句3和语句4包含3步操作,读取i的值,执行加1,写入工作内存,不具备原子性。
复制代码
-
可见性
a.synchronized或者Lock来保证可见性。
b.volatile保证可见性。
c.happens-before原则
a:synchroized和Lock能够保证同一时刻只有一个县城获取锁然后执行同步代码,
并且在释放锁之前将变量的修改刷新到主内存中。所以保证了可见性。
b:volatile修饰的共享变量,一个线程修改该变量的值,会强制将修改的值立即写入主内存,
并且会导致其它线程中该缓存变量失效,如果其它线程使用该共享变量,需要从主内存中获取。所以也具有可见性。
c.满足happens-before原则,则保证了操作间的可见性。
tips:细心的同学估计有疑问了,volatile 在使其它线程工作缓存中变量失效但是主内存中数据还未更新,
这时候读其它线程取了主内存的共享变量,这样不还是旧数据吗?
答案在这:volatile 修饰的变量读写操作为原子操作,但是本身其他操作(如自增)是非原子操作。
复制代码
-
有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。如果没有指令重排,也就不会出现有序性问题。
a.synchronized或者Lock来保证有序性。
b.volatile保证有序性。
a.synchroized和Lock可以保证在同一时刻,该内部代码只有一个线程执行,
单线程指令重排是没有问题的,所以保证了有序性。
b.volatile禁止了指令重排,所以也能保证有序性。
复制代码
happens-before原则
-
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
-
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
-
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
-
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
-
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
-
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
-
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
-
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
-
总结
知道了java内存模型,我们才算掀开并发编程的遮羞布。下一篇将讲解一下synchroized,volatile,lock…等锁机制。