synchronized 保证三大特性

  • 保证原子性:保证只有一个线程可以拿到锁,进入同步代码块
  • 保证可见性:执行时会定义lock原子操作,会刷新工作内存共享变量的值
  • 保证有序性:加synchronized后,依然会发生重排序,只不过因为加了同步代码块,可以保证只有一个线程执行同步代码块中的代码

synchronizd 的特性

可重入特性

什么是可重入?

一个线程可以多次执行synchronized,重复获取同一把锁

img

可重入性原理:

synchronized的锁对象有一个计数器(recurisons变量)会记录线程获得几次锁,当计数器为0时,就释放锁

好处:

  1. 避免死锁
  2. 可以让我们更好的封装代码

不可中断特性

一个线程获得锁后,另一个线程也要获得锁,那么这个线程处于阻塞或等待状态,如果第一个线程不释放锁第二个线程将会一直阻塞或等待(不可中断)

img

与此对比,lock可以调用lock方法(不可中断)和trylock方法(可中断)

img

img

synchronized 原理

monitor监视器锁

img

monitorenter:

  • synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是jvm线程执行到这个同步代码块,发现锁对象没有monitor就会主动创建(是c++代码创建),其内部有两个重要的成员变量owner:拥有这把锁的线程。recursion:记录线程拥有锁的次数,当减到0次会释放锁

monitorexit:

  • recursion -1
  • 在方法结束处和异常处,jvm保证每个monitorenter都有一个对应的monitorexit

对于同步方法,在反汇编后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter和monitorexit

img

monitor竞争

执行monitorenter时,最终会调用ObjectMonitor::enter(c++代码):

  1. 通过CAS尝试把monitor的owner字段设置为当前线程
  2. 如果之前设置的owner指向当前线程,说明是重入锁,执行recursion++,记录当前重入的次数
  3. 如果是第一次进入该monitor,设置recursion为1,_owner为当前线程,获得成功则返回
  4. 如果获取锁失败,则等待锁的释放(对应ObjectMonitor对象的EnterI方法)

monitor等待

进入EnterI方法:

  1. 先尝试获得两次,如果还是不能则当前线程被封装成ObjectWaiter对象node,状态设置为ObjectWaiter::TS_CXQ
  2. 在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中
  3. node节点被push之后,通过尝试自旋获取到锁,如果还是没有获取到锁,通过park将当前线程挂起,等待被唤醒
  4. 当该线程被唤醒时,会从挂起的点继续执行,通过ObjectWaiter::TryLock尝试获取到锁

monitor释放

具体实现位于ObjectWaiter的exit方法,实现锁的释放:

  1. 退出同步代码块让_recurison -1,当_recurison减到0时,说明锁释放了
  2. 根据不同的策略(QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点包装的线程,唤醒操作最终由unpark完成

monitor是重量级锁

因为函数调用涉及到操作系统用户态和内核态的切换,比如park和unpark等内核函数(在内核态运行),这样切换就会消耗大量的系统资源

img

JDK6 synchronized做的优化

CAS

CAS全称:Compare And Swap(比较相同再交换),是现代cpu对内存中的共享数据进行操作的特殊指令

作用:CAS可以将比较和交换转换为原子操作,这个原子操作由cpu保证。CAS可以保证共享变量赋值的原子操作

CAS依赖3个值,内存中的值V,旧的预估值X,要修改的值B。如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中

比如AtomicInteger的源码中,由Unsafe类提供原子操作。其中,Unsafe类使Java拥有了像C语言指针一样操作内存空间的能力,但是由于出错概率大,因此不能直接调用,只能通过反射获得

img

CAS获取变量时,为了保证变量的可见性,需要用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核cpu的场景

  1. 因为没有使用synchronized,所以线程不会陷入阻塞,提高了效率

  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

为什么value要使用volatile修饰?

因为要保证每个线程读取value内存中的值都是最新值(可见性),然后每次操作的时候再通过CAS判断此时有没有“落后可见性”

java对象头

img

img

其中hashCode在使用时才生成,并且整个存储采用大端存储

img

锁升级

jdk6实现了锁升级

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁

原理:

  1. 执行到一个线程访问同步块时,虚拟机看对象头mark word标志位是否是01,如果是将偏向锁标志位设置为1,表示偏向锁
  2. 通过CAS将mark word 前54位设置成当前线程ID,如果CAS操作成功,下次持有该偏向锁的线程再次进入这个锁相关的同步块时,虚拟机可以不用再进行任何同步操作,效率变高

撤销:

  1. 必须等到全局安全点
  2. 判断锁对象是不是偏向锁
  3. 如果是,恢复到无锁或者升级成轻量级锁

优点:
偏向锁在一个线程执行同步代码块时提高效率,适用于一个线程反复获得锁的情况,提高带有同步但无竞争的程序性能

如果是被多个线程访问比如线程池,就是多余的

在jkd5默认关闭,jdk6默认开启,但是在应用程序启动几秒后才开启。可以通过-XX:BiasedLockingStartupDelay=0关闭延迟

轻量级锁

轻量级锁是相当于monitor的传统锁而言,因此传统的锁机制称为重量级锁。但是,轻量级锁不能够代替重量级锁,因为只有在特定情况下开销小

适合多线程交替执行同步块的情况,但是不适合多线程同一时刻竞争的情况

原理:

  1. 判断当前对象是否处于无锁状态(hashcode,0,01),如果是,则jvm在当前栈帧创建一个锁记录(lock record)的空间,用于存储锁对象目前的mark word的拷贝(官方把这个拷贝加了一个前缀 displaced),将当前对象的mark word赋值到那里,并将lock record的owner指向当前对象
  2. jvm利用cas操作尝试将当前对象的mark word中的指针更新为指向lock record的指针,如果成功表示竞争到了锁,将锁标记置为00,执行同步操作
  3. 如果失败判断当前锁对象的mark word中的指针是否指向当前栈帧,如果是表示当前线程已经持有此对象,直接执行同步代码块。否则说明该锁对象被其他前程占用了,该锁升级为重量级锁。锁标志设置成10,后面等待的线程会进入阻塞状态

img

自旋锁

由于实现重量级锁,线程的阻塞和唤醒需要cpu从用户态转换为内核态,对cpu开销大。并且开发者注意到在许多应用上,共享数据的锁定只会持续很短,为了这点短暂时间阻塞和唤醒线程并不值得。因此我们可以让线程执行一个忙循环(自旋),不放弃处理器的执行时间,这就是自旋锁

在jdk6时引入了自适应自旋锁,意味着自旋的次数不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定

img

锁消除

虚拟机即时编译(JIT)运行时,根据逃逸分析,判断在一段代码中,堆上的所有数据都不可能会逃逸出去被其他线程访问到,那么可以把他们当做栈上数据来对待,认为他们是线程私有的,同步加锁自然无须进行

img

锁粗化

jvm会探测到一连串细小的操作都由同一个对象加锁,将同步代码块的范围放大,放到这串操作的外面,这就是锁粗化

img

平时写代码如何对synchronized做优化

减少synchronized的范围

尽量让synchronized同步的代码尽量短,减少同步代码块的执行时间,减少锁的竞争

降低synchronized锁的粒度

将一个锁拆分为多个锁提高并发度

例如:Hashtable 和 ConcurrentHashMap

img

img

读写分离

读时不加锁,写入和删除时加锁

比如ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteSet