synchronized
应用方式
首先要知道synchronized实现同步的基础为Java中每一个对象都可以作为锁,synchronized一般的使用方法有以下三个。
- 在普通方法上加synchronized,为方法的对象加锁。
- 在静态方法上加synchronized,为该方法的类模板加锁。
- 同步方法块,为给定对象加锁。
每一个线程执行到同步块或者是同步方法(对共享内存进行访问的程序片段称为临界区),就会去申请获得锁。当同步的操作执行完或者是抛出异常时就会把该锁给释放掉。
简单应用
之前的volatile文章有提过,volatile是不保证原子性的,现在就用synchronized来保证那个例子的原子性。
1 | package com.yw; |
由此可以看见测试了很多次,答案都是20000,从而用synchronized可以保证了这个例子的原子性。下面我用图来简单讲解一下为什么synchronized能够保持这个例子的原子性。
Monitor
介绍synchronized的原理之前先了解一个重要的概念,Monitor。
Monitor又称监视器,是一种同步机制,synchronized就是通过这个机制来实现方法或者代码块的同步的。
实际上任何一个Java对象都可以作为Monitor机制的monitor object,每一个Object的类都在底层实现了ObjectMonitor模式(这是底层用C来实现的),我们一般看不到,只要知道每一个Object类的对象都可以作为monitor对象
同步代码块会在代码块开始位置插入一条monitorenter
在代码块结尾插入一条monitorexit
,用这两条指令来实现Monitor机制。
同步方法是通过一个ACC_SYNCHRONIZED
标志来确定,执行方法时判断有无该标志位,然后会隐式调用上面两条指令。
起初在没有线程执行到临界区时,monitor的大概情况是这样子的。
当有一个线程进入临界区,Monitor中的Owner就会指向该线程。
此时要是有其他线程也执行到了临界区的范围,就会进入Enter Set,即将线程的状态转变为阻塞状态,故Enter Set也可以称为阻塞队列。
当Owner的线程1离开了临界区就会出现三种情况。
线程1因某些原因发生了
wait
等待,Owner恢复为null,并进入Wait set
,然后阻塞队列中的线程开始竞争Owner的位置。(这里假设线程2竞争到了)线程1正常离开临界区,Owner恢复为null,线程1之后不再进入临界区,阻塞队列中的线程就会开始竞争Owner的位置。(这里假设线程2竞争到了)
线程1正常离开临界区,Owner恢复为null,线程1之后还会进入临界区,那线程1就会与阻塞队列中的线程竞争Owner的位置。(这里假设线程2竞争到了)
Monitor是synchronized实现线程同步的原理(当初只有重量级锁,锁的优化靠其他的机制)。
从图中也可以看出为什么wait这些方法要放在synchronized中使用,因为都需要借助到Monitor的机制来进行实现。
Java对象头
Java对象头是存储对象的一些基础信息的集合。
长度 | 内容 | 说明 |
---|---|---|
32bit | Mark Word | 存储对象的hashCode或锁信息等 |
32bit | Class Metadata Address | 存储到对象类型数据的指针 |
32bit | Arraylength | 数组的长度(要是对象不是数组则没有该项) |
这里就不详细探讨后面两个了,这里的重点是Mark Word
,现在我们来看看这里面具体存储了什么。
锁状态 | 25bit | 4bit | 1bit | 2bit | |
---|---|---|---|---|---|
23bit | 2bit | 是否是偏向锁 | 锁的标志位 | ||
无锁 | 对象的hashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | |||
GC标志 | 空 | 11 |
在JDK1.6之前,synchronized只是一个重量级的锁,当JDK1.6进行更新之后,引入了偏向锁和轻量级锁的概念,所以锁的状态变成了以上的四种。
偏向锁
偏向锁是JDK1.6之后引进的,HotSpot作者发现大多数情况锁不仅存在多线程竞争,而且总是由同一线程多次获得。如果是之前,该线程每次都要获得锁,这样代价也未免太大了一点,所以为了优化这种情况,就出现了偏向锁。
偏向锁,顾名思义,偏爱某个线程的锁。当一个线程访问同步块并获取锁,会在对象头和栈帧中的锁记录里存储偏向的线程ID。以后该线程再次来获取锁,就只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁就好了,不必再次进行CAS来操作加锁和解锁。
- 偏向锁在Java6中是默认开启的,但是要在应用程序启动几秒之后才会激活,也就是说是有延迟的,有必要也可以通过设置参数来设置延迟
-XX:BiasedLockingStartupDelay=0- 当不希望使用偏向锁,也可以通过设置参数来关闭,默认进入轻量级锁
-XX:-UseBiasedLocking=false