From 965953bb5c3c118fa2b4c316056b39518500c06e Mon Sep 17 00:00:00 2001 From: wr <191040964@qq.com> Date: Sun, 29 Dec 2019 22:11:39 +0800 Subject: [PATCH] 23 week03 --- week_03/23/AbstractQueuedSynchronize.md | 102 ++++++++++ week_03/23/JMM.md | 74 +++++++ week_03/23/ReentrantLock.md | 78 ++++++++ week_03/23/Semaphore.md | 8 + ...06\345\270\203\345\274\217\351\224\201.md" | 185 ++++++++++++++++++ week_03/23/synchronized.md | 31 +++ 6 files changed, 478 insertions(+) create mode 100644 week_03/23/AbstractQueuedSynchronize.md create mode 100644 week_03/23/JMM.md create mode 100644 week_03/23/ReentrantLock.md create mode 100644 week_03/23/Semaphore.md create mode 100644 "week_03/23/mysql,Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" create mode 100644 week_03/23/synchronized.md diff --git a/week_03/23/AbstractQueuedSynchronize.md b/week_03/23/AbstractQueuedSynchronize.md new file mode 100644 index 0000000..d5cf388 --- /dev/null +++ b/week_03/23/AbstractQueuedSynchronize.md @@ -0,0 +1,102 @@ +### 内部类Node + + + +```java + static final class Node { + // 共享模式的标记 + static final Node SHARED = new Node(); + // 独占模式的标记 + static final Node EXCLUSIVE = null; + + // waitStatus变量的值,标志着线程被取消 + static final int CANCELLED = 1; + // waitStatus变量的值,标志着后继线程(即队列中此节点之后的节点)需要被阻塞.(用于独占锁) + static final int SIGNAL = -1; + // waitStatus变量的值,标志着线程在Condition条件上等待阻塞.(用于Condition的await等待) + static final int CONDITION = -2; + // waitStatus变量的值,标志着下一个acquireShared方法线程应该被允许。(用于共享锁) + static final int PROPAGATE = -3; + + // 标记着当前节点的状态,默认状态是0, 小于0的状态都是有特殊作用,大于0的状态表示已取消 + volatile int waitStatus; + + // prev和next实现一个双向链表 + volatile Node prev; + volatile Node next; + + // 该节点拥有的线程 + volatile Thread thread; + + // 可能有两种作用:1. 表示下一个在Condition条件上等待的节点 + // 2. 表示是共享模式或者独占模式,注意第一种情况节点一定是共享模式 + Node nextWaiter; + + // 是不是共享模式 + final boolean isShared() { + return nextWaiter == SHARED; + } + + // 返回前一个节点prev,如果为null,则抛出NullPointerException异常 + final Node predecessor() throws NullPointerException { + Node p = prev; + if (p == null) + throw new NullPointerException(); + else + return p; + } + + // 用于创建链表头head,或者共享模式SHARED + Node() { + } + + // 使用在addWaiter方法中 + Node(Thread thread, Node mode) { + this.nextWaiter = mode; + this.thread = thread; + } + + // 使用在Condition条件中 + Node(Thread thread, int waitStatus) { + this.waitStatus = waitStatus; + this.thread = thread; + } + } +``` + + + + + +#### acquire方法 + +```java + /** + * 获取独占锁。如果没有获取到,线程就会阻塞等待,直到获取锁。不会响应中断异常 + * @param arg + */ + public final void acquire(int arg) { + // 1. 先调用tryAcquire方法,尝试获取独占锁,返回true,表示获取到锁,不需要执行acquireQueued方法。 + // 2. 调用acquireQueued方法,先调用addWaiter方法为当前线程创建一个节点node,并插入队列中, + // 然后调用acquireQueued方法去获取锁,如果不成功,就会让当前线程阻塞,当锁释放时才会被唤醒。 + // acquireQueued方法返回值表示在线程等待过程中,是否有另一个线程调用该线程的interrupt方法,发起中断。 + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); + } +``` + +Node如何入队 +Node节点的入队可以分解为以下几个步骤: + +(1)在 acquire(int arg) 中,如果没有通过 tryAcquire(arg) 获取到资源,则通过 addWaiter(Node.EXCLUSIVE) 把当前线程构建为一个Node节点准备入队。(这里的代码是“排它模式”的入队)。 + +(1)调用 acquireQueued 方法将Node节点入队。在 acquireQueued 中,AQS会让node本地自旋,不断轮训前继节点的状态。 + +Node如何出队 + +node节点的出队分为2步 + +(1)释放state:**tryRelease(int arg)** + +(2)唤醒后继节点:**unparkSuccessor(Node node)** \ No newline at end of file diff --git a/week_03/23/JMM.md b/week_03/23/JMM.md new file mode 100644 index 0000000..ea33565 --- /dev/null +++ b/week_03/23/JMM.md @@ -0,0 +1,74 @@ +## **Java内存模型(JMM)** + +Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。 + +**缓存一致性问题**:在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等: + +![](F:\git\personal\pic\缓存一致性协议.jpg) + +**指令重排序问题**:为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化 + +JMM定义了线程和主内存之间的抽象关系: + +- 线程之间的共享变量存储在主内存(Main Memory)中 +- 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。 +- 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。 +- Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。 + +![](F:\git\personal\pic\JMM.jpg) + +关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成: + +- **lock(锁定)**:作用于主内存的变量,把一个变量标识为一条线程独占状态。 +- **unlock(解锁)**:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 +- **read(读取)**:作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 +- **load(载入)**:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 +- **use(使用)**:作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 +- **assign(赋值)**:作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 +- **store(存储)**:作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。 +- **write(写入)**:作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 + + + +Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则: + +- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。 +- 不允许read和load、store和write操作之一单独出现 +- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。 +- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。 +- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。 +- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现 +- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值 +- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。 +- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。 + +**as-if-serial语义:** + +不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义) + +比如 A —> C , B ----> C + + A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。 + +A —> B ----> C 或者 B ----> A ----> C + + as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器 +共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。asif-serial语义使 单线程 (因为在多线程环境下 重排序将会对程序造成影响,是结果不会是预期的)程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。 + + + + + +**happens before:** + +从JDK 5开始,Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来阐述操作之间的内存可见性:在JMM中,如果一个操作执行的结果需要对另一个操作可见(两个操作既可以是在一个线程之内,也可以是在不同线程之间),那么这两个操作之间必须要存在happens-before关系: + +- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。 +- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 +- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 +- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 + +一个happens-before规则对应于一个或多个编译器和处理器重排序规则 + + + diff --git a/week_03/23/ReentrantLock.md b/week_03/23/ReentrantLock.md new file mode 100644 index 0000000..6911d9b --- /dev/null +++ b/week_03/23/ReentrantLock.md @@ -0,0 +1,78 @@ +## 非公平锁的实现 + +非公平锁的实现很简单,通过CAS操作判断state这个变量是不是为0,如果是0,则将state设置为1,并且将持有锁的线程改为当前拿到锁的线程。如果是不是0,则调用aquire()方法去抢占锁。代码如下: + + + + + +```java + + 1 /** + 2 * Performs lock. Try immediate barge, backing up to normal + 3 * acquire on failure. + 4 */ + 5 final void lock() { + 6 if (compareAndSetState(0, 1)) + 7 setExclusiveOwnerThread(Thread.currentThread()); + 8 else + 9 acquire(1); +10 } + +``` + +因为acquire()方法需要调用tryAcquire()方法,而AQS定义了一个模板给子类去实现,所以Syn类去实现了这个方法。而Syn的非公平锁的tryAcquire()实际上调用的是Syn的nonfairTryAcquire()方法,代码如下: + + + +## 公平锁 + +公平锁的实现相比非公平锁的实现,相对来说复杂了一点点,公平锁的加锁方式是直接acquire()方法,因为公平锁不能去抢占。 + + + +```java +static final class FairSync extends Sync { + private static final long serialVersionUID = -3000897897090466540L; + //加锁的方法,直接调用acquire()方法继而调用自己实现的tryAcquire()方法 + final void lock() { + acquire(1); + } + + /** + * Fair version of tryAcquire. Don't grant access unless + * recursive call or no waiters or is first. + */ + protected final boolean tryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { + //这里需要判断当前线程是不是在同步队列的队首 + if (!hasQueuedPredecessors() && + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + //这里的逻辑是和公平锁一样的,就是判断是不是当前线程是不是已经是获取到锁的线程,是的话将state变量值递增 + else if (current == getExclusiveOwnerThread()) { + int nextc = c + acquires; + if (nextc < 0) + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; + } + } +``` + +### ReentrantLock相对于synchronized的区别: + +1) ReentrantLock可以实现公平锁和非公平锁。 + +2) ReentrantLock可中断 + +3) ReentrantLock需要手动加锁,释放锁 + +4) ReentrantLock是JDK API层面的,而synchronized是JVM层面的 \ No newline at end of file diff --git a/week_03/23/Semaphore.md b/week_03/23/Semaphore.md new file mode 100644 index 0000000..6d8aa03 --- /dev/null +++ b/week_03/23/Semaphore.md @@ -0,0 +1,8 @@ +init():设置计数器的初始值(信号量是支持多个线程访问一个临界区的,所以可以通过设置计数器的初始值来控制线程访问临界资源的数量) +down():计数器减一操作;如果此时计数器的值小于0,则当前线程被阻塞,否则当前线程可以继续执行。 +up():计数器加一操作;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。 + +Semaphore有一个独特的功能,就是可以允许多个线程访问一个临界区。这里就通过“快速实现一个限流器”来说明; + +对象池:指的是一次性创建出 N 个对象,之后所有的线程重复利用这 N 个对象,当然对象在被释放前,也是不允许其他线程使用的。 +这里的限流就是指不允许多余N个线程同时进入临界区 diff --git "a/week_03/23/mysql,Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" "b/week_03/23/mysql,Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" new file mode 100644 index 0000000..c62bfd8 --- /dev/null +++ "b/week_03/23/mysql,Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" @@ -0,0 +1,185 @@ +JUC 包的锁在同一个 JVM 中是全局的,所有线程都可以使用方法获取到,然后该锁就可以保证同一个 JVM 进程内多个线程竞争同一个共享资源的安全性。那么对应不同主机的 JVM 进程中的线程,同样可以搞一个对各个主机来说是全局的锁,这个全局的锁就是分布式锁,所谓分布式,说白了,是说存在多个 JVM 进程,而分布式锁,就是能保证多个 JVM 进程中的线程共同访问共享资源时候,资源安全性的锁。 + +## mysql实现分布式锁 + +`select * from 表 where id = #id for update`,当多个线程(无论是同一个 JVM 中的线程还是不同 JVM 中的多个线程)开启事务传递相同的 id 执行该语句时,只有一个线程会获取到该 id 对应的行记录的锁然后返回,其它线程则会阻塞到该语句的执行上,等获取行锁的线程提交事务后就释放了行锁,阻塞的多个线程就会通过竞争使一个线程获取到行锁,其它线程继续阻塞。由于不同 JVM 中线程共同去竞争的同一个行记录,所以这就实现了一个分布式锁 + +```java +public void lock(CallBack callBack) { + Connection conn = null; + PreparedStatement stmt = null; + ResultSet rs = null; + + try { + //3.1try get lock + System.out.println(Thread.currentThread().getName() + " begin try lock"); + conn = dataSource.getConnection(); + conn.setAutoCommit(false); + stmt = conn.prepareStatement(cmd); + rs = stmt.executeQuery(); + + //3.2do business thing + callBack.doAction(); + + //3.3release lock + conn.commit(); + System.out.println(Thread.currentThread().getName() + " release lock"); + + } catch (SQLException e) { + e.printStackTrace(); + + } finally { + //3.4 + if (null != conn) { + try { + conn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + } + } +``` + +其中 lock 方法就是加锁用的方法,其内部代码 3.1 首先从数据源获取一个数据库连接,然后设置事务自动提交为 false(也就是设置为手动提交事务),然后具体执行 CMD 对应的 SQL(也就是使用 for update 锁住记录),多个线程执行,只有一个线程能获取到行锁,其他线程阻塞到 `stmt.executeQuery()` 处。 + +当线程执行 3.1 获取到记录的行锁后会执行代码 3.2,3.2 执行传递的 callback 的业务逻辑(也就是需要在锁内执行的代码),业务执行完毕后 执行 3.3、commit 提交事务,这意味着当前线程释放了获取的锁,这时候被阻塞的线程会竞争获取该锁。 + +```java +rs = stmt.executeQuery(); + +//3.2do business thing +callBack.doAction(); + +//3.3release lock +conn.commit(); +``` + +如果不设置为手动提交,则多个线程执行完 `stmt.executeQuery()` 后就释放了行锁,那么多个线程就可以同时执行代码3.2,这显然是错误的。 + + + +### 使用 Redis 实现分布式锁 + +String set(final String key, final String value, final String nxxx, final String expx,final int time) + +- 在 Redis 中支持 kv 存储,这里 key 就是 kv 中的 key,value 就是 kv 中的 value。 +- 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改;其中 nxxx 的枚举值为 NX 和 XX,模式 NX 意思是说如果 key 不存在则插入该 key 对应的 value 并返回 OK,否者什么都不做返回 null;XX 意思是只在 key 已经存在时,才对 key 进行设置操作, 否者 null, 如果已经存在 key 并且进行了多次设置,则最终 key 对应的值为最后一次设置的值。 +- 其中 expx 的枚举值为 EX 和 PX,当为 EX 时候标示设置超时时间为 time 秒,当为 PX 时候标示设置超时时间为 time 毫秒 + + + + + +String set(final String key, final String value, final String nxxx, final String expx,final int time) + +- 在 Redis 中支持 kv 存储,这里 key 就是 kv 中的 key,value 就是 kv 中的 value。 + +- 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改;其中 nxxx 的枚举值为 NX 和 XX,模式 NX 意思是说如果 key 不存在则插入该 key 对应的 value 并返回 OK,否者什么都不做返回 null;XX 意思是只在 key 已经存在时,才对 key 进行设置操作, 否者 null, 如果已经存在 key 并且进行了多次设置,则最终 key 对应的值为最后一次设置的值。 + +- 其中 expx 的枚举值为 EX 和 PX,当为 EX 时候标示设置超时时间为 time 秒,当为 PX 时候标示设置超时时间为 time 毫秒 + +- ```java + if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end + ``` + + Redis 有一个叫做 eval 的函数,支持 Lua 脚本执行,并且能够保证脚本执行的原子性,也就是在执行脚本期间,其它执行 redis 命令的线程都会被阻塞。 + + + +```java +public class DistributedLock { + + private static final String LOCK_SUCCESS = "OK"; + private static final String SET_IF_NOT_EXIST = "NX"; + private static final String SET_WITH_EXPIRE_TIME = "PX"; + private static final Long RELEASE_SUCCESS = 1L; + + private static void validParam(JedisPool jedisPool, String lockKey, String requestId, int expireTime) { + if (null == jedisPool) { + throw new IllegalArgumentException("jedisPool obj is null"); + } + + if (null == lockKey || "".equals(lockKey)) { + throw new IllegalArgumentException("lock key is blank"); + } + + if (null == requestId || "".equals(requestId)) { + throw new IllegalArgumentException("requestId is blank"); + } + + if (expireTime < 0) { + throw new IllegalArgumentException("expireTime is not allowed less zero"); + } + } + + + public static boolean tryLock(JedisPool jedisPool, String lockKey, String requestId, int expireTime) { + + validParam(jedisPool, lockKey, requestId, expireTime); + + Jedis jedis = null; + try { + + jedis = jedisPool.getResource(); + String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); + + if (LOCK_SUCCESS.equals(result)) { + return true; + } + } catch (Exception e) { + throw e; + } finally { + if (null != jedis) { + jedis.close(); + } + } + + return false; + } + + +public static void lock(JedisPool jedisPool, String lockKey, String requestId, int expireTime) { + + validParam(jedisPool, lockKey, requestId, expireTime); + + while (true) { + if (tryLock(jedisPool, lockKey, requestId, expireTime)) { + return; + } + } + } + + public static void unLock(JedisPool jedisPool, String lockKey, String requestId) { + + validParam(jedisPool, lockKey, requestId, 1); + + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + + Jedis jedis = null; + try { + + jedis = jedisPool.getResource(); + Object result = jedis.eval(script, Collections.singletonList(lockKey), + Collections.singletonList(requestId)); + + if (RELEASE_SUCCESS.equals(result)) { + System.out.println("relese lock ok "); + } + + } catch (Exception e) { + throw e; + } finally { + if (null != jedis) { + jedis.close(); + } + } + + } + +``` + +- 通过 tryLock 方法尝试获取锁,内部是具体调用 Redis 的 set 方法,多个线程同时调用 tryLock 时候, 会同时调用 set 方法,但是 set 方法本身是保证原子性的,对应同一个 key 来说,多个线程调用 set 方法时候只有一个线程返回 OK,其它线程因为 key 已经存在会返回 null,返回 OK 的线程就相当与获取到了锁,其它返回 null 的线程则相当于获取锁失败。 +- 通过 lock 方法让使用 tryLock 获取锁失败的线程本地自旋转重试获取锁,这类似 JUC 里面的 CAS。 +- 通过 unLock 方法使用 redis 的 eval 函数传递 lua 脚本来保证操作的原子性。 \ No newline at end of file diff --git a/week_03/23/synchronized.md b/week_03/23/synchronized.md new file mode 100644 index 0000000..2daa781 --- /dev/null +++ b/week_03/23/synchronized.md @@ -0,0 +1,31 @@ +**锁的内存语义:** + +- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中 + +- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量 + +- ## **synchronized锁** + + synchronized用的锁是存在Java对象头里的。 + + JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。 + + 根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。 + + 注意两点: + + 1、synchronized同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题; + + 2、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。 + +### **Java对象头** + +![](F:\git\personal\pic\java对象头.png) + +在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化,以32位的JDK为例: + +![](F:\git\personal\pic\锁.png) + +- 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较ThreadID。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存货,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁 +- 轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁 +- 重量级锁:指向互斥量(mutex),底层通过操作系统的mutex lock实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。 \ No newline at end of file -- Gitee