多线程读书笔记二(java内存模型、volatile变量、内存模型与synchronized、CAS)

java内存模型
java中,线程之间的通信是通过 共享内存的方式,存储在堆中的实例域,静态域以及数组元素都可以在线程间通信。java内存模型控制一个线程对共享变量的改变何时对另一个线程可见。
线程间的共享变量存在主内存中,而对于每一个线程,都有一个私有的工作内存。工作内存是个虚拟的概念,涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化,总之就是指线程的本地内存。存在线程本地内存中的变量值对其他线程是不可见的。
如果线程A与线程B之间如要通信的话,必须要经历下面2个步骤,如图所示:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。 
  多线程读书笔记二(java内存模型、volatile变量、内存模型与synchronized、CAS)_第1张图片

关于volatile变量
由于java的内存模型中有工作内存和主内存之分,所以可能会有两种问题:
(1)线程可能在工作内存中更改变量的值,而没有及时写回到主内存,其他线程从主内存读取的数据仍然是老数据
(2)线程在工作内存中更改了变量的值,写回主内存了,但是其他线程之前也读取了这个变量的值,这样其他线程的工作内存中,此变量的值没有被及时更新。
为了解决这个问题,可以使用同步机制,也可以把变量声明为volatile,volatile修饰的成员变量有以下特点:
(1)每次对变量的修改,都会引起处理器缓存(工作内存)写回到主内存。
(2)一个工作内存回写到主内存会导致其他线程的处理器缓存(工作内存)无效。
基于以上两点,如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
此外, java虚拟机规范(jvm spec)中,规定了声明为volatile的long和double变量的get和set操作是原子的。这也说明了为什么将long和double类型的变量用volatile修饰,就可以保证对他们的赋值操作的原子性了
 
关于volatile变量的使用建议:多线程环境下需要共享的变量采用volatile声明;如果使用了同步块或者是常量,则没有必要使用volatile。

java内存模型与synchronized关键字
synchronized关键字强制实施一个互斥锁,使得被保护的代码块在同一时间只能有一个线程进入并执行。当然synchronized还有另外一个 方面的作用: 在线程进入synchronized块之前,会把工作存内存中的所有内容映射到主内存上,然后把工作内存清空再从主存储器上拷贝最新的值。而 在线程退出synchronized块时,同样会把工作内存中的值映射到主内存,但此时并不会清空工作内存。这样一来就可以强制其按照上面的顺序运行,以 保证线程在执行完代码块后,工作内存中的值和主内存中的值是一致的,保证了数据的一致性!  
所以由synchronized修饰的set与get方法都是相当于直接对主内存进行操作,不会出现数据一致性方面的问题。


关于CAS
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

为什么CAS可以用于同步?
例如,有一个变量i=0,Thread-1和Thread-2都对这个变量执行自增操作。 可能会出现Thread-1与Thread-2同时读取i=0到各自的工作内存中,然后各自执行+1,最后将结果赋予i。这样,虽然两个线程都对i执行了自增操作,但是最后i的值为1,而不是2。
解决这个问题使用互斥锁自然可以。但是也可以使用CAS来实现,思路如下:
自增操作可以分为两步:(1)从内存中读取这个变量的当前值(2)执行(变量=上一步取到的当前值+1)的赋值操作。

多线程情况下,自增操作出现问题的原因就是执行(2)的时候,变量在主内存中的值已经不等于上一步取到的当前值了,所以赋值时,用CompareAndSet操作代替Set操作:首先比较一下内存中这个变量的值是否等于上一步取到的当前值,如果等于,则说明可以执行+1运算,并赋值;如果不等于,则说明有其他线程在此期间更改了主内存中此变量的值,上一步取出的当前值已经失效,此时,不再执行+1运算及后续的赋值操作,而是返回主内存中此变量的最新值。 “比较并交换(CAS)”操作是原子操作,它使用平台提供的用于并发操作的硬件原语。

通过下面代码可以加深理解:
package com.jyq.multithread;

import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.lang.Thread;

public class Counter {
	private AtomicInteger atomicInteger = new AtomicInteger(0);
	private int i = 0;

	// 使用CAS实现线程安全的计数器
	public void safeCount() {
		//用一个for循环,如果没有计数成功的话,会一直执行这段代码,知道计数成功break为止
		for (;;) {
			int i = atomicInteger.get(); //读取value值,赋给i,i在线程的工作内存中
			//将主内存中的值(current)与工作内存中的值i相比较,如果相等的话,说明工作内存中的i值仍然是value的最新值
			//计数运算对当前i操作没有问题,将value值设为i+1,因为value是violent的,所以写的时候也就写到了主内存
			boolean suc = atomicInteger.compareAndSet(i, i + 1); 
			if (suc) {
				break;
			}
		}
	}

	// 非安全的线程计数器
	public void count() {
		i++;
	}

	public static void main(String[] args) {
		final Counter cas = new Counter();
		List ts = new ArrayList();
		for (int j = 0; j < 100; j++) {
			Thread t = new Thread(new Runnable() {

				@Override
				public void run() {
					for (int i = 0; i < 10000; i++) {
						cas.safeCount();
						cas.count();
					}

				}
			});
			ts.add(t);
		}
		for (Thread t : ts) {
			t.start();
		}
		// 等待所有线程执行完成
		for (Thread t : ts) {
			try {
				t.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(cas.atomicInteger.get());
		System.out.println(cas.i);
	}
}

参考文章:
http://ifeve.com/java-memory-model-0/ 

深入理解java内存模型系列文章


http://ifeve.com/atomic-operation/

聊聊并发(五)原子操作的实现原理


你可能感兴趣的:(Java,多线程)