synchronized是最早接触到的锁

synchronized是一种互斥锁,一次只允许一个线程进入被锁住的代码块

synchronized是Java的一个关键字,它能够将代码块/方法锁起来

1.synchronized使用

1.1.synchronized的作用

synchronized的作用主要有三点:

  • 原子性:被synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。
  • 可见性synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
  • 有序性:【防止指令重排】synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

1.2.synchronized使用

synchronized主要有三种用法

  • 修饰实例方法: 作用域当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    1. synchronized void method(){
    2. //...
    3. }
  • 修饰静态方法:给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得 当前 class 的锁

    因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。

  1. synchronized void static method(){
  2. //...
  3. }
  • 修饰代码块:指定加锁对象,对给定对象/类加锁。

    synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

  1. synchronized(this){
  2. //...
  3. }

总结

无论是对一个对象进行加锁还是对一个方法进行加锁,实际上,都是对实例(对象)进行加锁。

2.同步原理

数据同步需要依赖锁,那么锁的同步依赖谁呢? synchronized是在软件层面依赖JVMj.u.c.Lock是在硬件层面依赖特殊的CPU指令

2.1.同步代码块原理

  1. public class SynchronizedDemo {
  2. public void method() {
  3. synchronized (this) {
  4. System.out.println("synchronized 代码块");
  5. }
  6. }
  7. }

使用javap查看字节码信息:

Synchronized - 图1

从图中可以看出:

synchronized同步语句块实现使用的是monitorentermonitorexit指令

其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个 ObjectMonitor对象。 另外,**wait/notify**等方法也依赖于**monitor**对象,这就是为什么只有在同步的块或者方法中才能调用**wait/notify**等方法,否则会抛出**java.lang.IllegalMonitorStateException**的异常的原因。

在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。

在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

2.2.同步方法原理

  1. public synchronized void method() {
  2. System.out.println("synchronized 方法");
  3. }

Synchronized - 图2

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

2.3.总结

两者的本质都是对对象监视器 monitor 的获取。

3.同步概念

3.1.Java对象头

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充

Synchronized - 图3

synchronized用的锁是存在Java对象头里的

对象头由两部分组成

  • Mark Word:存储自身的运行时数据,例如 HashCode、GC 年龄、锁相关信息等内容。【重点】
  • Klass Pointer:类型指针指向它的类元数据的指针。

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。

Synchronized - 图4

3.2.Monitor监视器

每个对象都会有一个与之对应的monitor对象monitor对象中存储当前持有锁的线程以及等待锁的线程队列.

4.锁升级

在JDK6以后,JVM堆synchronized的实现机制进行了较大调整,除了引进CAS+自旋以外,还增加了锁消除、锁粗化、偏向锁、轻量级锁这些优化策略

锁主要存在4种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

锁可以升级,但不能降级!

优化原因:【重量级锁】

在JDK1.6之前默认为重量级锁。线程进入同步方法/代码块时。monitor对象就存储当前线程id,设置Mark Word的monitor对象地址。并把阻塞的线程存储到monitor等待队列。它加锁是依赖底层操作系统的mutex相关指令实现的,所以会有用户态和内核态之间的切换,性能消耗十分明显

引入偏向锁和轻量级锁在JVM层面实现加锁的逻辑,不依赖底层操作系统,没有切换的消耗

4.1.偏向锁

偏向锁是指JVM认为只有某个线程才会执行同步代码【无竞争环境】

当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

当线程访问同步代码并要获取锁时。

1.线程A第一次访问同步块,先检测对象头Mark Word中的锁标志位是否为01【01表示无锁/偏向锁】

2.判断标志位为01后,进一步判断偏向锁位标志是否为1【1表示当前为偏向锁】,如果不是则CAS获取锁,并记录线程id

3.如果当前对象已经为偏向锁,那么判断Mark Word中记录的Thread ID是否为当前线程。如果是则获得锁。

4.如果线程ID不匹配,则用CAS尝试修改当前Mark Word的线程ID,如果当前对象处于【匿名偏向状态】,则会修改成功。

如果修改不成功!则表示出现竞争!!撤销偏向锁

偏向锁的撤销需要等待全局安全点

简单来说: Mark Word记录线程id—->对比线程ID——>相等【获得锁】——->不相等【CAS修改】—->成功【获得】——>失败【升级轻量级锁】

4.2.轻量级锁

在轻量级锁状态下,当前线程会在【栈帧】中创建Lock RecordLock Record会把Mark Word的信息拷贝进去,且有一个Owner指针指向加锁的对象!

当其他线程来执行同步代码块,则试图用CAS将Mark word替换为指向到线程栈帧的Lock Record,假设CAS修改成功,则获取到轻量级锁

假设修改失败,则自旋,自旋一定次数后,升级为重量级锁

总结

synchronized锁原来只有重量级锁,依赖操作系统的mutex指令。需要用户态和内核态切换。性能损耗明显!

  • 重量级锁用到monitor对象,
  • 偏向锁在Mark Word记录线程ID进行比对
  • 轻量级锁则是拷贝Mark WordLock Record用CAS+自旋的方式获取

锁状态情况:

  • 只有一个线程进入临界区,【偏向锁】
  • 多个线程交替进入临界区【轻量级锁】
  • 多线程同时进入临界区【重量级锁】