一、公平锁&非公平锁
公平锁:先到先得。线程A持有锁,此时线程B过来尝试获取锁,通过cas判断没有成功,B进入队列中(队列先入先出特点),C过来尝试获取锁仍然未成功也进入到队列中;一直到E过来尝试获取锁,此时A释放锁,E仍然进入队列中,由于B在队列最前边,此时B获得锁
非公平锁:线程A持有锁,此时线程B过来尝试获取锁没有成功,B进入队列中(队列先入先出特点),C过来尝试获取锁仍然未成功也进入到队列中;一直到E过来尝试获取锁,此时A释放锁,E不用进入队列中,直接获得锁(B由于线程上下文切换是需要不少开销的,获取锁的效率低于E)
公平锁&非公平锁优缺点及使用范围:
非公平锁由于不存在上线文切换,执行效率更高,cpu利用率高。公平锁适用于线程执行时间大于等待线程等待时间的场景(这样会有很多线程积累),如果线程执行时间<=等待时间则不会有线程积累,采用非公平锁效率更高
公平锁&非公平锁代码区别:
//非公平锁 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //state是volitle类型,其他线程修改会对其他线程可见 if (c == 0) { //c==0表示当前没有线程持有锁,当前线程可以尝试获取锁 //区别重点看这里 if (compareAndSetState(0, acquires)) { //使用CAS将当前内存位置值和期望值0比较,如果true则将当前内存位置值更新为acquires;这里采用CAS判断的原因是防止其他线程获取到锁 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //这是代表当前线程本就持有这个锁,再来尝试获取锁的结果就是获取锁的次数+1,这表明这个锁是可重入锁 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } //公平锁 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //hasQueuedPredecessors这个方法就是最大区别所在 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } 公平锁的实现只多了一个hasQueuePredecessors()方法的判断,这个方法就是判断当前线程的前一个线程是否也有资格去获取锁,只有没有资格获取锁时,当前的线程才可以返回true.否则就不能获取锁,
ReentrantLock(true):公平锁,排它锁,同一时刻,只允许一个线程访问
ReenTrantLock(false):非公平锁,排它锁,同一时刻,只允许一个线程访问
sychronized:非公平锁,可重入锁,排它锁,同一时刻,只允许一个线程访问
ReadWriteTrantLock:共享锁,允许多线程读,但不允许多线程读、写;不允许多线程写、写操作;适用于多读的场景
超级参考:https://www.jianshu.com/p/f584799f1c77
二、ReenTrantLock
ReenTrantLock是基于aqs实现的
2.1 ReentrantLock底层(CAS+queue)
2.1.1 AQS
AQS是abstractQueuedSynchronizer简称。AQS提供一种实现阻塞锁和FIFO队列的同步器的框架.AQS内部保存一个原子性的状态变量state,aqs中对state操作是原子的,通过cas修改该变量值,修改成功的线程表示获取到该锁,没有修改成功或者发现state已经是加锁状态则通过一个Waiter对象封装线程添加到等待队列中,并挂起等待被唤醒,
AQS同时提供排他模式(exclusive)和共享模式(shared)
2.1.2 State
state状态值定义为线程获取该锁的重入次数,state为0表示当前没有被任何线程持有,state为1表示被其他线程持有,因为支持可重入,如果是持有锁的线程再次获取同一把锁,直接成功并且state状态值+1,线程释放锁state状态值-1,同理冲入多次锁的线程需要释放相应的次数
AQS中state类型:private volatile int state,表示当前同步状态,state访问方式有三种:getstate()、 setstate() 、compareAndSetState(),其中compareAndSetState()是依赖unsafe类的compareAndSwapInt()方法,代码实现如下:
/** * The synchronization state. */ private volatile int state; /** * Returns the current value of synchronization state. * This operation has memory semantics of a {@code volatile} read. * @return current state value */ protected final int getState() { return state; } /** * Sets the value of synchronization state. * This operation has memory semantics of a {@code volatile} write. * @param newState the new state value */ protected final void setState(int newState) { state = newState; } /** * Atomically sets synchronization state to the given updated * value if the current state value equals the expected value. * This operation has memory semantics of a {@code volatile} read * and write. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that the actual * value was not equal to the expected value. */ protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
3. ReentrantLock
ReentrantLock是一个可重入锁(可重入锁是指同一个线程可以多次获取这一把锁--注意是同一把锁,如果不是同一把不满足可重入性)。在释放锁之前该线程再次访问了该同步锁的其他方法时,这个线程不需要再次竞争锁便可持有该锁,只是需要记录重入次数+1.重入锁的设计目的是为了解决死锁的问题(所谓死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局。比如threadA按照先获得锁a再获得锁b的顺序获得锁;threadB按照先获得锁b再获得锁a顺序获得锁,这样threadA和threadB就产生了死锁)
可重入锁,简单来说就是一个线程如果抢占了互斥锁资源,在锁释放前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数
在多线程并发编程里面,绝大部分锁都是可重入的,比如Synchronized(比如线程安全单例模式,就是使用二次synchronized锁实现的)、ReentrantLock等,但也有不支持重入的锁,比如JDK8里面提供的读写锁StampedLock
锁的可重入性,主要解决的问题是避免线程死锁的问题。
示例如下,两个线程都是同一个类锁,\两个线程间不会发送死锁情况
public class TestLock1 {
public static void main(String[] args) {
new Thread(() -> {
new LockClass().lock1();
}).start();
new Thread(() -> {
new LockClass().lock2();
}).start();
}
}
class LockClass{
public static synchronized void lock1(){
System.out.println("LockClass.lock1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockClass1.lock1();
}
public static synchronized void lock2(){
System.out.println("lock2");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1();
}
}
如上,由于一个已经获取同步锁X的线程,在释放锁X之前再去竞争锁X的时候,相当于会出现自己要等待自己释放锁,所以只有可重入锁才能解决类似死锁问题
3.1 ReentrantLock作用范围
Lock只加在代码块上,需要手动解锁,Lock锁住的是当前对象,作用范围是.lock()~.unlock()
public class ReentrantLockTest {
private static Lock lock=new ReentrantLock();
private static int count=0;
private static void inc(){
try{
// lock.lock(); //条件1
count++;
Thread.sleep(10);
desc();
}catch(Exception e){
e.printStackTrace();
}finally {
// lock.unlock(); //条件1
}
}
private static void desc(){
--count;
}
public static void main(String[] args) {
for (int i = 0; i < 20000; i++) {
new Thread(() -> {
inc();//1
}).start();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count:"+count);
}
}
上述代码执行结果:
count:212(每次执行结果可能不一样,因为线程不安全)
当把条件1注释去掉后后每次执行结果:
count: 1
3.2 重入锁
如下例中红色标记部分
inc() 方法获取锁成功并没有释放锁的情况下调用dec()再次获取锁,假如没有重入锁的话这里会导致死锁。此线程如果再次访问了该同步锁的其他的方法,这个线程不需要再次竞争锁,只需要记录重入次数
public class ReentrantLockTest { private static Lock lock=new ReentrantLock(); private static int count=1; private static void inc(){ try{ lock.lock(); count++; Thread.sleep(10); desc(); }catch(Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } private static void desc(){ lock.lock(); --count; lock.unlock(); } public static void main(String[] args) { for (int i = 0; i < 20000; i++) { new Thread(() -> { inc();//1 }).start(); } try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count:"+count); } }
参考:https://www.jianshu.com/p/da9d051dcc3d
https://www.jianshu.com/p/7e2ec0eccd1e
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?