java内存模型
共享内存的方式,存储在堆中的实例域,静态域以及数组元素都可以在线程间通信。java内存模型控制一个线程对共享变量的改变何时对另一个线程可见。
线程间的共享变量存在主内存中,而对于每一个线程,都有一个私有的工作内存。工作内存是个虚拟的概念,涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化,总之就是指线程的本地内存。存在线程本地内存中的变量值对其他线程是不可见的。
如果线程A与线程B之间如要通信的话,必须要经历下面2个步骤,如图所示: 1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。 2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
关于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修饰的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)的赋值操作。
“比较并交换(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<Thread> ts = new ArrayList<Thread>();
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);
}
}