什么是Synchronized? - Java技术债务

1、作用:

(1)确保线程互斥的访问同步代码

(2)保证共享变量的修改能够及时可见

(3)有效解决重排序问题。

2、用法:

1、修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。

2、修饰静态方法

作用于类的所有对象的锁。

如果线程A调用实例对象的非静态Synchronized方法,而线程B调用这个实例对象所属类的静态Synchronized方法,是允许的,不会发生互斥现象,

因为访问静态Synchronized方法是占用的锁是当前类的锁,而访问非静态Synchronized方法是占用当前实例对象的锁。

3、修饰代码块

指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

什么是Synchronized? - Java技术债务

3、为什么说Synchronized 是非公平锁?

非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。

4、Synchronized底层原理

① Synchronized修饰语句块

使用的是monitorenter和monitorexit标识同步代码块的起始,线程获取锁就是获取monitorenter的持有权,当计数器为0成功获取锁,计数器加一,当执行monitorexit后计数器减一,其他线程可以正常获取锁。

② Synchronized修饰方法

使用ACC_SYNCHRONIZED标识,标识指明该方法是一个同步方法,JVM通过标识判断此方法是否声明的是同步方法,从而执行相应的同步调用。

Java对象头是synchronized实现的关键,Synchronized用的锁是存在Java对象头中

其中 Mark Word 在默认情况下存储着对象的 HashCode、分代年龄、锁标记位等。Mark Word在不同的锁状态下存储的内容不同,在32位JVM中默认状态为下:

5、同步过程(锁升级过程)

synchronized 在开始的时候是依靠操作系统的互斥锁来实现的,是个重量级操作,为了减少获得锁和释放锁带来的性能消耗,在 JDK 1.6中,引入了偏向锁和轻量级锁。锁一共有4中状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几种状态会随着竞争情况逐渐升级,但不能降级,目的是为了提高锁和释放锁的效率。

5.1、偏向锁

大部分情况下,锁不存在多线程竞争,偏向锁就是为了在只有一个线程执行同步块时提高性能。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

5.2、轻量级锁

轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,轻量级锁并不是用来代替重量级锁的,它是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

5.3、重量级锁

什么是Synchronized? - Java技术债务

偏向锁

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。

轻量级锁是为了在线程交替执行同步块时提高性能(通过自旋锁,使无法获得锁的线程无需立即进入阻塞状态,而是在一定时间内循环以获得锁,减少挂起线程和恢复线程带来的消耗)

而偏向锁则是在只有一个线程执行同步块时进一步提高性能(在无多线程竞争情况下,获得锁的线程不释放锁,以减少CAS操作)

偏向锁获取过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为 01——确认为可偏向状态。
  1. 如果为可偏向状态,则测试 Thread ID 是否指向当前线程,如果是,执行同步代码。
  2. 如果不是指向当前线程,使用 CAS 竞争锁,如果竞争成功,则将 Mark Word 中 Thread ID 设置为当前线程ID,并在栈帧中锁记录(Lock Record)里存储当前线程ID。
  3. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint,在这个时间点上没有正在执行的字节码)时,会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着(因为可能持有偏向锁的线程已经执行完毕,但是该线程并不会主动去释放偏向锁)。
  4. 如果线程不处于活动状态,则将对象头设置成无锁状态(标志位为“01”),然后重新偏向新的线程;如果线程仍然活着,撤销偏向锁后升级到轻量级锁状态(标志位为“00”),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

偏向锁的释放

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁的撤销,需要等待全局安全点(这个时间点没有正在执行的字节码)。

  1. 到全局安全点后,先暂停拥有偏向锁的线程,检查该线程是否或者。
  2. 不活动或已经退出代码块,则对象头设置为无锁状态,然后重新偏向新的线程。
  3. 如果仍然活着,则遍历线程栈中所有的 Lock Record,如果能找到对应的 Lock Record 说明偏向的线程还在执行同步代码块中的代码。需要升级为轻量级锁,直接修改偏向线程栈中的Lock Record。
  4. 此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

轻量级锁

偏向锁“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

轻量级锁的加锁过程

  1. 如果锁对象不是偏向模式或已经偏向其他线程,这时候会构建一个无锁状态的mark word设置到Lock Record中去,我们称Lock Record中存储对象mark word的字段叫 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录中。然后虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。
  3. 如果更新成功,当前线程获得锁,执行同步代码。如果更新失败,当前线程便尝试使用自旋来获取锁。
  4. 当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞。

锁释放过程:

  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
  2. 如果替换成功,整个同步过程就完成了。
  3. 如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

重量级锁

轻量级锁膨胀为重量级锁,Mark Word的锁标记位更新为10,Mark Word 指向互斥量(重量级锁)。

Synchronized 的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

   登录后才可以发表评论呦...

专注分享Java技术干货,包括
但不仅限于多线程、JVM、Spring Boot
Spring Cloud、 Redis、微服务、
消息队列、Git、面试题 最新动态等。

想交个朋友吗
那就快扫下面吧


微信

Java技术债务

你还可以关注我的公众号

会分享一些干货或者好文章

Java技术债务