简述 Synchronized,Volatile,可重入锁的不同使用场景及优缺点
在多线程编程中 Synchronized
,Volatile
都扮演着重要角色, 都已用来实现原子操作。 Volatile
是轻量级的 Synchronized
,保证了共享变量的可见性。
可见性的意思是:当线程A修改共享变量的值后,线程B能立刻读到这个修改后的值。
Volatile
不会引起线程上下文的切换和调度,如果使用的恰当,会比 Synchronized
执行成本更低。
# Java中的内存可见性
可见性:一个线程对共享变量值的修改,能够及时被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那这个变量就是这几个线程的共享变量。
Java内存的规定:
-线程对共享变量的所有操作都必须在自己的工作内存中进行,不可直接从主内存中读写; -不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存。
# Volatile
的实现原理
如果对用 Volatile
修饰的变量写操作,JVM
会向处理器发出一条 Lock
前缀的指令,Lock
前缀的指令在多核处理器下会引发两件事情:
将当前处理器缓存行的数据写会到系统内存
这个写会内存的操作会使其他缓存中的该内存地址的数据无效
# Synchronized
的用法
- 普通同步方法:锁当前实例对象
public synchronized void method() {
}
2
3
4
5
- 静态同步方法:锁当前类的Class对象
public synchronized static void method() {
}
2
3
4
5
- 同步代码块:锁括号里对象
public void method()
{
synchronized(this) {
}
}
2
3
4
5
6
7
8
9
10
11
# Synchronized
的锁存储在哪里?
Synchronized
的锁存储在Java的对象头里。
Java 对象头里的 Mark Word 用于存储对象的 HashCode 、分代年龄和锁标记位。
32位虚拟机中, Mark Word 的存储结构如下:
在运行期, Mark Word 有四种状态:轻量级锁、重量级锁、GC 标记、偏向锁,各状态下的存储结构如下图:
在64位虚拟机下,Mark Word 的存储结构如下图:
# 为什么Java中每个对象都可以作为锁?
任何对象都有一个 monitor
与之关联,当一个 monitor
被持有后,他将处于锁定状态。monitor
是用 C++ 实现的。
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
monitorenter
指令是在编译后插入到同步代码块的开始位置,而 monitorexit
是插入到同步块的结束处和异常处,JVM 要保证每个 monitorenter
都有 monitorexit
与之配对。
# Synchronized
锁升级
Synchronized
一直被称为重量级锁。但是在JDK 1.6之后它已经变得不那么重了。JDK 1.6 对Synchronized
的优化点在于:
引入了偏向锁
引入了轻量级锁
在JDK 1.6 中,Synchronized
锁有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。
这几个状态会随着锁竞争升级,但是不可以降级。
# 偏向锁
为什么引入偏向锁?
不存锁竞争,或者总是由同一线程多次获得锁的场景,偏向锁的代价更低。
当一个线程访问同步块并获取到锁时,在锁对象头记录该线程的id,以后该线程进入和退出该同步块时不需要CAS来加锁和解锁。
偏向锁何时释放?
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
偏向锁一定起到正面作用吗?
不是的。
偏向锁的适用场景是:不存锁竞争,或者总是由同一线程多次获得锁的场景。
如果你确定你的程序中 锁通常处于竞争状态,可以通过JVM参数关闭偏向锁。关闭后,程序回魔人进入轻量级锁状态。
-XX:UseBiasedLocking=false
2
3
# 轻量级锁
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。
轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
轻量级锁何时升级为重量级锁?
若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。
另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。
# 重量级锁
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁的获取是释放一般会有线程上下文切换,代价是比较大的,所以说是重量级锁。
# 锁升级
# 锁的优缺点对比
# Java如何实现原子操作
原子操作:不可被中断的一个或一系列操作。
Java 有两种实现原子操作的方式:CAS(compare and swap)
、锁。
# CAS实现原子操作
CAS理论是 juc 包实现的基石,在intel的CPU中,CAS 通过调用本地方法(JNI)使用cmpxchg指令来实现的非阻塞算法。对比于synchronized阻塞算法,基于 CAS 实现的 juc 在性能上有了很大的提升。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
# CAS 存在的三个问题
- ABA问题
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
只能保证一个共享变量的原子操作
从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
# 锁机制实现原子操作
锁机制保证了只有获得锁的线程才能操作指定的内存区域。除了偏向锁,JVM实现锁的方式都使用了循环CAS。