聊一聊synchronize关键字

Java并发编程中,锁是一个绕不开的话题。锁都是基于对象的,Java中每一个对象都可以作为一个锁。值得注意的是,我们所说的类锁也是基于对象的,因为一个类唯一对应着一个Class对象。

Synchronize关键字

锁通常与 synchronize 关键字联系到一起,synchronize有三种使用方法:分别是(1)作用在实例方法上,锁为当前实例;(2)作用在静态方法上,锁为当前Class对象;(3)作用在代码块上,锁为括号里的对象。

// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

在上面的例子中,如果synchronize关键字作用在方法上,那么临界区就是整个方法内部;如果作用在代码块,那么临界区指的就是代码块内部的区域。

通过上面的例子可以看出,下面的写法是等价的:

// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this) {
        // code
    }
}

同理,下面的写法也是等价的:

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this.getClass()) {
        // code
    }
}

四种锁

Java6以后为了减少获得锁和释放锁所带来的性能开销,引入了“偏向锁”和“轻量级锁”。而在Java6以前,所有的锁都是重量级锁。因此现在共有四种锁的状态,从低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

无锁就是并不会对资源进行任何的锁定,任何线程都可以随意改变他,因此在这里不做细讲。

几种锁会随着竞争的情况的升级,锁的升级也很容易发生。下面分别介绍几种锁和他们的升级。

Java对象头

前文中提到,Java的锁都是基于对象的,一个对象中锁的信息存在于对象头中。每个对象都有对象头。如果是非数组类型,则有两个字宽来存储对象头;如果是数组类型,则有三个字宽来存储对象头。字宽是由处理器的位数决定的,如下所示:
在这里插入图片描述
其中Mark Word部分存储着锁信息,下面探秘一下Mark Word格式:
在这里插入图片描述
可以发现,当对象状态为偏向锁时,Mark Word中存储的是偏向线程的ID;当状态为轻量级锁时,Mark Word存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时,Mark Word为指向堆中monitor对象的指针。

(1)偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

实现原理如下:

  1. 一个线程第一次进入到同步块时,会在对象头和栈帧的锁记录中存储锁偏向的线程ID。当下一次该线程进入到这个同步块时,会检查锁的Mark Word里面是否放置的是自己的线程ID。
  2. 如果是,则该线程已经获得了锁,以后该线程进入和退出同步代码块时不需要额外花费CAS操作来进行加锁和解锁;如果不是,就代表有另一个线程来竞争这个偏向锁。这时就尝试通过CAS操作替换Mark Word里面的线程ID为新的线程ID,需要分为如下两种情况:
  3. (1)如果成功,则表明之前占有锁的线程已经不存在了,Mark Word里面的线程为新线程ID,锁不会升级;(2)如果失败,表明原来的线程还在,此时设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行锁的竞争。

线程竞争偏向锁的过程如下图所示;
在这里插入图片描述
偏向锁的撤销

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

当偏向锁升级为轻量级锁,会暂停拥有偏向锁的线程,重置偏向锁的标识,这个过程开销是比较大的,具体来说分为下面三个步骤:

  1. 在一个安全点(在这个时间没有字节码正在执行)停止拥有锁的线程;
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Word,使其成为无锁状态;
  3. 唤醒被停止的线程,将当前锁升级为轻量级锁。

下图中总结了偏向锁的获得与撤销:
在这里插入图片描述

(2)轻量级锁

多个线程在不同时间段获取同一把锁,即不存在锁竞争的情况,也就没有线程的阻塞。针对这种情况,JVM采用轻量级锁来避免线程的阻塞与唤醒。

轻量级锁加锁

JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到自己的Displaced Mark Word里面。

然后线程尝试用CAS将锁的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。

自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。

但是JDK采用了更机智的方式——适应性自旋,就是线程如果自旋成功了,则下次自旋的次数会更多;如果自旋失败了,则自旋的次数就会减少。

自旋也不是一直进行下去的,如果自旋到一定程度(和JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁。

轻量级锁释放

在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。

一张图说明加锁和释放锁的过程:
在这里插入图片描述

(3)重量级锁

重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

前面说到,每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:

在这里插入图片描述
当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到Contention List的队列的队首,然后调用park函数挂起当前线程。

当线程释放锁时,会从Contention List或EntryList中挑选一个线程唤醒,被选中的线程叫做Heir presumptive即假定继承人,假定继承人被唤醒后会尝试获得锁,但synchronized是非公平的,所以假定继承人不一定能获得锁。这是因为对于重量级锁,线程先自旋尝试获得锁,这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。

如果线程获得锁后调用Object.wait方法,则会将线程加入到WaitSet中,当被Object.notify唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。

总结锁的升级流程

  1. 每一个线程在获取共享资源时,首先检查MarkWord中存放的是不是自己线程的ID,如果是,则表示当前线程是处于“偏向锁”;
  2. 如果Mark Word中不是自己的ID,进行锁升级。此时使用CAS来执行切换,新的线程会根据MarkWord的里现有的线程ID,通知之前的线程暂停,之前线程将MarkWord的内容置为空;
  3. 两个线程都把锁对象的Hashcode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把锁对象的MarkWord字段修改为自己新建的记录空间地址的方式来竞争MarkWord;
  4. 第三步中成功执行CAS的获得资源,失败的则进行自旋;
  5. 自旋的线程在自旋过程中,成功获得资源(即之前获得资源的线程执行完成并且释放了共享资源),则整个状态依然处于轻量级锁状态;如果失败则进入重量级锁的状态,此时自旋的线程进行阻塞,等待之前线程执行完毕并唤醒自己。

各种锁的优缺点对比

在这里插入图片描述

参考:http://concurrent.redspider.group/article/02/9.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值