Synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外一篇博客Java:这是一份全面 & 详细的 Sychronized关键字 学习指南从多个方面介绍Synchronize的原理和使用方式等,值得大家阅读。
接下来我们正式进入本篇博客的主题。
学习Java的小伙伴都知道synchronized关键字是解决并发问题常用解决方案,常用的有以下三种使用方式:
关于synchronized的使用方式以及三种锁的区别在学习指南中讲解的十分清楚。
实现原理: JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。
具体实现是在编译之后在同步方法调用前加入一个monitor.enter
指令,在退出方法和异常处插入monitor.exit
的指令。
对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit
之后才能尝试继续获取锁。
通过一段代码来演示:
public static void main(String[] args) {
synchronized (Synchronize.class){
System.out.println("Synchronize");
}
}
使用javap -c Synchronize
可以查看编译之后的具体信息。
可以看到在同步块的入口和出口分别有monitorenter
和monitorexit
指令。当执行monitorenter
指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit
指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
在synchronized修饰方法时是添加ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
synchronized的特点:
从synchronized的特点中可以看到它是一种重量级锁,会涉及到操作系统状态的切换影响效率,所以JDK1.6中对synchronized进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁和轻量锁。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
3.1.1 偏向锁的获取过程
(1)访问Mark Word中偏向锁的标识是否设置成“1”,锁标志位是否为“01”——确认为可偏向状态。
(2)如果为可偏向状态,判断线程ID是否指向当前线程,如果是进入步骤(5),否则进入步骤(3)。
(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
3.1.2 偏向锁的释放
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。
3.2.1 轻量级锁的加锁过程
(1)在代码进入同步块时,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
(4)如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
(5)如果这个更新操作失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
3.2.2 轻量级锁的解锁过程
(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程完成。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
适应性自旋:在使用CAS时,如果操作失败,CAS会自旋再次尝试。由于自旋是需要消耗CPU资源的,所以如果长期自旋就白白浪费了CPU。JDK1.6 加入了适应性自旋,即如果某个锁自旋很少成功获得,那么下一次就会减少自旋。
通过--XX:+UseSpinning
参数来开启自旋(JDK1.6之前默认关闭自旋)。
通过--XX:PreBlockSpin
修改自旋次数,默认值是10次。
锁消除:锁消除指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
锁粗化:我们在写代码时推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
注意:在大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
其他控制并发/线程同步方式还有 Lock/ReentrantLock。
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized依赖于JVM而ReenTrantLock依赖于API
synchronized是依赖于JVM实现的,前面我们也讲到了 虚拟机团队在JDK1.6为synchronized关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock是JDK层面实现的(也就是API层面,需要lock()和unlock()方法配合try/finally语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③ ReenTrantLock比synchronized增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
④ 性能已不是选择标准
在JDK1.6之前,synchronized的性能是比ReenTrantLock差很多。具体表示为:synchronized关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。在JDK1.6之后JVM团队对synchronized关键字做了很多优化,性能基本能与ReenTrantLock持平。所以JDK1.6之后,性能已经不是选择 synchronized 和ReenTrantLock的影响因素,而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。
CAS的原理是通过不断的比较内存中的值与旧值是否相同,如果相同则将内存中的值修改为新值,相比于synchronized省去了挂起线程、恢复线程的开销。
// CAS的操作参数
// 内存位置(A)
// 预期原值(B)
// 预期新值(C)
// 使用CAS解决并发的原理:
// 1. 首先比较A、B,若相等,则更新A中的值为C、返回True;若不相等,则返回false;
// 2. 通过死循环,以不断尝试尝试更新的方式实现并发
// 伪代码如下
public boolean compareAndSwap(long memoryA, int oldB, int newC){
if(memoryA.get() == oldB){
memoryA.set(newC);
return true;
}
return false;
}
具体使用当中CAS有个先检查后执行的操作,而这种操作在 Java 中是典型的不安全的操作,所以CAS在实际中是由C++通过调用CPU指令实现的。
具体过程:
AtomicInteger的 i++ 与 i-- 是典型的CAS应用,通过compareAndSet & 一个死循环实现。
private volatile int value;
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
/**
* Atomically decrements by one the current value.
*
* @return the previous value
*/
public final int getAndDecrement() {
for (;;) {
int current = get();
int next = current - 1;
if (compareAndSet(current, next))
return current;
}
}
以上内容引用自学习指南。
Synchronized 与 ThreadLocal(有关ThreadLocal的知识会在之后的博客中介绍)的比较:
参考链接:
synchronized 关键字原理
Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
Java:这是一份全面 & 详细的 Sychronized关键字 学习指南