转载

深入了解Synchronized原理

同一个时间只允许一个线程拥有一个对象锁,这样在同一时间只有一个线程对需要同步的代码块进行访问

可见性

必须确保在某个线程的某个对象锁在释放之前,对某个共享变量所做的改变,对于下一个拥有在这个对象锁的线程是可见的,否则另外线程读取的是本地的副本从而进行操作,导致结果不一致。

重入性

从互斥锁的设计上来说,一个线程试图操作一个由其他线程持有的临界资源的时候,这个线程会处于堵塞状态。

如果一个线程再次请求自己持有对象锁的临界资源的时候,这就属于重入锁。

因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

获取对象锁的方式

获取对象锁的方式

  1. 修饰实例方法,作用于当前实例加锁,进行同步代码块之前需要获得当前实例的锁(Synchronized method)
  2. 修饰代码块,指定加锁对象,作用于给定对象加锁,进入同步代码快之前要获得给定对象的锁(Synchronized instance)
  3. 修饰静态方法,作用于当前类对象加锁,进入同步代码块之前要获得当前类对象的锁(Synchronized static method)
  4. 修饰类对象,作用于类对象加锁,进入同步代码块之前要获得指定类对象的锁(Synchronized **.class)

对象锁和类锁的区别

  1. 一个线程可以访问对象的同步代码块时,另外一个线程也可以访问同一个对象的非同步代码块
  2. 若锁住的是同一个对象,其他线程访问对象的同步代码块或者同步方法的时候会被阻塞
  3. 同一个类的不同对象的对象锁互不干扰
  4. 类锁是一种特殊的锁,因为类就是Class的实例,所以只要不同对象都是属于同一个类,那么他们的类锁都是一样的
  5. 类锁和对象锁互不干扰

底层原理

深入了解Synchronized原理

锁对象存储在Java对象头里面

位数 头对象结构
32 Mark word 存储对象的HashCode,GC分代年龄,锁类型,锁标记
32 Class MeteDataAddress 类型指针:指向实例对象所属的类

MarkWord被设定为一个非固定的数据结构,用来存储更多的数据,结构如下(这里不是很懂)

深入了解Synchronized原理

Monitor(内部锁,Monitor锁,管程,监视器锁,也就是和对象锁对应的对象)

每个对象都存在这一个Monitor与之关联

每个Java对象天生带有这把看不见的锁,在MarkWord的结构中,重量级锁的标记为是10,也就是指针就是指向Monitor对象的起始地址,在这里也就说明了Synchronized的默认锁是重量级锁。monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当 一个 monitor 被某个线程持有后,它便处于锁定状态

在Java虚拟机中,Monitor是有MonitorObject所实现的,部分结构如下

深入了解Synchronized原理

_owner:指向持有ObjectMonitor对象的线程

_WaitSet:存放处于wait状态的线程队列

_EntryList:存放处于等待锁block状态的线程队列

_count:用来记录该线程获取锁的次数

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向 持有 ObjectMonitor对象的线程,当有多个线程访问同一块同步代码块的时候,线程会线程会进入_EntryList,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用 wait() 方法, 将释放当前持有的monitor,owner变量恢复为null ,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。

Monitorenter和Monitorexit

Synchronized代码块执行原理

字节码中可知同步语句块的实现使用的是 monitorentermonitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置 。当执行monitorenter指令时,如果当前线程获取 对象锁所对应的monitor的特权 的时候

1 会去检查monitor的对象的count是否为0

2 如果为0的话就获取成功,并且将count置为1

3 倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。

编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。一般字节码文件中都会多出一条monitorexit指令。

Synchronized方法执行原理

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。

如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放

锁的类型

自旋锁

synchronized在jdk1.6之前的锁是重量级锁,对于互斥同步的性能来说,阻塞挂起的是影响最大的。因为挂起线程和恢复线程都是要让操作系统从 用户态 转化到 内核态 中完成,而这两个状态的转换是比较影响性能的。

大多数情况下,线程拥有锁的时间不会太长,如果直接挂起的话,会影响系统的性能。因为前面说过,线程切换是需要在操作系统的用户态和内核态之间转换的。所以为了解决这个问题,引进了自旋锁。

自旋锁假设在不久,当前线程可以获得这个锁,因此JVM就让这个想要获得锁的线程,先做几个空循环先,让这个线程先不要放弃占有CPU资源的机会,经过若干次空循环之后,如果获得锁,那么就顺利的进入临界区。否则,你也不能让这个线程一直占有CPU资源呀,所以经过大概10次空循环之后,就只能老老实实地挂起了。

自旋适应锁

