Java内存模型内存间交互操作
在介绍synchronized之前先简单的介绍一下JMM的交互操作
Java内存模型定义了8个操作来完成主内存和工作内存的交互操作。
- read:把一个变量的值从主内存传输到工作内存中
- load:在read之后执行,把read得到的值放入工作内存的变量副本中
- use:把工作内存中一个变量的值传递给执行引擎
- assign:把一个从执行引擎接收到的值赋给工作内存的变量
- store:把工作内存的一个变量的值传送到主内存中
- write:在store之后执行,把store得到的值放入主内存的变量中
- lock:作用于主内存的变量
- unlock
synchronized
synchronized的底层是使用操作系统的mutex lock实现的。
内存可见性:同步块的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
操作原子性:持有同一个锁的两个同步块只能串行地进入
锁的内存语义
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把线程对应的本地内存置为无效。从而使得监视器保护的临界区代码必须从主内存中读取共享变量
锁释放和锁获取的内存语义
- 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
- 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
- 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
synchronized锁
synchronized锁的是对象的头。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经有用了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放
Mutex Lock
监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
synchronized的使用场景
分类 | 被锁住的对象 | 伪代码 | |
---|---|---|---|
方法 | 实例方法 | 类的实例对象 | //实例方法,锁住的是该类的实例对象public synchronized void method(){ ……} |
静态方法 | 类对象 | //静态方法,锁住的是类对象public static synchronized void method(){ ……} | |
代码块 | 实例对象 | 类的实例对象 | //同步代码块,锁住的是该类的实例对象synchronized(this){ ……} |
class对象 | 类对象 | //同步代码块,锁住的是该类的类对象synchronized(xxx.class){ ……} | |
任意实例对象Object | 实例对象Object | //同步代码块,锁住的是配置的实例对象//String对象作为对象锁String lock = "";synchronized(lock){ ……} |
Java对象保存在内存中的组成
1.对象头
2.实例数据
3.对其填充字节
对象头
java的对象头由以下三部分组成:
- 1.Mark Word
- 2.指向类的指针
- 3.数组长度(只有数组对象才有)
Mark Word
Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
Mark Word在不同的锁状态下存储的内容不同,在32位JVM中是这么存的:
其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态
PS:JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
指向类的指针
该指针在32位JVM中的长度是32bit,在64位的JVM中的长度是64bit。
数组长度
只有数组对象保存了这部分数据。该数据再32位和64位JVM中的长度都是32bit。
实例数据
在java代码中能看到的属性及其值。
对齐填充字节
因为JVM要求java对象所占的内存大小是8bit的倍数,所以后面有几个字节用于把对象的大小补全至8bit的倍数。
synchronized锁的升级
下面通过一张图来看一下synchronized锁升级的过程(图片来自网络)。
JVM中一般是这样使用锁的:
无锁状态
当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁位是0。
无锁-->偏向锁
1)只有一个线程抢锁
当对象被当做同步锁并有一个线程抢到了锁,锁标志位是01,是否偏向锁位是1,前32bit记录抢到锁的线程id,表示进入偏向锁状态。
2)有一个线程已经抢到锁,其他线程也来抢锁
线程2试图再次获取锁时,JVM发现同步锁对象处于偏向状态,Mark Word中记录的线程id就是线程2的id,表示线程2已经获得了这个偏向锁,可以执行同步锁的代码。
当线程3视图获取这个锁时,JVM发现同步锁对象处于偏向状态,但是Mark Word中记录的线程id不是线程3的id,那么线程3会先用CAS操作试图获得锁。如果抢锁成功,就把Mark Word里的线程id改为线程3的id,代表线程3获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
偏向锁-->轻量级锁
当同步对象处于偏向锁状态线程抢锁失败将会升级成轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改为00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈。JVM会使用自旋锁,不断的重试,尝试抢锁。从jdk1.7开始,自旋锁默认启用,自旋次数由jvm决定。如果抢锁成功则执行同步锁代码,如果抢锁失败轻量级锁将升级为重量级锁。
轻量级锁-->重量级锁
当线程自旋获取轻量级锁失败将会升级成重量级锁。锁标志位改为10.这个状态下未抢到锁的线程都会被阻塞。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。