Synchronized关键字使用较多的形式如下所示:
1:以synchronized同步代码块的形式
synchronized(Object){
//代码
}
2:对一个方法进行synchronized声明,进而对一个方法进行加锁来实现同步。如下面代码
public synchornized void test(){
//代码
}上述中其实都是对对象加锁,对于第二种如果非static方法,当前锁为this对象,若为static方法当前锁为当前类的Class对象。
锁对象
java中任何一个对象都可以称为锁对象,原因在于java对象在内存中存储结构,如下图所示:

在对象头中主要存储的主要是一些运行时的数据,如下所示:
| 长度 | 内容 | 说明 |
|---|---|---|
| 32/64bit | Mark Work | hashCode,GC分代年龄,锁信息 |
| 32/64bit | Class Metadata Address | 指向对象类型数据的指针 |
| 32/64bit | Array Length | 数组的长度(当对象为数组时) |
其中 在Mark Work中存储着该对象作为锁时的一些信息,如下所示是Mark Work中在64位系统中详细信息:

从上图可以看出,对于对象锁可能存在的4中状态:无锁->偏向锁->轻量级锁->重量级锁,并且其中锁的升级是不可逆的。
偏向锁
偏向锁和无锁的标志位在Mark Work中的标志位都为01,只不过区别在于是否生效来,这也说明任何一个对象被创建出来,都可能是偏向的,具体是否偏向要看具体实现。从偏向锁的定义上可以看出,该锁的着重点在于偏字上,它会偏向与第一个获取它的线程,这句话什么意思呢?当一个线程进入到被Synchronized修饰的代码时(官方话叫临界区时-只允许一个线程进去执行操作的区域),就根据CAS(Compare and Swap)的操作将线程的id插入到Mark Work中指定区域(上图中54bit区域),并且将当前偏向锁状态改为1(上述的操作实际上就是一个偏向锁上锁的过程)。那么代表当前线程已经获取执行同步代码块的权利了。当后续该线程进来时,如果该锁没有被其他锁获取到或者没发生锁竞争,那么就会再有任何的同步措施,即加锁或者解锁的措施了,只会进行Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致。
如果当前偏向锁出现了锁竞争的话,当前线程也会判断之前拥有锁的线程是否存在或者存在但没拥有该锁状态,就进行重置该偏向锁,并重新进行上锁过程,若仍然存在,此时该偏向锁就会升级为轻量级锁。在这个升级的过程中就会涉及到锁撤销的过程,锁撤销的过程也是满复杂的,资源的消耗也挺大的。所以如果我们的应用中大量都是存在多线程锁竞争的关系,那么不断的进行锁升级,其实是一个没必要的事情,此时我们可以在启动的时候设置-XX:-UseBiasedLocking = false,即该应用就不存在偏向锁了。
轻量级锁
当一个锁从偏向锁升级为轻量级锁时,那么对应的Mark Work中的数据格式也会发生变更,如图二所示:
- 锁的标志位切换为00
- 在当前线程栈创建锁记录LockRecord
- 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中
- 将锁记录中的Owner指针指向锁对象
- 将Mark Work中62bit的空间存储指向获取锁线程的锁记录LockRecord
如下图所示:


轻量级锁有两种:自旋锁和自适宜自旋锁
自旋锁:
顾名思义当出现线程锁竞争的时候,已经获取锁资源的线程执行同步代码块中代码,没有获取到的线程不会挂起而是在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这些等待的线程就会进行下一次的锁竞争。在线程自旋的过程中这些线程是存活的,只不过cpu是一直空转的,就是相对于执行一个没有结束的for循环(在java源码中有关于CAS的操作大都是通过for进行自旋)。如果说自旋次数过多就会导致cpu资源的浪费,所以对于自旋的次数,java对其做了规定,默认每个线程最多执行10次(该值)。所以从这上面看来如果要求自旋锁能达到最优状态,最好是同步代码块中的代码执行时间短,并且只存在少量的锁竞争关系。
自适宜自旋锁:
该锁在jdk1.6的时候被引入,线程的自旋次数不再是固定值了而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,为了避免浪费处理器资源,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接升级为重量级锁
重量级锁
对包含Synchronized的代码进行反编译之后会发现在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。
关于这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
从上面对于monitorenter与monitorexit的描述也可以看出Synchronized是一个重入锁,每次重入对应的monitor的进入数+1,退出减1.
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。在JDK1.6之前,Synchronized主要就是依赖与重量级锁实现各个功能,所以1.6之前建议不要使用Synchronized而使用ReentrantLock锁,在JDK1.6之后为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”,最终使得Synchronized的效率与ReentrantLock相差无几,甚至在某些场景下还优胜与ReentrantLock。

参考:周志明老师的<深入理解Java虚拟机>