自旋适应锁就是从自旋锁改进而来的。在自旋锁的基础上,假如A线程通过自旋一定的时间之后获得了锁,然后释放锁。这时B线程也获得了这个锁,如果此时A线程再次想得到这个锁,那么JVM就会根据之前A线程曾经获得过这个锁,那么我就给你适当地增加一点空循环的次数,比如说从10次空循环到100次。假如有个C线程,他也想获得这个锁,也得自旋等待,可是很少轮到他或者没得到过这个锁(可能是被A抢了机会或者其他的),那么JVM就会认为C线程以后可能没什么机会获得了,就适当地减少C线程的空循坏次数甚至不让他做空循环。

偏向锁

如果A线程第一次获得锁,那么锁就进入偏向模式(虚拟机把对象头中的标志位设为“01”),MarkWord的结构也变成偏向锁结构,如果没有其他线程和A线程竞争,A线程再次请求该锁时,无需任何同步操作

只需要检查MarkWord的锁标记位是否为偏向锁和当前线程的Id是否为ThreadId即可。

也就是说当一个线程访问同步块并且获取锁的时候,会通过 CAS操作 在对象头的偏向锁结构里记录线程的ID,如果记录成功,线程在进入和退出同步块时, 不需要进行CAS操作来加锁和解锁 ,从而提高程序的性能。

TIPS:偏向锁只能被第一个获取它的线程进行 CAS 操作,一旦出现线程竞争锁对象,其它线程无论何时进行 CAS 操作都会失败。

加锁具体步骤如下

  1. 先检查Mark Word是否为可偏向状态,也就是说是否 是偏向锁1,锁标识位为01

  2. 如果是 可偏向状态 ,那么就测试Mark Word结构的线程ID是不是和当前线程的ID一致,

    如果是就直接执行同步代码块。

    如果不是就通过CAS操作竞争锁,

    ​ 如果操作成功,就把Mark Word的线程ID设置为线程的ID

    ​ 如果操作失败,那么就说明此时有 多线程竞争 的状态,等到安全点,获得偏向锁的线程就挂起,进行解锁操作。偏向锁升级为轻量锁,被阻塞在安全点的线程继续往下执行同步代码块。

解锁

当获得偏向锁的 线程挂起 之后,就会进行解锁操作。

在解锁成功之后,JVM判断此时线程的状态,

如果还没有执行完同步代码,则直接将偏向锁升级为轻量级锁,然后继续执行剩下的代码块。

如果此时已经执行完同步代码,则撤销锁为 无锁状态 ,以后执行同步代码的时候JVM则会直接升级为轻量锁。

深入了解Synchronized原理

轻量锁(加锁解锁操作是需要依赖多次CAS原子指令的)

偏向锁一旦受到多线程竞争,就会膨胀为轻量锁

获取锁

  1. 先判断当前对象是否处于无锁状态,如果是,JVM就首先在想要获取这个锁的线程的栈帧中建立一个锁记录(Lock Record)的空间,其中header部分用来存储Mark Word的备份,否则执行3。
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向锁记录的指针,如果成功,那么就获得轻量锁,就将标志位设置为00,执行同步代码块,否则执行3。
  3. 判断当前对象的Mark Word是否指向当前想要竞争的线程的锁记录,如果是表示则该线程拥有这个轻量锁,继续执行同步代码块,也就是重入。否则,说明这个轻量锁已经被其他线程拥有,那么这个先进行 自旋 获取锁,如果一直没有得到锁,那么轻量锁则要膨胀为重量锁(也就是将标记为设置为10),锁标记设置为10,后面等待的线程则会进入阻塞状态,如果通过自旋成功获取了锁,那么轻量锁不会膨胀为重量锁。

释放锁

  1. 取出线程锁记录之前保存的轻量锁的Mark Word记录,通过CAS操作将取出的记录替换当前对象的Mark Word中
  2. 判断当前对象的Mark Word是否指向当前线程的锁记录
  3. 如果1,2都成功,那么就成功释放锁
  4. 如果1失败,那么就是之前有过线程对当前对象的锁竞争过,但是失败了,由轻量级锁变为重量级锁,导致Mark Word的结够发生了改变。那么后面就释放锁,唤醒等待的线程,进行新一轮的竞争。
深入了解Synchronized原理

重量级锁

重量级锁通过对象内部的监视器(monitor)实现

其中monitor的本质是依赖于底层操作系统的Mutex Lock实现

操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

锁的升级

锁主要存在四种状态,无状态锁,偏向锁,轻量锁,重量锁,会随着线程竞争的程度逐渐增大。锁只可以单向升级,不可以降级。

主要是为了提高获得锁和解锁的效率。

原文  https://juejin.im/post/5ccd84dee51d456e3428c1af
正文到此结束
Loading...