1、Synchronized特性
- 原子性
-
- 原子性就是指一个或者多个操作不会被任何因素打断要么执行完整,要么就都不执行,被synchronized修饰的类或者对象的所有操作都是原子的,因为在执行操作之前都必须获得类或者对象的锁,执行完之后才能释放锁,,执行过程中不会被其他线程所终端,这就保证了其原子性
- 可见性
-
- 指多个线程访问同一个资源的时候,该资源的状态、值信息对于其他线程是可见的
- synchronized和volatile都具有可见性,synchronized对一个类或者对象加锁时,一个线程如果要访问该类或对象必须先获得他的锁,锁状态对于其他线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存中,保证了资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池内等待锁的释放,然后再重新去竞争锁
- 有序性
-
- 有序性指程序的执行按照代码先后进行执行,因为Java允许编译器和处理器根据情况对指令进行重排序,指令的重排序并不会影响单线程的顺序,它会影响多线程并发执行的顺序性,synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性
- 可重入性
-
- synchronized和ReentrantLock都是可重入锁,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程重复再次请求自己持有对象锁的临界资源,属于重入锁
2、 Monitor(锁)
Java对象头
以32位虚拟机为例
- 普通对象
-
- 对象头为8个字节,其中四个字节为Mark Word 另外四个字节为Klass Word,Klass Word是一个指针,指向了对象所存储的class,通过这个指针可以找到类对象
- 数组对象
-
- 对象头为12个子街,其中四个字节为Mark Word 另外四个字节为Klass Word,多了四个字节用来存储数组的长度
Mark Word结构:
| Mark Word(30bits) | State |
| hashcode:25 age:4 biased_lock:0 01 | Normal(正常状态) |
| thread:23 epoch:2 age:4 biased_lock:1 01 | biased(偏向状态) |
| ptr_to_lock_record:30 00 | Lightweight Locked(轻量级锁) |
| ptr_to_heavyweight_monitor30 10 | Heavyweight Locked(重量级锁) |
| 11 | Marked For Gc |
age:JVM分代年龄
biased_lock:偏向锁
01、00、10:标识加锁状态,01代表没有和任何锁关联,一旦获得了锁该对象的锁标识状态修改为10,并把ptr_to_heavyweight_monitor:30变成指向Monitor对象的指针,占用30位 10占用2位,清除hashCode 和分代年龄等信息,00代表轻量级锁
概念
Monitor被翻译为监视器或者管程,每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级锁)的时候,该对象头中的Mark Word中就被设置指向Monitor对象的指针
Monitor三个属性:
- WaitSet:
- EntrySet:阻塞队列
- Owner:存储锁的所有者
加锁流程
- 当使用synchronized给对象加锁后,Thread1执行给对象obj加锁,obj的MarkWord会用一个指针指向操作系统提供的Monitor对象,在obj的MarkWord中记录了指向Monitor对象的指针地址
- 当有Thread2执行时,会先去检查obj对象是否有关联锁,检查关联Monitor中的Owner查看该锁是否有持有者,有的话,Thread2就会进入EntryList阻塞队列,然后该线程进入BLOCKED状态
- Thread1执行完临界区代码后,Monitor就回去EntryList阻塞队列中唤醒阻塞的线程,然后其他线程再去竞争锁,竞争时是非公平的
锁膨胀流程
无锁 --------->偏向锁---------->轻量级锁---------->重量级锁
偏向锁
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有
获取流程
- Thread1查看obj对象中偏向锁的标识以及锁标志位,若是否偏向锁为1且锁标志位为01,则该锁为可偏向状态
- 如果是可偏向状态,则检查MarkWord中的线程ID是否和当前线程一致,如果相同就执行同步代码
- 如果不相同,当前线程通过CAS操作竞争锁,如果竞争成功,,将MarkWord中的线程Id设置为当前线程ID,然后执行同步代码
- 如果竞争失败,当到达全局安全点时之前获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
释放:偏向锁只有遇到其他线程尝试竞争偏向锁的时候,持有偏向状态的线程才会释放锁,偏向锁的撤销需要等待全局安全点(即没有字节码正在执行),它会暂停拥有偏向锁的线程,撤销后偏向锁恢复到未锁定状态或轻量级锁状态。
轻量级锁
为了减少在没有锁竞争的时候,获得锁和释放锁带来的性能消耗,JDK6引入了轻量级锁。轻量级锁在对象内存布局中 MarkWord 锁标志位为 00,它可以由偏向锁对象因存在多个线程访问而升级成轻量级锁,轻量级锁也可能因多个线程同时访问同步代码块升级成重量级锁。
加锁流程
假设此时由两个方法同步块,利用同一个对象加锁
static final Object obj = new Object(); public static void method1() { synchronized( obj ) { // 同步块 A method2(); } } public static void method2() { synchronized( obj ) { // 同步块 B } }
- 当Thread1执行代码进入同步代码块时,如果对象的MarkWord的锁标志位为01无锁状态,虚拟机会在该线程的栈帧中创建一个Lock Record对象,用来存储锁定当前对象的MrakWord
- 然后复制对象头中的MarkWord到锁记录中,复制成功后,让锁记录中的Object reference会指向锁对象,并用CAS尝试替换对象头的中的MarkWord为Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word
- 如果CAS替换成功,对象头中存储的内容会编程 锁记录的地址,锁状态标识会变成00,表示由Thread1给该对象加锁,处于轻量级状态
- 如果CAS加锁失败,可能会有两种情况,这是虚拟机会检查对象头中的MarkWord的Lock Record指针是否指向当前线程的栈帧,如果是则说明当前线程执行了锁冲入,那么就会在当前线程的栈帧中再添加一条LockRecord作为锁重入的计数,如果不是当前线程的栈帧,则说明是其他线程已经持有了该obj的轻量级锁,此时发现有锁竞争的情况,则进入锁膨胀状态
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减1
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
- 自旋会占用CPU时间,单核CPU自旋就是浪费资源,多核 CPU 自旋才能发挥优势。
- Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功之后,那么这一次自旋就会多自旋几次,反之就会少自旋甚至不自旋
- Java 7 之后不能控制是否开启自旋功能
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。