一:JMM模型
Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
MM与JVM内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
线程,工作内存,主内存工作交互图(基于JMM规范):
主内存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。
工作内存:主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
根据JVM虚拟机规范主内存与工作内存的数据存储类型以及操作方式,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存。
JMM模型定义了八种操作来实现主内存和工作内存的交互过程:
1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态;
2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的
变量才可以被其他线程锁定;
3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,
以便随后的load动作使用;
4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作
内存的变量副本中;
5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎;
6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存
的变量;
7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,
以便随后的write的操作;
8. write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送
到主内存的变量中;
要把一个变量从主内存复制到工作内存中,就必须按顺序执行read和load操作,把一个变量从工作内存同步到主内存,也必须按顺序执行store和write操作,但JMM模型只要求按顺序操作,没有保证必须是连续执行的。
二、synchronized
那么我们如何解决线程并发安全问题?
目前采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。
同步器的本质就是加锁,加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)
不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。其加锁方式:
1. 同步实例,锁当前实例对象
2. 同步类方法,锁当前类对象
3. 同步代码块,锁括号里的对象
synchronized底层原理:synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现。jdk1.5之前是一个重量级锁,性能较低,1.5版本后做了重大优化,如锁粗化,锁消除、轻量级锁、偏向锁、适应性自旋等来减少锁操作开销,性能提高很多。
锁消除:Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
synchronized关键字被编译成字节码后会被翻译成monitorenter 和monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
每个同步对象都有一个自己的Monitor(监视器锁),synchronized加锁加在对象上,其锁状态是被记录在每个对象的对象头(Mark Word)中。
对象的内存布局:
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
实例数据:即创建对象时,对象中成员变量,方法等
对齐填充:对象的大小必须是8字节的整数倍
Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0。
锁的膨胀升级过程:
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
若此时还没有任何线程访问同步块,那么锁就处于无所状态,此时对象锁的mark word中的标志位是01,是否偏向是0;
Object Mark Word | |||
对象的HashCode | (对象的分代年龄)age | (是否是偏向锁)0 | (锁标志位)01 |
若此时进来线程t1访问同步块,会先检查mark word的标志位,若标志位是01,则处于无所状态或偏向锁状态,再去倒数第三位判断是无所状态还是偏向锁状态,若为0则为无所状态,然后升级锁为偏向锁状态,并把mark word中的前25位的hashcode去掉替换为当前线程ID,替换成功后把无锁的状态0改为偏向锁1(偏向锁不会自动释放,若没有其他线程竞争,锁会一直偏向这个线程);若此时又进来一个线程t2方位同步块,他会先检查mark word中的是否偏向线程ID是否值自己,若不是就会尝试修改偏向线程ID为自己,如果修改失败,就想jvm发生发出请求,jvm则把锁升级为轻量级锁,并且线程在当前线程栈中开辟一个空间LockRecord,并把mark word的内容复制一份到LockRecord,同时定义一些变量,如owner等。并且mark word的前30位修改为指针,指向线程栈中LockRecord,并且LockRecord中的owner也存在一个指针指向mark word。锁升级为轻量级锁后,两个线程都已经开辟了LockRecord空间并复制mark word到空间中,此时,两个线程同时修改Mark Word头,指向线程的LockRecord,若修改成功,则抢到锁,若修改失败,则进行自旋(不断尝试修改mark word指针,获取锁)。若自旋失败,则升级锁为重量级锁,并阻塞t2线程,直到t1执行完,唤醒被阻塞的线程。
三、volatile
volatile是Java虚拟机提供的轻量级的同步机制,作用:
1.保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
(volatile可见性)被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但volatile无法保证原子性,如果在并发访问下对某个变量进行自增操作就会出现线程安全问题,该操作是先读取值,再写回值,相当于原来的值加1,是分两步完成的,如果第二个线程在第一个线程读取旧值和写回新值之间也读取了该变量的值,那么两个线程看到的值是一样的,并进行自增操作,这就造成了线程安全失败。
2.禁止指令重排序优化,从而避免多线程环境下程序出现乱序执行的现象。
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。