lesson3:java的锁机制原理和分析
jdk1.5之前,我们对代码加锁(实际是对象加锁),都是采用Synchronized关键字来处理,jdk1.5及以后的版本中,并发编程大师Doug Lea在concurrrent包中提供了Lock机制。两种机制在性能上目前的jdk版本都差不多,Synchronized作为jvm的关键字,是在jvm层面实现的锁机制,而Lock机制是在java语言这个级别实现的锁机制,其实锁的核心原理都是为某个对象加锁。本文中将以Lock机制的源代码来分析锁机制的原理和实现,后面的demo代码也将按照Lock锁来展开。
demo源码:https://github.com/mantuliu/javaAdvance 中的类 Lesson3CoarseGrainedLock和Lesson3FinedGrainedLock
下面的斜体段落是java api中对Lock的描述:
Lock
实现提供了比使用 synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition
对象。
锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock
的读取锁。
synchronized
方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
虽然 synchronized
方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock
接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized
方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。
Lock
实现提供了使用 synchronized
方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 (tryLock()
)、一个获取可中断锁的尝试 (lockInterruptibly()
) 和一个获取超时失效锁的尝试 (tryLock(long, TimeUnit)
)。
Lock
类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。
注意,Lock
实例只是普通的对象,其本身可以在 synchronized
语句中作为目标使用。获取 Lock
实例的监视器锁与调用该实例的任何 lock()
方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock
实例。
除非另有说明,否则为任何参数传递 null
值都将导致抛出 NullPointerException
。
内存同步
所有 Lock
实现都必须 实施与内置监视器锁提供的相同内存同步语义,如 The Java Language Specification, Third Edition (17.4 Memory Model) 中所描述的:
- 成功的
lock
操作与成功的 Lock 操作具有同样的内存同步效应。 - 成功的
unlock
操作与成功的 Unlock 操作具有同样的内存同步效应。
不成功的锁定与取消锁定操作以及重入锁定/取消锁定操作都不需要任何内存同步效果。
实现注意事项
三种形式的锁获取(可中断、不可中断和定时)在其性能特征、排序保证或其他实现质量上可能会有所不同。而且,对于给定的 Lock
类,可能没有中断正在进行的 锁获取的能力。因此,并不要求实现为所有三种形式的锁获取定义相同的保证或语义,也不要求其支持中断正在进行的锁获取。实现必需清楚地对每个锁定方法所提供的语义和保证进行记录。还必须遵守此接口中定义的中断语义,以便为锁获取中断提供支持:完全支持中断,或仅在进入方法时支持中断。
由于中断通常意味着取消,而通常又很少进行中断检查,因此,相对于普通方法返回而言,实现可能更喜欢响应某个中断。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。
Lock接口的实现类有:ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock;本文将以ReentrantLock的源码来分析内部实现原理:
a.lock()方法
1 2 3 | public void lock() { sync.lock(); //调用sync对象的lock()方法 }<br>下面看看sync对象的创建过程及它的lock()方法<br> public ReentrantLock() {<br> sync = new NonfairSync(); //我们发现实现sync对象的类是NonfairSync<br> } |
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
/*调用unsafe的compareAndSwapInt方法(原子操作),如果Lock对象存储的对象值是0(与第一个参数值相同),则将此值设置为1(与第二个参数值相同),反之,则返回失败*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());//设置当前线程为锁的持有者
else
acquire(1);//当前线程等待其它线程释放锁
}
}
b.tryLock()方法:仅在调用时锁为空闲状态才获取该锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public boolean tryLock() { return sync.nonfairTryAcquire( 1 ); //尝试获取锁 } final boolean nonfairTryAcquire( int acquires) { final Thread current = Thread.currentThread(); //获取到当前线程 int c = getState(); //获取锁的状态 if (c == 0 ) { //0表示锁未被占用 if (compareAndSetState( 0 , acquires)) { //调用unsafe的compareAndSwapInt方法(原子操作),标识此锁为已经占有状态 setExclusiveOwnerThread(current); //设置当前线程占有锁 return true ; //标识获取锁成功 } } else if (current == getExclusiveOwnerThread()) { //如果当前线程在之前已经持有过此锁,并且没有释放 int nextc = c + acquires; if (nextc < 0 ) // overflow throw new Error( "Maximum lock count exceeded" ); setState(nextc); //标识同一个线程占用此锁几次 return true ; } return false ; } |
c.tryLock(long time, TimeUnit unit)方法,介于lock()和tryLock()方法之间,如果锁已经被其它线程占用,则会等待一段时间
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || //调用NonfairSync的tryAcquire(方法),与上面的tryLock()方法调用的nonfairTryAcquire()一致 doAcquireNanos(arg, nanosTimeout); }
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
long lastTime = System.nanoTime();//按纳秒返回当前时间
final Node node = addWaiter(Node.EXCLUSIVE);//创建一个节点并入队列来等待锁
boolean failed = true;
try {
for (;;) {//一直循环
final Node p = node.predecessor();//返回当前节点的前一个等待节点
if (p == head && tryAcquire(arg)) {//如果前一个节点已经是head节点,则试图去获取锁,如果获取到了,则执行下面的代码
setHead(node);//设置当前节点为队列中的头节点
p.next = null; // help GC
failed = false;
return true;
}
if (nanosTimeout <= 0)
return false;//超时返回失败
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())//线程被中断了
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
d.lockInterruptibly()方法,可以相应线程中断的去获取锁,如果线程被中断了,则抛出异常
1 2 3 4 5 6 7 8 9 10 11 | public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly( 1 ); //调用acquireInterruptibly } public final void acquireInterruptibly( int arg) throws InterruptedException { if (Thread.interrupted()) //如果线程被中断了,则抛出异常 throw new InterruptedException(); if (!tryAcquire(arg)) ////调用NonfairSync的tryAcquire(方法),与上面的tryLock()方法调用的nonfairTryAcquire()一致 doAcquireInterruptibly(arg); } private void doAcquireInterruptibly( int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true ; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null ; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) && //检测线程是否被中断 parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } |
e.unlock()方法,释放锁,通常只有持有锁的线程才能释放锁成功
public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
上面的代码分析了ReentrantLock的非公平锁的实现,在实现的使用过程中,锁和线程是息息相关的,如果我们把线程比作高速公路上的车道,那么行驶在高速公路上的车辆就是我们实际的代码(操作),因为路况或其它原因,各条车道需要并线,为防止意外发生,在一定时间内只能有一条车道或几条路上的车可以通过(代码上了锁),为保证通行时间,有两点需要注意:1是车辆通过的速度要快(线程占用锁的时间要少),2是在高速公路的车道数足够多的情况下,要尽可能的将车道分成多个小组,各个小组内部交替通行,小组与小组之间不影响。总结起来,在使用锁的情况下,要想对系统的性能影响足够小:1.锁的粒度要足够小,尽量减少同时使用同一把锁的线程数量;2.加锁的线程执行时间要足够快。下面的demo从粒度的角度展示了锁对于性能的影响:
先来看看粗粒度的锁,100个线程锁同一个资源,每个线程执行1秒钟的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package com.mantu.advance; import java.util.concurrent.locks.ReentrantLock; /** * blog http://www.cnblogs.com/mantu/ * github https://github.com/mantuliu/ * @author mantu * */ public class Lesson3CoarseGrainedLock implements Runnable{ public static ReentrantLock lock = new ReentrantLock(); public static void main(String args[]){ for ( int i= 0 ;i< 100 ;i++){ Thread thread = new Thread( new Lesson3CoarseGrainedLock()); thread.start(); } } @Override public void run() { try { lock.lock(); //100个线程使用同一个锁,每个时刻只能有一个线程执行 Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println( "线程" +Thread.currentThread().getId()+ "执行完毕" ); } } } |
再来看看细粒度锁的demo,每10个线程分为一个组,组内的线程才会存在资源竞争的情况,执行完代码后,会发现效率比上一个demo提升了非常多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package com.mantu.advance; import java.util.HashMap; import java.util.concurrent.locks.ReentrantLock; /** * blog http://www.cnblogs.com/mantu/ * github https://github.com/mantuliu/ * @author mantu * */ public class Lesson3FinedGrainedLock implements Runnable{ public static HashMap<Integer,ReentrantLock> lockMap = new HashMap<Integer,ReentrantLock>(); public static void main(String args[]){ init(); for ( int i= 0 ;i< 100 ;i++){ Thread thread = new Thread( new Lesson3FinedGrainedLock()); thread.start(); } } public static void init(){ for ( int i= 0 ;i< 10 ;i++){ lockMap.put( new Integer(i), new ReentrantLock()); } } @Override public void run() { ReentrantLock lock = Lesson3FinedGrainedLock.lockMap.get(( int )(Thread.currentThread().getId())% 10 ); try { lock.lock(); //细粒度的锁,效率提高非常多 Thread.sleep( 1000 ); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.out.println( "线程" +Thread.currentThread().getId()+ "执行完毕" ); } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?