Fork me on GitHub

【Java多线程】synchronized 关键字

synchronized 关键字

特点

  • 在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
  • 当我们调用某对象的synchronized方法时,就获取了该对象的同步锁
  • 不同线程对同步锁的访问是互斥的

规则

当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程

  • “该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
  • 仍然可以访问“该对象”的非同步代码块
  • “该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞

用法

修饰代码块

同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。(实例锁)

synchronized代码块可以更精确的控制冲突限制访问区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Test
{
// 锁定当前对象
public void method1() {
synchronized(this)
{
// todo
}
}

//锁定指定对象obj
public void method2(Object obj) {
synchronized(obj)
{
// todo
}
}

private byte[] lock = new byte[0]; // 特殊的instance变量
//当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁
//说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码
public void method3()
{
synchronized(lock) {
// todo 同步代码块
}
}
}

修饰方法

  • synchronized修饰方法,作用范围为整个函数
  • synchronized关键字不能被继承,子类重写父类synchronized方法,想要同步需要添加synchronized关键字,或者调用父类同步方法
  • 定义接口方法时不能使用synchronized关键字
  • 构造方法不能使用synchronized关键字,可以用synchronized代码块来进行同步

用法:

1
2
3
4
5
6
7
8
9
10
11
12
public synchronized void method()
{
// todo
}

//等同于
public void method()
{
synchronized(this) {
// todo
}
}

修饰静态方法

synchronized修饰的静态方法锁定的是这个类的所有对象(全局锁)

用法:

1
2
3
public synchronized static void method() {
// todo
}
  • 实例锁

    锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。

    实例锁对应的就是synchronized关键字。

  • 全局锁

    该锁针对的是对象对应的Class实例,而Class实例存于永久,无论实例多少个对象,那么线程都共享该锁。

    全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)

示例:

1
2
3
4
5
6
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}

假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况。

  • x.isSyncA()与x.isSyncB()

不能同时被访问。isSyncA() 和 isSyncB() 都是访问同一个对象(对象x)的同步锁

  • x.isSyncA()与y.isSyncA()

可以同时被访问。因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。

  • x.cSyncA()与y.cSyncB()

    不能被同时访问。因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。

  • x.isSyncA()与Something.cSyncA()

    可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。

修饰类

synchronized作用于类时,是给这个类加锁,类中所有对象用的是同一把锁

用法:

1
2
3
4
5
6
7
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}

底层原理

字节码

synchronized 语句

对于synchronized语句当Java源代码被javac编译成bytecode的时候,会在同步块的入口位置和退出位置分别插入monitorentermonitorexit字节码指令,这两个指令通过一个reference类型的参数来指明要锁定和解锁的对象

  • monitorenter监视器准入指令

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
  • monitorexit监视器释放指令
  1. 执行monitorexit的线程必须是object所对应的monitor的所有者。
  2. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

monitorentermonitorexit依赖于底层操作系统的Mutex Lock,而使用互斥锁需要将当前线程挂起,并从用户态切换到内核态来执行,代价高昂

synchronized 方法

synchronized方法被翻译成普通的方法调用和返回指令如:invokevirtualareturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象

对象头

HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头(Object Header)包括两部分信息:

第一部分”Mark Word“:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等.

第二部分”Klass Pointer“:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )

32位HotSpot虚拟机对象头(Mark Word):

1530861009468

锁优化

适应性自旋

使用互斥同步,挂起线程和恢复线程的操作需要转入内核态完成,对系统的并发性能影响较大。可以使请求锁的线程等待,不放弃处理器的执行时间,执行忙循环(自旋),看持有锁的线程是否很快释放锁;当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态

如果锁被占用的时间很长,自旋的线程将白白消耗处理器资源,带来性能上的浪费,自旋有一定的限定次数

自适应自旋锁: 自旋时间不固定,由前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定

锁粗化

减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁

锁消除

锁消除指虚拟机JIT编译器在运行时,对一些代码上要求同步,但检测到不可能存在共享数据竞争的锁进行消除

依据于运行时JIT编译器的逃逸分析技术

轻量级锁

在进入同步块的时候,如果对象没有被锁定,在当前线程的栈帧中建立一个锁记录的空间(Monitor Record列表),用于存储对象的Mark Word(称为Displaced Mark Word)。虚拟机使用CAS操作将对象的Mark Word更新为指向Monitor Record的指针,更新成功则线程拥有该对象的锁;更新失败,如果对象Mark Word不是指向当前线程的栈帧,则膨胀为重量级锁,Mark Word存储的就是指向重量级锁的指针

Monitor Record 结构:

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
  • EntryQ: 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
  • RcThis: 表示blocked或waiting在该monitor record上的所有线程的个数。
  • Nest: 用来实现重入锁的计数。
  • HashCode: 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
  • Candidate: 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

偏向锁

为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟

偏向锁,轻量级锁的状态转换关系:

偏向锁,轻量级锁的状态转换关系

不同锁的比较:

参考资料

《深入理解Java虚拟机》

http://www.importnew.com/21866.html

http://www.cnblogs.com/skywang12345/p/3479202.html

http://www.open-open.com/lib/view/open1352431526366.html