synchronized
是jvm提供的同步和锁机制,与之对应的是jdk层面的J.U.C提供的基于AbstractQueuedSynchronizer
的并发组件。synchronized
提供的是互斥同步,互斥同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只有一个线程访问。
在jvm中,被synchronized
修饰的代码块经javac编译之后,会在代码块前后分别生成一条monitorenter和moniterexit字节码指令,这两个字节码都需要一个reference类型的参数来指定要锁定和解锁的对象。如果synchronized
指定了对象参数,reference就是该对象的引用,如果没有手动指定,那就根据synchronized
修饰的是实例方法还是类方法,取对应的对象实例或Class对象来作为锁对象。
值得一提的是,java中Object
类有两个方法wait()
和notify()
(notifyAll()
与notify()
类似)和同步锁相关。至于这两个方法为什么要放在Object
类中,原因如下:wait()
方法的语义是使当前线程(调用wait()
方法的线程)等待,知道被notify()
方法唤醒。当前线程必须拥有对象的锁,调用wait()
方法后,当前线程会释放同步对象的锁。因此,wait()
方法必须在synchronized
修饰的代码块内(肯定获取了同步锁),由于任何Java对象都能作为对象锁,因此这两个方法需要放在Object
中。
Java线程是映射到操作系统的原生线程上的,如果要阻塞或唤醒一个线程,都需要进入到内核完成,这种从用户态转换到内核态的状态转换需要耗费很多的处理器时间,所以synchronized
是Java中非常消耗资源的一种操作。在JDK1.5之前,synchronized
与基于JDK实现的ReentrantLock
相比,性能要低很多。在JDK1.6及之后的版本中,synchronized
实现了很多针对于锁的优化措施,这些优化有很大一部分与ReentrantLock
的实现思路相似。
阻塞同步经常涉及到加锁和解锁,这就意味着用户态和内核态的切换,非常消耗资源。为此,一种基于冲突检测的乐观并发策略应运而生,这种策略的核心思想是:先对数据进行操作,如果没有其它线程争用数据,那就认为操作成功;否则,采取其它方式来保证数据操作成功(例如:一直重试,知道成功)。这种并发策略不需要把线程挂起,所以称为非阻塞同步。
CAS是CompareAndSwap的简称,它需要三个操作数,分别是内存位置、预期值和更新值。当CAS指令执行时,处理器先判断内存位置的值与预期值是否相等,如果相等,则将预期值更新成更新值;否则,认为有其它线程已经修改过这个内存位置的值了,更新操作不会允许。不论更新操作是否发生,CAS操作都会返回预期值,CAS操作是一个原子操作。
CAS非常适用于非阻塞同步,例如:要将一个int型的值i加1,使用CAS的思路是先判断变量i的内存位置的值是否有修改,如果没有,则将这个位置的值更新成i+1;否则,认为其它线程已经修改过这个值了,可以再次调用前面的操作尝试更新,直到更新成功。因为,在判断内存位置的值时需要先获取这个内存位置的值,在Java中,为了保证这个变量的可见性,需要用volatile
关键字进行修饰。在JDK1.5后,Java程序提供了sun.misc.Unsafe
类实现了基本类型的CAS操作的封装,JUC组件就是基于这个类实现的。以java.util.concurrent.atomic.AtomicInteger
类为例,这个类包装了一个实例变量value:
private volatile int value;
对这个变量实现自增的源代码如下:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
其中Unsafe#getAndAddInt()
的实现如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
这里var2就是内存地址的值,var5是预期值,当compareAndSwapInt()方法返回true时,表示这个CAS操作成功,否则重试,知道成功为止。从这里,可以发现CAS操作的三个缺点:
synchronized
那样对一段代码同步。 Unsafe
的实例,这个类不是提供给用户程序使用的类,它里面封装的大都是一些JNI方法,程序员一般不需要使用这个类,只需要知道它是Java中CAS操作的封装即可。前面描述的synchronized
对应的同步锁比较重量级,为此,JDK1.5之后,开发人员实现了各种锁优化技术,包括自适应自旋锁、锁消、锁粗化、轻量级锁和偏向锁等。注意,这些锁都是虚拟机层面的优化,可以认为是对synchronized
对应的字节码的优化。
自旋锁是虚拟机开发人员的对共享数据的统计分析得到的一种优化技术,就是,大多数情况下,共享数据的锁定状态只会持续很短的一段时间,其它线程在等待锁的时候,为了这段等待时间而挂起和恢复线程并不值得。为了让线程等待一小会时间而不挂起,可以让线程执行一个忙循环(自旋),这就是自旋锁。注意,自旋锁是需要消耗CPU资源的,如果,碰巧共享数据的锁定时间很长,那么,自旋锁的性能反而会下降。自适应自旋锁是自旋锁的一种优化,它可以动态调整自旋时间,避免盲目等待。
锁消除是javac层面上的优化。对一些程序员编写的用或调用synchronized
修饰的代码,但是被检测到不可能存在共享数据竞争的情况下,javac会对这部分代码进行优化,消除多余的同步指令。
原则上,编写程序时,要求同步块越小越好,但是,当一系列的连续操作都是对同一个对象的反复加锁和解锁时,就是对资源的浪费,这时,将锁的范围扩大到整个操作序列上有利于节省反复加锁和解锁占用的资源。锁消除和锁粗化技术都是javac或jvm的智能优化。
轻量级锁是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁实现的依据是:“绝大部分时候,在整个同步周期内是不存在竞争的”,当不存在竞争时,使用轻量级锁不需要使用操作系统的互斥量,这样能节省资源。但是,当存在竞争时,轻量级锁将既存在CAS开销,还存在传统重量级锁操作的开销,性能更低。
轻量级锁的实现分为加锁和解锁两个过程:
偏向锁相对于轻量级锁是一种更加激进的做法:在无竞争的情况下把整个同步都消除掉,偏向锁的意思是锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁没有被其它线程获取,那么持有偏向锁的线程将永远不需要同步。虚拟机通过-XX:+UseBiasedLocking参数启动偏向锁,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”——偏向锁模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作;当有另一个线程尝试获取这个锁时,偏向锁就结束。这时,通过锁对象目前是否处于锁定状态,撤销偏向后恢复到未锁定或轻量级锁定的状态,后续的同步操作就如同前面介绍的轻量级锁那样执行。
参考:《深入理解Java虚拟机》第二版,周志明