简介
synchronized它能用来同步方法和代码块,那 synchronized 到底锁的是什么呢?
首先需要明确的一点是:Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁。
Class 对象是一种特殊的 Java 对象,代表了程序中的类和接口。Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。
Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。
我们可以通过 Class 对象来获取类的元数据,甚至动态地创建类的实例、调用类的方法、访问类的字段等。也就是Java反射机制的实现。
获取Class对象的三种方法:
对象.getClass() --> 调用对象的getClass()方法
类.class --> 调用类的class属性
Class.forName("类的全路径") --> 调用Class类中的forName()静态方法
所以我们常说的类锁,其实就是 Class 对象的锁。
锁的基本用法
synchronized 翻译成中文就是“同步”的意思。
我们通常使用synchronized关键字来给一段代码或一个方法上锁,它通常有以下三种形式:
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
这里介绍一下“临界区”的概念。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。在上面的例子中,如果synchronized关键字在方法上,那临界区就是整个方法内部。而如果是 synchronized 代码块,那临界区就指的是代码块内部的区域。
通过上面的例子我们可以看到,下面这两个写法其实是等价的作用:
// 关键字在实例方法上,锁为当前实例
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
}
}
锁的四种状态
在 JDK 1.6 以前,所有的锁都是”重量级“锁,因为使用的是操作系统的互斥锁,当一个线程持有锁时,其他试图进入synchronized块的线程将被阻塞,直到锁被释放。涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低。
这也是为什么很多开发者会认为 synchronized 性能很差的原因。
那为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了“偏向锁”和“轻量级锁” 的概念,对 synchronized 做了一次重大的升级,升级后的 synchronized 性能可以说上了一个新台阶。
在 JDK 1.6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它,很好理解。
锁 | 优点 | 缺点 | 应用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗 CPU。 | 追求响应时间。同步块执行速度非常快。 |
重量级锁 | 线程竞争不使用自旋,不会消耗 CPU。 | 线程阻塞,响应时间缓慢。 | 同步块执行时间较长。 |
小结
- Java 中的每一个对象都可以作为一个锁,Java 中的锁都是基于对象的。
- synchronized 关键字可以用来修饰方法和代码块,它可以保证在同一时刻最多只有一个线程执行该段代码。
- synchronized 关键字在修饰方法时,锁为当前实例对象;在修饰静态方法时,锁为当前 Class 对象;在修饰代码块时,锁为括号里面的对象。
- Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在 Java 6 以前,所有的锁都是”重量级“锁。所以在 Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
- 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连 CAS 操作都不做了,提高了程序的运行性能。
- 轻量级锁是通过 CAS 操作和自旋来实现的,如果自旋失败,则会升级为重量级锁。
- 重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。