Java多线程---我是这样理解synchronized的

学了好长时间的synchronized,写篇博客对synchronized做些总结。

synchronized

简介

它是一个Java的关键字,它可以用来对Java对象、方法、代码块提供线程安全,可以理解为它是一个悲观的独占锁,也是一种可重入锁。这句话说出了三个点:悲观锁、独占锁、可重入锁

什么是悲观锁呢?

我们可以详细介绍下乐观锁和悲观锁的概念

  • 乐观锁:乐观锁的实现思想就是,当我去读取数据的时候,我认为别人不会修改该数据,而当我去更新数据的时候我会采用CAS操作来判断别人有没有更新该数据,具体过程:比较当前版本号与上一次的版本号,如果版本号一致,就进行更新,如果不一致就重复进行读、比较、写操作。这里提到的CAS可以简单理解为一种原子更新操作
  • 悲观锁:悲观锁的实现思想就是,在我每次读取数据的时候都认为别人会修改数据,所以每次读写数据的时候都会加锁,这样别人想读取该数据的时候就会阻塞,等待直到获取锁

什么是独占锁呢?

独占锁和共享锁:

  • 独占锁:也叫排他锁,每次只允许一个线程持有该锁
  • 共享锁:允许多个线程同时获取该锁,并发访问共享资源。

什么是可重入锁呢?

可重入锁的话,可以理解为当一个线程获取到该对象的锁时,然后递归调用该对象的方法不会产生死锁的情况。不可重入锁的概念就是:当你获取到该锁之后,访问该对象的方法,需要先把当前锁释放才能访问。

加锁方式

我们都知道每个对象都有一个monitor对象,竞争锁其实就是在竞争这个monitor对象,对于同步代码块,有monitorEnter和monitorExit获取锁资源和释放锁资源,对于同步方法则是通过一个标志位ACC_SYNCHORNIZED,当调用一个同步方法时,会判断这个标志位是否被标记,被设置之后会持有这个monitor对象,无论正常/非正常的完成,都会释放掉该锁,如果出现异常,会将异常抛到线程外面去

缺点

它是一个重量级锁,依赖底层Mutex Lock实现,线程之间切换需要从用户态到内核态的转换,转换的时间比较长,开销比较大

优化

在jdk1.6,从JVM层面对synchronized进行了优化,引入了适应性自选、锁消除、锁粗化、轻量级锁、偏向锁等来提高锁的效率。

适应性自旋

这里提到自旋锁,自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待获取锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需要等一等(自旋)

自旋锁的优缺点

  • 优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或者锁竞争不激烈的代码块来说性能会有很大的提升,因为自旋的CPU耗时明显要少于线程挂起、再唤醒两次上下文切换所用的时间
  • 缺点:当持有锁的线程占有时间很长或者锁竞争非常激烈的情况时,线程在自旋中会长时间获取不到锁资源,将引起CPU的浪费

自旋锁的优化

jdk1.4自旋锁被引入,但是默认是关闭的,jdk1.6默认开启,在jdk1.5,对自旋锁进行了优化,将自选时间设定为某一确定的值,这样可以避免长时间获取不到锁资源而引起CPU的资源浪费,在jdk1.6更加只能,这个自旋时间不再固定,而是由上次自旋获取到同一把锁的时间和锁的拥有者的状态来决定,也就是适应性自旋

锁消除

锁消除:JIT编译时,会对运行上下文进行一次扫描,去除不可能存在竞争的锁,就比如当一个资源只在当前方法中处理,不返回出去,不可能被其他线程引用

锁粗化

锁粗化:拿StringBuffer来讲,当进行大量的循环进行append()方法时,StringBuffer是线程安全的,append方法上有synchronized关键字修饰,也就是说每次进行append方法都会牵扯到加锁、解锁的过程,JVM检测到这种情况,会将锁粗化,也就是将锁粗化到整个StringBuffer

偏向锁

它的引入目的就是 减少同一线程获取锁的代价

  • 大多数情况下,锁不存在多线程竞争,都是由同一线程去获取锁
  • 核心思想:如果一个线程获取到该锁,那么锁就进入到偏向模式,此时Mark Word的结构就变为偏向结构,当该线程再次请求锁时,无需进行任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记为偏向锁以及当前线程的id等于Mark Word的ThreadID即可
  • 不适用于锁竞争激烈的情况,这种场合下,偏向锁就失效了,因为每次获取锁的ThreadID都是不同的

轻量级锁

它是由偏向锁升级而来的,适用于线程交替执行的场合,当偏向锁运行在一个线程进入同步块的时候,当另一个线程参与争用该锁资源的时候,偏向锁会升级为轻量级锁,当同一时刻有多个线程竞争锁资源的时候,会将轻量级锁升级为重量级锁。

轻量级锁在JVM中的加锁过程

  1. 在代码进入到同步代码块时,如果同步对象锁的状态为无锁状态(锁标记位为"01"状态),JVM首先将当前线程创建一个Lock Record的空间,用于存储对象的Mark Word拷贝,官方称之为Displaced Mark Word
  2. 拷贝对象头的Mark Word复制到锁记录当中
  3. 拷贝成功后,JVM将使用CAS操作尝试将对象头的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向Object Mark Word,如果更新成功就进行步骤4,否则进行步骤5
  4. 如果更新成功,那么该线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为"00",表示该锁是轻量级锁
  5. 如果更新失败,JVM会首先检查对象的Mark Word的指针是否指向了Lock Record,如果指向了,说明该线程拥有了该锁,则继续执行代码块中代码,否则说明有多个线程参与竞争,那么JVM会将轻量级锁升级为重量级锁,所得标志位为"10",对象的Mark Word的指针指向重量级锁的指针,后面等待锁的线程也将进入到阻塞状态,而当前线程则尝试自旋来获取锁

轻量级锁的释放锁的过程

  1. JVM会尝试使用CAS操作将Displaced Mark Word中的对象替换当前的Mark Word
  2. 如果操作成功,整个同步过程也就结束了
  3. 如果操作失败,则说明,由别的线程尝试过获取该锁,那就要在释放锁的同时唤醒被挂起的线程

猜你喜欢

转载自blog.csdn.net/MarkusZhang/article/details/106175780