第12章 Java内存模型与线程
衡量一个服务性能的高低好坏,每秒事务处理数(Transactions Per Second, TPS)是重要的指标之一,它代表着一秒内服务端平均能响应的请求总数。
12.2 硬件的效率与一致性
为了维持缓存一致性(Cache Coherence),系统必须确保当一个处理器的缓存内容改变时,这种变化对所有的处理器都是可见的。这需要一种机制来维持处理器间的主存(Main Memory)和缓存数据的一致性。在多路处理器系统中,每 个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory),这种系统称为共享内存多核系统(Shared Memory Multiprocessors System),如图12-1所示。
12.3 Java内存模型
Java内存模型(Java Memory Model,JMM)用来来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
12.3.1 主内存与工作内存
-
Java内存模型主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。
-
此处的变量(Variables)包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
-
Java内存模型要求所有变量存储在主内存(Main Memory)中,线程有自己的工作内存(Working Memory),包含变量的副本,操作如读写必须在工作内存进行,线程间的变量传递均需要通过主内存完成。
12.3.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,Java内存模型中定义了以下8种操作来完成。Java虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的
-
lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
-
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程锁定。
-
read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以 便随后的load动作使用。
-
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的 变量副本中。
-
use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚 拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
-
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量, 每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
-
store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随 后的write操作使用。
-
write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的 变量中。
后来Java内存模型的操作简化为read、write、lock和unlock四种,但这只是语言描述上的等价化简,Java内存模型的基础设计并未改变。
12.3.3 对于volatile型变量的特殊规则
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,
-
当变量定义为volatile,此变量对所有线程具有可见性,它确保所有线程立即知晓该变量的更新,不像普通变量必须经过主内存同步。例如,线程A更新普通变量后回写到主内存,线程B之后读取主内存才能看到新值。
-
使用volatile变量可以禁止指令重排序,保证赋值顺序与代码执行顺序一致,而普通变量不保证这种顺序,因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
双锁检测(Double Check Lock,DCL)单例代码
public class Singleton {
private volatile static Singleton instance; // 禁止指令重排序优化
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
-
有volatile修饰的变量,赋值后多执行了一个
lock addl$0x0,(%esp)
操作 -
这个操作的作用相当于一个内存屏障 (Memory Barrier,指重排序时不能把后面的指令重排序到内存屏障之前的位置)。
-
lock addl$0x0,(%esp)
指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。
volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
12.3.4 针对long和double型变量的特殊规则
对于64位的数据类型(long和double),允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write这四个操作的原子性,这就是所谓的“long和double的非原子性协定”(Non-Atomic Treatment of double and long Variables)。
12.3.5 原子性、可见性与有序性
1. 原子性(Atomicity)
-
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个
-
基本数据类型的访问、读写都是具备原子性的(例外就是long和double的非原子性协定)
-
在synchronized块之间的操作也具备原子性。
2. 可见性(Visibility)
-
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。
-
volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。
-
synchronized和 final 也能实现可见性
-
同步块的可见性是对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
-
final的可见性是:被final修饰的字段在构造器中一旦被初始化完 成,并且构造器没有把“this”的引用传递出去,那么在其他线程中就能看见final字段的值。
-
3.有序性(Ordering)
-
Java程序中天然的有序性:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的。
-
前半句是指“线程内似表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
12.3.6 先行发生原则
“先行发生”(Happens-Before)原则是判断数据是否存在竞争,线程是否安全的非常有用的手段。
-
先行发生原则确保在Java内存模型中,操作A的效果(如修改共享变量、发送消息、方法调用)在操作B执行前可被感知。
-
下面是Java内存模型下一些“天然的”先行发生关系:
-
程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
-
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这 里必须强调的是“同一个锁”,而“后面”是指时间上的先后。
-
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量 的读操作,这里的“后面”同样是指时间上的先后
-
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
-
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
-
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程 的代码检测到中断事件的发生,可以通过Thread::interrupted()方法检测到是否有中断发生。
-
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
-
-
示例:
private int value = 0;
pubilc void setValue(int value){
this.value = value;
}
public int getValue(){
return value;
}
由于两个方法分别由线程A和B调用,不在一个线程中,尽管线程A在操作时间上先于线程B,但 无法确定线程B中getValue()方法的返回结果,换句话说,这里面的操作不是线程安全的。
-
一个操作“时间上的先发生”不代表这个操作会是“先行发生”。
-
时间先后顺序与先行发生原则之间基本没有因果关系,一切必须以先行发生原则为准。
12.4 Java与线程
12.4.1 线程的实现
-
线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。
-
线程是Java里面进行处理器资源调度的最基本单位
-
实现线程主要有三种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现), 使用用户线程加轻量级进程混合实现(N:M实现)。
1.内核线程实现
-
内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
-
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程
-
缺陷:
-
系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。
-
每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈 空间),因此一个系统支持轻量级进程的数量是有限的。
-
2.用户线程实现
-
使用用户线程实现的方式被称为1:N实现。只要不是内核线程,都可以认为是用户线程的一种
-
用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都 需要由用户程序自己去处理。
3.混合实现
-
将内核线程与用户线程一 起使用的实现方式,被称为N:M实现。在这种混合实现下,既存在用户线程,也存在轻量级进程。
-
在这种混合模式中,用户线程与轻量级进程的数量比是不定的,是N:M的关系,
4.Java线程的实现
-
早期的Classic虚拟机上(JDK 1.2以前)基于一种被称为“绿色线程”(Green Threads)的用户线程实现的
-
从JDK 1.3起,“主流”商用Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用1:1的线程模型(内核线程实现)
12.4.2 Java线程调度
-
线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式 (Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度。
-
Java使用的线程调度方式就是抢占式,每个线程将由系统来分配执行时间
-
Thread::yield()方法可以主动让出执行时间,但无法主动获取执行时间
-
优先级越高的线程越容易被系统选择执行,但并不是稳定的调节手段
12.4.3 状态转换
Java语言定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换。这6种状态分别是:
-
新建(New):创建后尚未启动的线程处于这种状态。
-
运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可 能正在执行,也有可能正在等待着操作系统为它分配执行时间。
-
无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线 程显式唤醒。以下方法会让线程陷入无限期的等待状态:
-
没有设置Timeout参数的Object::wait()方法;
-
没有设置Timeout参数的Thread::join()方法;
-
LockSupport::park()方法。
-
-
限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状 态:
-
Thread::sleep()方法;
-
设置了Timeout参数的Object::wait()方法;
-
设置了Timeout参数的Thread::join()方法;
-
LockSupport::parkNanos()方法;
-
LockSupport::parkUntil()方法。
-
-
阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到 一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
-
结束(Terminated):已终止线程的线程状态,线程已经结束执行。
12.5 Java与协程
12.5.1 内核线程的局限
-
1:1的内核线程模型是如今Java虚拟 机线程实现的主流选择,但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高昂,系统能容纳的线程数量也很有限。
-
在每个请求本身的执行时间变得很短、数量变得很多的前提下, 用户线程切换的开销甚至可能会接近用于计算本身的开销,这就会造成严重的浪费。
12.5.2 协程的复苏
-
内核线程的调度成本主要来自于用户态与核心态之间的状态转换,而这两种状态转换的开销主要来自于响应中断、保护和恢复执行现场的成本。
-
协程和线程相似,但它更轻量,因为它不依赖于操作系统的线程管理,而是在应用程序的控制下执行。
-
协程也有它的局限,需要在应用层面实现的内容(调用栈、调度器这些)特别多
12.5.3 Java的解决方案
-
Java的Loom项目提出了纤程(Fiber),一种轻量级用户态线程,由Java虚拟机而非操作系统调度,目的是简化并发编程并提高系统性能。
-
纤程占用资源少,易于扩展到数百万级别的并发任务,而且与现有的Fork/Join模型等并发结构相结合,能够提升任务执行效率。
第13章 线程安全与锁优化
13.2 线程安全
“当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。”
13.2.1 Java语言中的线程安全
按照线程安全的“安全程度”由强至弱来排序,可以将Java语言中各种操作共享的数据分为以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
-
在Java中,不可变 (Immutable)的对象一定是线程安全的,不需要再进行任何线程安全保障措施。
-
例如java.lang.String类的对象实例,用户调用它的 substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。
-
常用的还有枚举类型及 java.lang.Number的部分子类,如Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。
2. 绝对线程安全
-
在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全。
-
尽管Vector的方法都是同步的,但在多线程中,如果不做同步措施,使用下面这段代码仍然是不安全的。
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < vector.size(); i++) {
System.out.println((vector.get(i)));
}
}
});
removeThread.start();
printThread.start();
//不要同时产生过多的线程,否则会导致操作系统假死
while (Thread.activeCount() > 20);
}
}
因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号 i 已经不再可用,再用 i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException异常。
3. 相对线程安全
-
相对线程安全就是我们通常意义上所讲的线程安全,它需要保证对这个对象单次的操作是线程安全的。
-
在Java语言中,大部分声称线程安全的类都属于这种类型,例如Vector、HashTable、Collections的 synchronizedCollection()方法包装的集合等。
4. 线程兼容
-
线程兼容是指对象本身并不是线程安全的,但是可以通过使用同步手段来保证对象在并发环境中可以安全地使用。我们平常说一个类不是线程安全的,通常就是指这种情况。
5. 线程对立
-
线程对立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码。
-
一个线程对立的例子是Thread类的suspend()和resume()方法。
-
无论调用时是否进行了同 步,目标线程都存在死锁风险——假如suspend()中断的线程就是即将要执行resume()的那个线程,一定产生死锁。因此,suspend()和resume()方法都已经被声明废弃了。
13.2.2 线程安全的实现方法
1.互斥同步
-
互斥是实现同步的一种手段,临界区(Critical Section)、互斥量 (Mutex)和信号量(Semaphore)都是常见的互斥实现方式。
-
最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成 monitorenter和monitorexit这两个字节码指令。
-
被synchronized修饰的同步块对同一条线程来说是可重入的,重入时计数加一,计数为零时锁就被释放了。
-
持有锁是一个重量级(Heavy-Weight)的操作,虚拟机会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,以避免频繁地切入核心态之中。
-
重入锁(ReentrantLock)是Lock接口最常见的一种实现:与synchronized相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公 平锁及锁可以绑定多个条件。Lock应该确保在finally块中释放锁。
-
等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待。
-
公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。ReentrantLock在默认情况下也是非公平的,不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降。
-
锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized 中,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做。
-
2. 非阻塞同步
-
互斥同步也被称为阻塞同步(Blocking Synchronization),属于一种悲观的并发策略。
-
基于冲突检测的乐观并发策略,先进行操作,如果没有其他线程争用共享数据,操作就直接成功;如果产生了竞争冲突,那再进行其他的补偿措施(不断地重试)。这种乐观并发策略的实现不再需要把线程阻塞挂起,因此这种同步操作被称为非阻塞同步(Non-Blocking Synchronization),也被称为无锁(Lock-Free) 编程。
-
CAS指令需要有三个操作数,变量的内存地址,用V 表示、旧的预期值(用A表示)和准备设置的新值(用B表示)。CAS指令执行时,当且仅当V符合 A时,处理器才会用B更新V的值,否则它就不执行更新。
-
在JDK 5之后,Java类库中才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的 compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。
-
JDK 9之前只有Java类库可以使用CAS,Unsafe::getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问。直到JDK 9之后,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操作。
-
为了解决CAS操作的“ABA问题”,J.U.C包提供了一个带有标记的原子引用类AtomicStampedReference
3. 无同步方案
有一些代码天生就是线程安全的,简单介绍其中的两类:
-
可重入代码(Reentrant Code):这种代码又称纯代码(Pure Code),是指可以在代码执行的任何时刻中断它,所有可重入的代码都是线程安全的,但并非所有的线程安全的代码都是可重入的。
-
线程本地存储(Thread Local Storage)是一种限制数据在单个线程中可见的技术。当你的数据需要在不同代码块间共享,但这些代码块总是在同一线程中运行时,可以使用线程本地存储。这样做可以避免线程间的数据竞争,而不需要额外的同步操作。
-
Java语言中,如果一个变量要被多线程访问,可以使用volatile关键字。
-
如果一个变量只要被某个线程独享,可以通过
java.lang.ThreadLocal
类来实现线程本地存储的功能。
-
13.3 锁优化
高效并发是从JDK 5升级到JDK 6后一项重要的改进项,实现了各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)等。
13.3.1 自旋锁与自适应自旋
-
如果一个线程需要等待另一个释放锁,它可以通过忙循环(自旋)来等待,而不是放弃处理器的执行时间。这种等待锁的方法称为自旋锁。自旋锁在JDK 6中就已经改为默认开启了。
-
自旋次数的默认值是十次。在JDK 6中对自旋锁进行了优化,引入了自适应的自旋。
13.3.2 锁消除
-
锁消除是指即时编译器在运行时,对检测到不可能存在共享数据竞争的锁进行消除。
-
有许多同步措施并不是程序员自己加入的,一段看起来没有同步的代码:
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String类对字符串的连接操作总是通过生成新的String对象来进行的,因此编译器会对String连接做自动优化,会转化为StringBuilder对象的连续append()操作。Javac转化后的字符串连接操作:
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象。虚拟机观察变量sb,经过逃逸分析后会发现它的动态作用域被限制在concatString()方法内部。也就是sb的所有引用都永远不会逃逸到concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉。
13.3.3 锁粗化
-
大多数情况,总是推荐将同步块的作用范围限制得尽量小,然而,如果多个连续操作反复对同一对象加锁和解锁,特别是在循环中,即使没有线程竞争,频繁的互斥同步也会导致性能损耗。
-
如果虚拟机发现连续的操作如多次
append()
方法都在对同一个对象加锁,它会扩大锁的范围,从第一个append()
操作之前到最后一个操作之后,使得整个操作序列只需要加锁一次,以减少锁的频繁申请和释放。这种优化称为锁粗化。
13.3.4 轻量级锁
-
轻量级锁是JDK 6时加入的新型锁机制,“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的
-
虚拟机的对象头(Object Header)分为两部分,第一 部分用于存储对象自身的运行时数据,称为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针。
-
在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
-
然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这 更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的 最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。
-
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志 的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
-
-
轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争 的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销。
13.3.5 偏向锁
-
偏向锁也是JDK 6中引入的一项锁优化措施,它会偏向于第一个获得它的线程,如果该锁未被其他线程获取,则无需再同步。
-
线程首次获取锁时,虚拟机会把对象头中标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS把线程的ID记录在对象的Mark Word中。如果成功,该线程访问此锁相关同步块将不需同步操作。
-
如果对象已计算哈希码或在偏向模式下需计算哈希码,偏向锁将被撤销,锁升级为重量级锁。
-
偏向锁能提升无竞争同步程序的性能,但如果大部分锁频繁被多个线程访问,偏向模式可能反而无益。