让多线程下的类安全起来:无状态、加锁、让类不可变、栈封闭(方法封装) 、安全的发布对象(不暴露成员)
死锁
一定发生在多个线程争夺多个资源里的情况下,发生的原因是每个线程拿到了某个(某些)资源不释放,同时等待着其他线程所持有的资源。解决死锁的原则就是确保正确的获取资源的顺序,或者获取资源时使用定时尝试机制。
常见的死锁:简单顺序死锁、动态顺序死锁
/**
*简单顺序死锁
*/
public class SimpleDeadLock {
private static Object left = new Object();
private static Object right = new Object();
//先锁左 再右
private static void leftToRight() throws InterruptedException {
synchronized (left){
System.out.println(Thread.currentThread().getName()+" get left");
Thread.sleep(100);
synchronized (right){
System.out.println(Thread.currentThread().getName()+" get right");
}
}
}
//先锁左 再右(如果相反就会出现顺序死锁)
private static void rightToLeft() throws InterruptedException {
synchronized (left){
System.out.println(Thread.currentThread().getName()+" get right-left");
Thread.sleep(100);
synchronized (right){
System.out.println(Thread.currentThread().getName()+" get left-right");
}
}
}
private static class TestThread extends Thread{
private String name;
public TestThread(String name) {
this.name = name;
}
@Override
public void run(){
try {
rightToLeft();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("Main");
TestThread testThread = new TestThread("testThread");
testThread.start();
try {
leftToRight();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
===========
运行结果
死锁时=============
Main get left
Thread-0 get right-left
......
正常时=====================
Main get left
Main get right
Thread-0 get right-left
Thread-0 get left-right
动态顺序死锁示例
/**
* 帐号信息类
*/
public class Account {
private long number;
private final String name;
private int money;
private final Lock lock = new ReentrantLock();
public Lock getLock() {
return lock;
}
public Account(String name, int amount) {
this.name = name;
this.money = amount;
}
//加钱
public void addMoney(int amount){
money = money + amount;
}
//减钱
public void flyMoney(int amount){
money = money - amount;
}
public String getName() {
return name;
}
public int getAmount() {
return money;
}
@Override
public String toString() {
return "Account{" +
"name='" + name + '\'' +
", money=" + money +
'}';
}
}
/**
* 接口
*/
public interface ITransfer {
void transfer(Account from, Account to, int amount) throws InterruptedException;
}
public class NormalTransfer implements ITransfer{
@Override
public void transfer(Account from, Account to, int amount)
throws InterruptedException {
synchronized (from){
System.out.println(Thread.currentThread().getName()+" get "+from.getName());
Thread.sleep(100);
synchronized (to){
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
/**
* 银行业务类
*/
public class Bank {
private static class TransferThread extends Thread{
private String name;
private Account from;
private Account to;
private int amount;
private ITransfer transfer;
public TransferThread(String name, Account from, Account to,
int amout,ITransfer transfer) {
this.name = name;
this.from = from;
this.to = to;
this.amount = amout;
this.transfer = transfer;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
transfer.transfer(from,to,amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Bank bank = new Bank();
Account zhangsan = new Account("zhangsan",20000);
Account Lisi = new Account("lisi",20000);
ITransfer transfer = new NormalTransfer();//运行后出现动态顺序死锁
TransferThread zsToLisi = new TransferThread("zsToLisi",zhangsan,Lisi,
2000,transfer);
TransferThread lisiTozs = new TransferThread("lisiTozs",Lisi,zhangsan,
4000,transfer);
zsToLisi.start();
lisiTozs.start();
}
解决动态顺序死锁
/**
* 动态顺序锁 比较hash大小
* identityHashCode 返回hashcode 不管给定的对象曾经是否复写过hashcode
*/
public class SafeTransfer implements ITransfer {
private static Object tieLock = new Object();
@Override
public void transfer(Account from, Account to, int amount)
throws InterruptedException {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if(fromHash<toHash){
synchronized (from){
System.out.println(Thread.currentThread().getName()+" get "+from.getName());
Thread.sleep(100);
synchronized (to){
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(from);
System.out.println(to);
}
}
}else if(toHash<fromHash){
synchronized (to){
System.out.println(Thread.currentThread().getName()+" get "+to.getName());
Thread.sleep(100);
synchronized (from){
System.out.println(Thread.currentThread().getName()
+" get "+from.getName());
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(from);
System.out.println(to);
}
}
}else{
synchronized (tieLock){
synchronized (to){
System.out.println(Thread.currentThread().getName()+" get "+from.getName());
Thread.sleep(100);
synchronized (from){
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
/**
* 定时轮询获取锁
*/
public class TryLockTransfer implements ITransfer {
@Override
public void transfer(Account from, Account to, int amount)
throws InterruptedException {
Random r = new Random();
while(true){
if(from.getLock().tryLock()){
try{
System.out.println(Thread.currentThread().getName()
+" get from "+from.getName());
if(to.getLock().tryLock()){
try{
System.out.println(Thread.currentThread().getName()
+" get to "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(from);
System.out.println(to);
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
Thread.sleep(r.nextInt(5));//防止产生活锁
}
}
}
干货:查看线程中栈情况
jdkbin下的jstack.exe
进入bin目录
执行 jps -m 找到当前程序id号
再执行 jstack id号 查看
解决死锁:
保证正确的加锁、使用定时锁
其他活跃性危险:其他类型的死锁(资源争夺)、饥饿(长久拿不到锁无法工作)、糟糕的响应性能(多个应用不同业务,其中一个业务占有运行资源)、活锁(尝试获取时发生碰撞,又释放又碰撞,可以通过sleep来减少次数)
对性能的思考
- 程序的安全性优于性能的提升
- 使用多线程会带来额外的性能开销,滥用线程,有可能导致得不偿失。
- 所谓性能,包含多个指标。例如“多快”:服务时间、等待时间、延迟时间;例如“多少”:吞吐量,例如可伸缩性等等。
- 性能的各个指标方面,是完全独立的,有时候甚至是相互矛盾。
- 所以性能的提升是个包括成本在内多方面权衡和妥协的结果。
性能优化的黄金原则:
首先保证程序正确,然后再提高运行速度(如果有确切的证据表明程序确实慢)。
Amdahl定律
F :程序中的串行部分,是个百分比(100%-1%),
N:cpu的个数
Speedup:指在增加cpu的情况下,程序的加速比
线程引入的开销
上下文的切换
内存同步
阻塞
减少锁的竞争
快进快出,缩小锁的范围,将与锁无关的,有大量计算或者阻塞操作的代码移出同步范围。
减小锁的粒度,多个相互独立的状态变量可以使用多个锁来保护,每个锁只保护一个变量。
锁的分段,例如ConcurrentHashMap中的实现。
减少独占锁的使用,例如读多写少的情况下,用读写锁替换排他锁。
安全的单例模式
/**
* 饿汉式单例
*/
public class SingleEHan {
public static SingleEHan singleEHan = new SingleEHan();
private SingleEHan(){};
}
/**
* 懒汉式单例-双重检查(不推荐使用了)
*/
public class SingleDcl {
private volatile static SingleDcl single;
private SingleDcl(){}
public static SingleDcl getInstance(){
if(null==single){
synchronized (SingleDcl.class){
if(single==null){
single = new SingleDcl();
}
}
}
return single;
}
}
/**
* 延迟类占位符
*/
public class SingleClassInit {
private String name;
private SingleClassInit(){}
private static class InstanceHolder{
public static SingleClassInit instance = new SingleClassInit();
}
public static SingleClassInit getInstance(){
return InstanceHolder.instance;
}
}
内存模型的抽象结构
物理计算机中的并发问题,物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
“让计算机并发执行若干个运算任务”与“更充分地利用计算机处理器的效能”之间的因果关系,看起来顺理成章,实际上它们之间的关系并没有想象中的那么简单,其中一个重要的复杂性来源是绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是很难消除的(无法仅靠寄存器来完成所有运算任务)。由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。所谓的“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
在JMM中如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
volatile的实现原理
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile是如何来保证可见性的呢?是因为在编译的时候使用了一个Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事情,
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
synchronized的实现原理
synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。
Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态
轻量级锁
(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
重量级锁
重量锁在JVM中又叫对象监视器(Monitor),除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
原子操作的实现原理
使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。但是复杂的内存操作处理器是不能自动保证其原子性的。处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
(1)使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
(2)使用缓存锁保证原子性
在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。缓存锁定就是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取。
处理器提供了很多Lock前缀的指令来实现。例如,位测试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG。JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。在Java中可以通过锁和循环CAS的方式来实现原子操作。
重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
不管怎么重排序(编译器和处理器为了提高并行度),程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
例如:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
happens-before
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,为此jvm中提出了happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happensbefore关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
happens-before关系的定义如下。
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见。
上面计算圆的面积的示例代码存在3个happens-before关系,如下。
·A happens-before B。
·B happens-before C。
·A happens-before C。
在3个happens-before关系中,2和3是必需的,但1是不必要的。
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()方法的开始。
传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
例如代码:
显示的是一组再普通不过的getter/setter方法,假设存在线程A和B,线程A先(时间上的先后)调用了“setValue(1)”,然后线程B调用了同一个对象的“getValue()”,那么线程B收到的返回值是什么?
我们依次分析一下先行发生原则中的各项规则,由于两个方法分别由线程A和线程B各自run方法中调用,不在一个线程中,所以程序次序规则在这里不适用;由于没有同步块,自然就不会发生lock和unlock操作,所以管程锁定规则不适用;由于value变量没有被volatile关键字修饰,所以volatile变量规则不适用;后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系。因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起,因此我们可以判定尽管线程A在操作时间上先于线程B,但是无法确定线程B中“getValue()”方法的返回结果,换句话说,这里面的操作不是线程安全的。
那怎么修复这个问题呢?我们至少有两种比较简单的方案可以选择:要么把getter/setter方法都定义为synchronized方法,这样就可以套用管程锁定规则;要么把value定义为volatile变量,这样就可以套用volatile变量规则来实现先行发生关系。
volatile的内存语义
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
volatile写的内存语义如下。
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义如下。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
锁的内存语义
锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。