JAVA内存模型(JMM java memory model)
如上图即JMM,JMM是为了规范内存数据和工作空间的数据交互。 主内存是共享的数据区域,工作内存是私有的数据区域,基本数据类型直接分配到工作内存,引用的地址存放在工作内存,引用的对象存放在堆里。
工作方式:1 线程直接在工作空间里修改私有数据。 2 线程修改共享数据。首先把数据复制到工作空间中,在工作空间中修改,修改完成后刷新到主内存中。
三个特性 :原子性,不可分割。 可见性:线程之间只能操作自己工作空间的数据。 有序性:程序的顺序不一定就是程序的执行顺序。编译期间会了提高性能发生编译重排序和指令重排序。
导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性,有序性的办法就是禁止缓存和编译优化。虽然解决了问题,但是性能很差,合理的方案是按需禁用缓存和编译优化。JMM规范了JVM如何按需禁用缓存和编译优化的方法,这些方法有volatile,synchronized和final三个关键字,以及六项happens-before原则。这里提一下as-if-serial,他是单线程环境下重排序后不影响程序的执行结果。
volatile: volatile修饰一个变量,他告诉编译器,对这个变量的读写不能使用CPU的缓存,而要从内存中读取或者写入。java 1.5后对volatile进行了加强,happens-before原则。
happens-before:
Happens-Before 的意思是前面一个操作的结果对后续的操作可见。Happens-Before 约束了编译器的优化行为,允许编译器优化,但是要求编译器遵循Happens-Before 原则。
1 程序的顺序性规则
程序前面对某个变量的修改一定是对后续操作可见的。
2 volatile变量规则
volatile变量的写操作,Happens-Before 与volatile变量的读操作,其实就是禁用缓存。
3 传递性
A Happens-Before B ,B Happens-Before C 则A Happens-Before C。
4 管程中锁的规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程是一种通用的同步语句,在JAVA里就是synchronized。
synchronized (this) { // 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
如上X初始值为10,线程A执行完代码块后x变成12,后续线程B进来后看到线程A把X改成了12。
5 线程start规则
在线程A中启动线程B,子线程B能够看到主线程A在启动子线程B前的操作。
6 线程join原则
线程A中调用B的join方法并成功返回,那么B中的任意操作Happens-Before 于 该join方法的返回。
7 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
8 对象终结规则:
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
final:
final修饰的变量,告诉编译器这个变脸是不会变的。
互斥锁:解决原子性问题
原子性问题源头是线程切换。操作系统做线程切换是依赖CPU中断的,禁止CPU发生中断就可以禁用线程切换。
单核CPU的场景下,同一时刻只有一个线程执行,禁止CPU中断,意味着操作系统不会重新调度线程,禁止线程切换获得CPU使用权的线程就可以一直执行,具有原子性。但是多核场景下,两个线程同时执行,一个线程执行在CPU1上,一个执行在CPU2上,此时禁止CPU中断,只能保证CPU上的线程连续执行,但不能保证同一时刻只有一条线程执行。
同一时刻只有一条线程执行被称为互斥。如果能保证对共享变量的修改是互斥的,那就可以保证原子性了。
synchronized锁可以实现互斥。synchronized可以修饰方法,修饰代码块,修饰类。java编译器会在synchronized修饰的方法和代码块前后自动加上加锁和解锁。当修饰静态方法时,锁定的是当前类的class对象,当修饰非静态方法时锁定的就是当前实例对象this。
管程中锁的原则:对一个锁的解锁Happens-Before 后续这个锁的加锁。前一个线程在临界区修改的共享变量,对后续(该操作在加锁之后)进入临界区的线程可见。
class SafeCalc {
static long value = 0L;
synchronized long get() {
return value;
}
synchronized static void addOne() {
value += 1;
}
}
如上代码中其实是两个锁在保护一个资源。synchronized修饰的第一个是this,第二个是SafeCalc.class。因此这两个临界区资源是没有互斥性和可见性,这就会导致并发问题。
如何一把锁保护多个资源
一把锁来管理多个问题的情况下性能较差,举个例子,取款,查看余额,修改密码等一系列用同一把锁管理,所有的操作都是串行执行的,用不同的锁对受保护的临界区资源进行精细化管理,能够提升性能,这种锁被称为细粒度锁。细粒度锁可以提高程度的并行度。
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
如上例子,用Account.class作为共享的锁,Account.class是所有account对象共享的,而且这个对象是JAVA虚拟机在加载Account类的时候创建的,所以我们不用担心他的唯一性。