Java多线程-synchronized与锁

synchronized关键字

synchronized翻译成中文就是同步的意思。我们通常使用synchronized关键字来给一段代码或一个方法上锁。其主要有以下三种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  //关键字在实例方法上,锁为当前实例
public synchronized void instanceLock(){
//code
}
//关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock(){
//code
}
//关键字在代码块上,锁为括号里面对象
public void blockLock(){
Object o = new Object();
synchronized (o){
//code
}
}

java中的锁都是对象锁,我们常说的类锁也是对象锁。Java类只要一个Class对象,多个实例对象共享这一个Class对象。类锁就是Class对象的锁。

Java中临界区指的是某一代码区域同一时刻只能由一个线程执行。synchronized关键字如果加在方法上,那么整个方法都是临界区;如果加载代码块上,临界区就是代码块内部区域。

偏向锁、轻量级锁与重量级锁

Java6为了减少获得和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。Java6以前,所有的锁都是重量级锁。

一个对象有4种锁状态,从低到高分别为:

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

无锁就是没有对资源进行锁定,任何线程都可以去执行。

几种锁会随着竞争情况逐级升级,锁的升级很容易发生,但降级条件很苛刻。

Java对象头

每个Java对象都有对象头,如果是非数组类型,则用2个字宽来存储对象头,如果是数组,则会用3个字宽存储对象头。32位机器中,一个字宽是32位;64位虚拟机中,一个字宽是64位。对象头的内容如下表:

长度 内容 说明
32/64bit Mark Word 存储对象的hashCode或锁信息等
32/64bit Class Metadata Address 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果是数组)

其中,Mark Word的格式如下:

锁状态 29或61bit 1bit是否是偏向锁 2bit锁标志位
无锁 0 01
偏向锁 线程ID 1 01
轻量级锁 指向栈中锁记录的指针 此时这一位不用于标识偏向锁 00
重量级锁 指向互斥量(重量级锁)的指针 此时这一位不用于标识偏向锁 10
GC标志 此时这一位不用于标识偏向锁 11

当对象状态为偏向锁时,Mark Word中记录的是线程ID;当状态是轻量级锁时,Mark Word中存储的是指向线程栈中Lock Record的指针;当状态是重量级锁时,Mark Word为指向堆中的monitor对象的指针。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,还总是由同一线程多次获得,于是引入了偏向锁。

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

CAS: Compare And Swap

比较并设置。

实现原理

一个线程在第一次进入同步块时,会在对象头和栈帧的锁记录中存储锁的偏向的线程ID。当下次该线程进入这个同步块时,会去检查锁的Mark Word中是否存在自己的线程ID。

如果是,代表当前线程已获得锁,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁。

如果不是,代表有另一个线程来竞争这个偏向锁。这时会尝试用CAS操作来替换Mark Word中的线程ID为新线程的ID,有两种结果:

  • 如果成功,代表之前的线程不存在了,Mark Word中为新线程的ID,锁不会升级,仍然为偏向锁。
  • 如果失败,表示之前的线程依然存在。暂停之前的线程,设置锁标识为0,并设置锁标识位为00,升级为轻量级锁,按照轻量级锁的方式竞争锁。

线程竞争偏向锁的过程如下:

线程竞争偏向锁的过程.JPG

图中涉及到了lock record指针指向当前堆栈中最近的一个lock record,是轻量级锁按照先来先服务的模式进行轻量级锁的加锁。

撤销偏向锁

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

偏向锁升级为轻量级锁时,会暂停拥有偏向锁的线程,这个过程的开销是很大的。

偏向锁的获得和撤销流程如下:

偏向锁的获得和撤销流程.png

轻量级锁

多个线程在不同时间段获得同一把锁,不存在锁竞争的情况,也就没有线程阻塞。这种情况下,可以采用轻量级锁来避免线程的阻塞和唤醒。

轻量级锁的加锁

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

然后线程尝试把锁的Mark Word更改为指向自己的锁记录的指针。如果成功,当前线程获得锁;如果失败,表示Mark Word已经被替换为其他线程的锁记录,有其他线程正在竞争锁,当前线程则尝试使用适应性自旋来获得锁。

线程的自旋是会消耗CPU资源的,如果一直处于自选状态就会白白浪费CPU资源,所以JDK采用了适应性自旋的方式,就是如果线程自旋成功了,下次自旋的次数就会更多,如果失败了,自旋的次数就会减少。

当自旋一定次数后,依然没有获得锁,称为自旋失败,这个线程会阻塞。锁也会升级为重量级锁。

轻量级锁的释放

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

轻量级锁及膨胀流程图如下:

轻量级锁及膨胀流程图.png

重量级锁

重量级锁是依赖操作系统的互斥量(mutex)来实现的,而操作系统中线程间状态的转换需要较多的时间,因此重量级锁的效率很低,但被阻塞的线程不会消耗CPU。

每个对象都可以当做一个锁,当多个线程请求一个对象锁时,对象锁会设置几个状态来区分请求的线程。

  • Contention List:所有请求锁的线程都会被首先放置到该竞争队列
  • Entry ListContention List中那些有资格成为候选人的线程被移到Entry List
  • Wait Set:调用wait方法被阻塞的线程进入Wait Set
  • OnDeck:任何时刻最多只有一个线程在竞争锁,这个线程被称为OnDeck
  • Owner:获得锁的线程被称为Owner
  • !Owner:释放锁的线程

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

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

如果线程获得锁后调用Object.wait()方法,则会将线程加入Wait Set中,当被notify唤醒后,会将线程移到Contention ListEntry Set中去。当调用一个锁对象的waitnotify方法时,如果当前锁的状态是偏向锁或轻量级锁,则会先膨胀为重量级锁。

锁的升级流程总结

  1. 每一个线程在准备获取共享资源时,第一步先检查Mark Word中存放的是不是自己的ThreadId,如果是,当前线程处于偏向锁。
  2. 如果Mark Word不是自己的ThreadId,锁升级为轻量级锁。这时,采用CAS来执行切换,新的线程利用Mark Word中现有的ThreadId通知之前的线程暂停,之前的线程将Mark Word置为空。
  3. 两个线程都把锁对象的HashCode复制到自己新建的用于存储锁记录的空间,接着通过CAS操作把锁对象的Mark Word修改为指向自己新建的存储锁记录的空间的地址,通过这种方式来竞争锁。
  4. 成功执行CAS的获得锁,失败的进入自旋
  5. 自旋的线程在自旋中成功获得锁(之前获取锁的线程执行完毕并释放了锁),依然处于轻量级锁的状态,如果多次自旋失败,升级为重量级锁。
  6. 升级为重量级锁后,自旋的线程进入阻塞,等待之前线程执行完毕并唤醒自己。

各种锁的对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法的性能差距很小 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块的情况
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁,竞争的线程会消耗CPU资源 追求响应时间。同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间慢 追求吞吐量。同步块执行时间较长
-------------本文结束感谢您的阅读-------------