JAVA并发编程-synchronized解析

Scroll Down

JAVA并发编程-synchronized解析

synchronized的使用方式

  • 修饰实例方法,需要获取到当前对象的实例锁才可以进入
  • 修饰静态方法,对当前类进行加锁,在进入同步代码块时需要获得当前类的锁,因为静态方法只属于类而不属于对象
  • 修饰代码块,比修饰方法粒度更小,可以指定加锁的对象,获得对应加锁对象锁之后可以进入同步代码块.

synchronized内部锁介绍

Java虚拟机会在monitorenter对应的指令后临界区开始前的地方插入一个获取屏障,临界区结束后monitorexit对应的指令前插入一个释放屏障,这样可以保障临界区内的任何读写都无法重排序到临界区之外,使得临界区内的操作具有原子性.

image-20200405012420481

synchronized关键字对有序性的保障与volatile关键字对有序性的保障实现原理是一样的,通过释放屏障和获取屏障的配对使用实现的,释放屏障使得临界区内的写操作先于monitorexit操作被提交,获取屏障使得读线程必须获得锁之后才能执行临界区内的操作.

java虚拟机对内部锁的优化

在jdk1.6之后,java虚拟机对内部锁的实现进行了一些优化包括:

  • 锁消除
  • 锁粗化
  • 偏向锁
  • 适应型锁(自旋锁)

锁消除

锁消除是JIT编译器对内部锁的具体实现所做的一种优化,在动态编译同步块的时候,JIT编译器借助一种称为逃逸分析的技术来判断同步块的代码是否只能被一个线程访问而没有被发布到其他线程,如果同步块所使用的锁对象通过这种分析被证明只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候并不生成synchronized所表示的机器码,仅生成临界区代码对应的机器码,这时候动态编译的字节码就是不包含monitorenter和monitorexit两个字节码指令一样,即消除了锁的使用

锁粗化

锁粗化是相邻的几个同步代码块如果使用的是同一个锁实例,那么JIT编译器会把这些同步块合并成一个大同步块,从而避免了一个线程反复申请,释放同一个锁所导致的开销,当然锁粗化也会导致线程持有锁的时间变长,而即使是相邻的代码块有其他语句,也可能会被指令重排序到临街区内,这样也可能会被锁粗化所优化

public void test(){
	syncronized(this){
		doA();
	}
	syncronized(this){
		doB();
	}
	syncronized(this){
		doC();
	}
}
public void test(){
    syncronized(this){
		doA();
        doB();
        doC();
	}
}

偏向锁

偏向锁是JAVA虚拟机对锁的实现做的一种优化,大多数锁在使用中没有被争用并且在锁的生命周期中也只会被至多一个线程持有

引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉

偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。

但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。

轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁

适应型锁(自旋锁)

适应型锁是JIT编译器对内部锁实现所做的一种优化,存在锁争用的情况下,一个线程申请一个锁的时候该锁正好被其他锁持有,导致需要暂停线程,而暂停线程会导致上下文切换,产生更大的开销,而一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙等(自旋),这项技术就叫做自旋

//自旋
while(lockIsHeldByOtherThread)

而Java虚拟机默认是关闭自旋锁的,需要启用的话要在参数中增加--XX:+UseSpinning来开启,并且java虚拟机可以对一个实例进行自旋,另一个实例采取暂停切换上下文的策略.