你认识多少种锁?
-
偏向锁/轻量级锁/重量级锁
-
可重入锁/非可重入锁
-
共享锁/独占锁
-
公平锁/非公平锁
-
悲观锁/乐观锁
-
自旋锁/非自旋锁
-
可中断锁/不可中断锁
锁分别有什么特点?
偏向锁/轻量级锁/重量级锁
这三种锁特指synchronized
锁的状态,通过在对象头中的mark word
来表明锁的状态。
锁升级的路径:无锁→偏向锁→轻量级锁→重量级锁。
偏向锁:自始至终锁都不存在竞争,就没必要上锁,只需要打个标记就行了,这就是偏向锁的思想。适用于只有一个线程获取锁。当第二个线程尝试获取锁时,即使此时第一个线程已经释放了锁,此时还是会升级为轻量级锁。但是有一种特例,如果出现偏向锁的重偏向,则此时第二个线程可以尝试获取偏向锁。
轻量级锁:适用于多个线程交替获取锁。跟偏向锁的区别在于可以有多个线程来获取锁,但是必须没有竞争,如果有则会升级会重量级锁。或者是只有短时间的锁竞争,用 CAS 就可以解决,这种情况下,用完全互斥的重量级锁是没必要的。
重量级锁:重量级锁是互斥锁,它是利用操作系统的同步机制实现的,所以开销相对比较大。当多个线程直接有实际竞争,且锁竞争时间长的时候,轻量级锁不能满足需求,锁就会膨胀为重量级锁。
可重入锁/非可重入锁
可重入锁指的是线程当前已经持有这把锁了,能在不释放这把锁的情况下,再次获取这把锁。不可重入锁指的是虽然线程当前持有了这把锁,必须要先释放锁后才能再次尝试获取。
java的juc包中提供 ReentrantLock 就是可重入锁。
共享锁/独占锁
共享锁指的是我们同一把锁可以被多个线程同时获得,而独占锁指的就是,这把锁只能同时被一个线程获得。我们的读写锁,就最好地诠释了共享锁和独占锁的理念。读写锁中的读锁,是共享锁,而写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
公平锁/非公平锁
公平锁的公平的含义在于如果线程现在拿不到这把锁,那么线程就都会进入等待,开始排队,在等待队列里等待时间长的线程会优先拿到这把锁,遵循先来先得规则。而非公平锁会在一定情况下,忽略掉已经在排队的线程,发生插队现象。synchronized
是典型的非公平锁。
悲观锁/乐观锁
悲观锁比较悲观的,预设场景存在多线程在对资源进行竞争的,所以在获取资源之前,必须先拿到锁,以便达到“独占”的状态,当前线程在操作资源的时候,其他线程由于不能拿到锁。
乐观锁比较乐观的,预设场景不存多线程在对资源进行竞争的,所以它不要求在获取资源前拿到锁,也不会锁住资源;相反,乐观锁利用 CAS 理念,在不独占资源的情况下,完成了对资源的修改。
自旋锁/非自旋锁
自旋锁:如果线程拿不到锁,不会直接陷入阻塞或者释放 CPU 资源,而是开始利用循环,不停地尝试获取锁,这个循环过程被形象地比喻为“自旋”。
非自旋锁:如果拿不到锁就直接放弃,或者进行其他的处理逻辑,例如去排队、陷入阻塞等。
可中断锁/不可中断锁
不可中断锁:如果线程申请了锁,只能等到拿到锁以后才能进行其他的逻辑处理。synchronized
关键字修饰的锁代表的是不可中断锁。
可中断锁:如果线程申请了锁,不想获取了,可以在中断之后去做其他的事情,不需要一直等到获取到锁才离开。ReentrantLock
是一种典型的可中断锁,例如使用 lockInterruptibly
方法中断之后去做其他的事情。
讲讲java中悲观锁和乐观锁?
悲观锁:synchronized 关键字和 Lock 接口
Java 中悲观锁的实现包括 synchronized 关键字和 Lock 相关类等,我们以 Lock 接口为例,例如 Lock 的实现类 ReentrantLock,类中的 lock() 等方法就是执行加锁,而 unlock() 方法是执行解锁。处理资源之前必须要先加锁并拿到锁,等到处理完了之后再解开锁,这就是非常典型的悲观锁思想。
乐观锁:原子类
乐观锁的典型案例就是原子类,例如 AtomicInteger
在更新数据时,就使用了乐观锁的思想,多个线程可以同时操作同一个原子变量。
数据库悲观锁和乐观锁:
例如,我们如果在 MySQL 选择 select for update 语句,那就是悲观锁,在提交之前不允许第三方来修改该数据,这当然会造成一定的性能损耗,在高并发的情况下是不可取的。
相反,我们可以利用一个版本 version 字段在数据库中实现乐观锁。在获取及修改数据时都不需要加锁,但是我们在获取完数据并计算完毕,准备更新数据时,会检查版本号和获取数据时的版本号是否一致,如果一致就直接更新,如果不一致,说明计算期间已经有其他线程修改过这个数据了,那我就可以选择重新获取数据,重新计算,然后再次尝试更新数据。
两种锁各自的使用场景:
悲观锁适合用于并发写入多、临界区代码复杂、竞争激烈等场景,这种场景下悲观锁可以避免大量的无用的反复尝试等消耗。
乐观锁适用于大部分是读取,少部分是修改的场景,也适合虽然读写都很多,但是并发并不激烈的场景。在这些场景下,乐观锁不加锁的特点能让性能大幅提高。
Synchronized 各种加锁场景
-
作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。
public synchronized void method() {}
-
作用于静态方法,锁住的是类的 Class 对象,Class 对象全局只有一份,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。
public static synchronized void method() {}
-
作用于 Lock.class,锁住的是 Lock 的 Class 对象,也是全局只有一个。
synchronized (Lock.class) {}
-
作用于 this,锁住的是对象实例,每一个对象实例有一个锁。
synchronized (this) {}
-
作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。
public static Object monitor = new Object(); synchronized (monitor) {}
synchronized
获取和释放monitor
锁的时机?
在java中,通常利用 synchronized
关键字来修饰代码块或者修饰一个方法来实现同步。synchronized
的背后正是利用 monitor
锁实现的。所以首先我们来看下获取和释放 monitor
锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor
锁,获得 monitor
锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized
保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。synchronized
代码块实际上通过 monitorenter
和 monitorexit
指令实现加锁及释放锁的。
为什么调用Object
的wait
notify
notifyAll
方法,需要加synchronized
锁?
根本原因,因为这3个方法都会操作monitor
锁对象,所以需要先获取锁对象,而加 synchronized 锁可以让我们获取到锁对象。
synchronize 底层维护了几个列表存放被阻塞的线程?
synchronized
底层对应的 JVM 模型为 objectMonitor
,使用了3个双向链表来存放被阻塞的线程:_cxq(Contention queue)、_EntryList(EntryList)、_WaitSet(WaitSet)
。
当线程获取锁失败进入阻塞后,首先会被加入到 _cxq
链表,_cxq
链表的节点会在某个时刻被进一步转移到 _EntryList
链表。
当持有锁的线程释放锁后,_EntryList
链表头结点的线程会被唤醒,该线程称为 successor
(假定继承者),然后该线程会尝试抢占锁。
当我们调用 wait()
时,线程会被放入 _WaitSet
,直到调用了 notify()/notifyAll()
后,线程才被重新放入 _cxq
或 _EntryList
,默认放入 _cxq
链表头部。objectMonitor
的整体流程如下图:
为什么释放锁时被唤醒的线程会称为“假定继承者”?被唤醒的线程一定能获取到锁吗?
因为被唤醒的线程并不是就一定获取到锁了,该线程仍然需要去竞争锁,而且可能会失败,所以该线程并不是就一定会成为锁的“继承者”,而只是有机会成为,所以我们称它为假定的。这也是 synchronized
为什么是非公平锁的一个原因。
synchronized 是公平锁还是非公平锁?
非公平锁
synchronized 为什么是非公平锁?非公平体现在哪些地方?
1)当持有锁的线程释放锁时,该线程会执行以下两个重要操作:
-
先将锁的持有者
owner
属性赋值为 null。 -
唤醒等待链表中的一个线程(假定继承者)。
-
在1和2之间,如果有其他线程刚好在尝试获取锁(例如自旋),则可以马上获取到锁。
2)当线程尝试获取锁失败,进入阻塞时,放入链表的顺序,和最终被唤醒的顺序是不一致的,也就是说你先进入链表,不代表你就会先被唤醒
既然加了 synchronized 锁,那当某个线程调用了 wait 的时候明明还在 synchronized 块里,其他线程怎么进入到 synchronized 里去执行 notify 的?
public class SynchronizedTest {
private static final Object lock = new Object();
public static void testWait() throws InterruptedException {
synchronized (lock) {
// 阻塞住,被唤醒之前不会输出aa,也就是还没离开synchronized
lock.wait();
System.out.println("aa");
}
}
public static void testNotify() throws InterruptedException {
synchronized (lock) {
lock.notify();
System.out.println("bb");
}
}
}
当线程进入synchronized
时,需要获取lock
锁,但是在调用lock.wait()
的时候,此时虽然线程还在synchronized
块里,但是其实已经释放掉了lock
锁。所以其他线程此时可以获取lock
锁进入到synchronized
块,从而去执行lock.notify()
。
synchronized 和 ReentrantLock 的区别
-
底层实现:
synchronized
是 Java 中的关键字,是 JVM 层面的锁;ReentrantLock
是 JDK 层次的锁实现。 -
是否需要手动释放:
synchronized
不需要手动获取锁和释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;ReentrantLock
在发生异常时,如果没有主动通过unLock()
去释放锁,很可能会造成死锁现象,因此使用ReentrantLock
时需要在 finally 块中释放锁。 -
锁的公平性:
synchronized
是非公平锁;ReentrantLock
默认是非公平锁,但是可以通过参数选择公平锁。 -
是否可中断:
synchronized
是不可被中断的;ReentrantLock
则可以被中断。 -
灵活性:使用
synchronized
时,等待的线程会一直等待下去,直到获取到锁;ReentrantLock
的使用更加灵活,有立即返回是否成功的,有响应中断、有超时时间等。 -
性能上:随着近些年
synchronized
的不断优化,ReentrantLock
和synchronized
在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因。官方推荐尽量使用synchronized
,除非synchronized
无法满足需求时,则可以使用Lock
。
synchronized
和 Lock
如何选择?
-
如果能不用最好既不使用
Lock
也不使用synchronized
。因为在许多情况下你可以使用java.util.concurrent
包中的机制,它会为你处理所有的加锁和解锁操作,也就是推荐优先使用工具类来加解锁。 -
如果
synchronized
关键字适合你的程序, 那么请尽量使用它,这样可以减少编写代码的数量,减少出错的概率。因为一旦忘记在finally
里unlock
,代码可能会出很大的问题,而使用synchronized
更安全。 -
如果特别需要
Lock
的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用Lock
。
JVM 做了哪些锁优化?
-
偏向锁(
**Biased Locking**
):JVM会偏向于第一个获取锁的线程,在这个线程再次获取锁时,会直接获得锁,而不需要进行CAS操作。如果另一个线程尝试获取这个偏向锁,并且偏向锁已经偏向于其他线程,则会进行偏向锁的撤销操作,将偏向锁升级为轻量级锁。偏向锁适用于线程交替执行同步块的场景,大部分锁在整个同步周期内不存在竞争。 -
轻量级锁(
**Lightweight Locking**
):当一个线程尝试获取一个轻量级锁时,如果没有其他线程竞争,它将通过CAS操作成功获取锁。轻量级锁使用CAS
操作来实现加锁和解锁,避免了系统调用和线程阻塞的开销。如果存在锁竞争,轻量级锁可能会升级为重量级锁。 -
自旋锁(
**Spin Lock**
):自旋锁是一种忙等待的锁机制,当一个线程尝试获取一个被其他线程持有的锁时,它会进行自旋,即在循环中不断尝试获取锁,而不是立即进入阻塞状态。自旋锁适用于锁持有时间非常短的场景,可以减少线程上下文切换的开销。 -
自适应自旋(
**Adaptive Spinning**
):JDK 1.6引入了自适应自旋锁,自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,并将自旋等待时间延长。 -
锁消除(
**Lock Elimination**
):在编译时,通过逃逸分析等技术,如果JVM检测到一个锁不存在多线程竞争,会对这个对象的锁进行消除,减少不必要的锁操作。 -
锁粗化(
**Lock Coarsening**
):如果一个对象被锁定,并且长时间只有一个线程访问,JVM会将多个连续的锁操作合并成一个锁操作,减少锁的请求、同步与释放带来的性能损耗。
为什么要引入偏向锁和轻量级锁?为什么重量级锁开销大?
重量级锁底层依赖于系统的同步函数来实现,在 linux
中使用pthread_mutex_t
(互斥锁)来实现。
这些底层的同步函数操作会涉及到:操作系统用户态和内核态的切换、进程的上下文切换,而这些操作都是比较耗时的,因此重量级锁操作的开销比较大。
而在很多情况下,可能获取锁时只有一个线程,或者是多个线程交替获取锁,在这种情况下,使用重量级锁就不划算了,因此引入了偏向锁和轻量级锁来降低没有并发竞争时的锁开销。
偏向锁有撤销、膨胀,性能损耗这么大为什么要用呢?
偏向锁的好处是在只有一个线程获取锁的情况下,只需要通过一次 CAS 操作修改 markword
,之后每次进行简单的判断即可,避免了轻量级锁每次获取释放锁时的 CAS 操作。
如果确定同步代码块会被多个线程访问或者竞争较大,可以通过 -XX:-UseBiasedLocking
参数关闭偏向锁。
自旋发生在哪个阶段?
自旋发生在重量级锁阶段。
在 JDK 8 中,自旋的确是发生在重量级锁阶段,而不是轻量级锁阶段。之前主流的说法大多停留在对早期 JVM 优化的解释中,但从源码角度看,自旋的确是在重量级锁中进行的。轻量级锁阶段并没有自旋操作,在轻量级锁阶段,只要发生竞争,就是直接膨胀成重量级锁。而在重量级锁阶段,如果获取锁失败,则会尝试自旋去获取锁。
轻量级锁为什么不自旋?
在 JDK 8 中,轻量级锁并没有自旋,而是直接升级为重量级锁。这背后的原因主要与锁的竞争和开销控制有关。
-
轻量级锁的设计是为了优化低竞争环境下的线程同步。当线程通过 CAS 操作尝试获取轻量级锁失败时,JVM 假定竞争较为激烈,所以直接升级为重量级锁,而不是在这个阶段进行自旋。
-
轻量级锁阶段的目标是通过最小的开销(CAS 操作)来解决无竞争或低竞争的同步需求。一旦检测到竞争,它立即膨胀为重量级锁,因为轻量级锁的机制并不适合处理高竞争场景。进入重量级锁后,再通过自旋优化锁的获取。
为什么要设计自旋操作?
因为重量级锁的挂起开销太大。在重量级锁阶段,线程已经进入了一个竞争相对激烈的环境,然而锁的持有时间并不总是很长。为了避免在锁持有时间很短的情况下频繁阻塞、唤醒,JVM 引入了自旋锁机制。
自适应自旋是如何体现自适应的?
自适应自旋锁有自旋次数限制,范围在:1000~5000。
如果当次自旋获取锁成功,则会奖励自旋次数100次,如果当次自旋获取锁失败,则会惩罚扣掉次数200次。
所以如果自旋一直成功,则JVM认为自旋的成功率很高,值得多自旋几次,因此增加了自旋的尝试次数。
相反的,如果自旋一直失败,则JVM认为自旋只是在浪费时间,则尽量减少自旋。
synchronized 锁能降级吗?
答案是可以的。
具体的触发时机:在全局安全点(safepoint
)中,执行清理任务的时候会触发尝试降级锁。
当锁降级时,主要进行了以下操作:
1)恢复锁对象的 markword
对象头;
2)重置 ObjectMonitor
,然后将该 ObjectMonitor
放入全局空闲列表,等待后续使用。
synchronized
锁升级流程?
synchronized
的底层实现
synchronized
的底层实现主要区分:方法和代码块。synchronized
修饰代码块时,编译后会生成 monitorenter
和 monitorexit
指令,分别对应进入同步块和退出同步块。可以看到有两个 monitorexit
,这是因为编译时 JVM 为代码块添加了隐式的 try-finally
,在 finally
中进行了锁释放,这也是为什么 synchronized
不需要手动释放锁的原因。synchronized
修饰方法时,编译后会生成 ACC_SYNCHRONIZED
标记,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了则会先尝试获得锁。
两种实现其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
Mark Word?
在HotSpot
中,对象在堆内存储布局可以分为三部分:对象头(Header
)、实例数据(Instance Data
)、对齐填充(Padding
)
对象头(Header)
主要包含两类信息:Mark Word
和 类型指针
Mark Word
记录了对象的运行时数据,例如:HashCode
、GC分代年龄、偏向标记、锁标记、偏向的线程ID、偏向纪元(epoch
)等,32 位的 markword
如下图所示。
类型指针,指向它的类型元数据的指针,Java 虚拟机通过这个指针来确定该对象时哪个类的实例。如果对象是数组,则需要有一个用于记录数组长度的数据。
实例数据(Instance Data
)
对象存储的真正有效信息,即我们在代码里定义的各种类型的字段内容。
对齐填充(Padding
)
Hotspot
要求对象的大小必须是8字节的整数倍,因此,如果实例数据不是8字节的整数倍时,需要通过该字段进行填充。
什么是匿名偏向?
所谓的匿名偏向是指该锁从未被获取过,也就是第一次偏向,此时的特点是锁对象 markword
的线程 ID 为0。
当第一个线程获取偏向锁后,线程ID会从0修改为该线程的 ID,之后该线程 ID 就不会为0了,因为释放偏向锁不会修改线程 ID。
这也是为什么说偏向锁适用于:只有一个线程获取锁的场景。
偏向锁模式下 hashCode
存放在哪里?
偏向锁状态下是没有地方存放 hashCode
的。
因此,当一个对象已经计算过 hashCode
之后,就再也无法进入偏向锁状态了。
如果一个对象当前正处于偏向锁状态,收到需要计算其 hashCode
的请求时(Object::hashCode()
或者System::identityHashCode(Object)
方法的调用),它的偏向锁状态就会立即被撤销。
偏向锁流程?
首先,在开启偏向锁的时候,对象创建后,其偏向锁标记位为1。如果没开启偏向锁,对象创建后,其偏向锁标记位为0。
加锁流程:
1)从当前线程的栈帧中寻找一个空闲的Lock Record
,将 obj 属性指向当前锁对象。
2)获取偏向锁时,会先进行各种判断,如加锁流程图所示,最终只有两种场景能尝试获取锁:匿名偏向、批量重偏向。
3)使用 CAS 尝试将自己的线程 ID 填充到锁对象 markword
里,修改成功则获取到锁。
4)如果不是步骤2的两种场景,或者 CAS 修改失败,则会撤销偏向锁,并升级为轻量级锁。
5)如果线程成功获取偏向锁,之后每次进入该同步块时,只需要简单的判断锁对象 markword
里的线程ID是否自己,如果是则直接进入,几乎没有额外开销。
解锁流程:
偏向锁的解锁很简单,就是将 obj 属性赋值为 null,这边很重要的点是不会将锁对象 markword
的线程ID还原回0。
偏向锁流程中,markword
的状态变化如下图所示:
升级轻量级锁流程?
加锁流程:
如果关闭偏向锁,或者偏向锁升级,则会进入轻量级锁加锁流程。
1)从当前线程的栈帧中寻找一个空闲的 Lock Record,obj
属性指向锁对象。
2)将锁对象的 markword
修改为无锁状态,填充到 Lock Rrcord
的 displaced_header
属性。
3)使用 CAS 将对象头的 markword
修改为指向 Lock Record
的指针。
此时的线程栈和锁对象的关系如下图所示,可以看到2次锁重入的 displaced_header
填充的是 null。
解锁流程:
1)将 obj 属性赋值为 null。
2)使用 CAS 将 displaced_header
属性暂存的displaced mark word
还原回锁对象的 markword
。
升级重量级锁流程?
加锁流程:
当轻量级锁出现竞争时,会膨胀成重量级锁。
1)分配一个 ObjectMonitor
,并填充相关属性。
2)将锁对象的 markword
修改为:该 ObjctMonitor
地址 + 重量级锁标记位(10)。
3)尝试获取锁,如果失败了则尝试自旋获取锁。
4)如果多次尝试后还是失败,则将该线程封装成 ObjectWaiter
,插入到 cxq
链表中,当前线程进入阻塞状态
5)当其他锁释放时,会唤醒链表中的节点,被唤醒的节点会再次尝试获取锁,获取成功后,将自己从 cxq
(EntryList
)链表中移除
此时的线程栈、锁对象、ObjectMonitor
之间的关系如下图所示:
ObjectMonitor核心属性
ObjectMonitor() {
_header = NULL; // 锁对象的原始对象头
_count = 0; // 抢占该锁的线程数,_count大约等于 _WaitSet线程数 + _EntryList线程数
_waiters = 0, // 调用wait方法后的等待线程数
_recursions = 0; // 锁的重入数
_object = NULL; // 指向锁对象指针
_owner = NULL; // 当前持有锁的线程
_WaitSet = NULL; // 存放调用wait()方法的线程
_WaitSetLock = 0 ; // 操作_WaitSet链表的锁
_Responsible = NULL ;
_succ = NULL ; // 假定继承人
_cxq = NULL ; // 等待获取锁的线程链表,竞争锁失败后会被先放到cxq链表,之后再进入_EntryList链接
FreeNext = NULL ; // 指向下一个空闲的ObjectMonitor
_EntryList = NULL ; // 等待获取锁的线程链表,该链表的头结点是获取锁的第一候选者
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ; // 标记_owner是指向占用当前锁的线程的指针还是BasicLock,1为线程,0为BasicLock,发生在轻锁升级重锁的时候
_previous_owner_tid = 0; // 监视器上一个所有者的线程id
}
解锁流程:
1)将重入计数器-1,ObjectMonitor
里的 _recursions
属性。
2)先释放锁,将锁的持有者 owner
属性赋值为 null,此时其他线程已经可以获取到锁,例如自旋的线程。
3)从 EntryList
或 cxq
链表中唤醒下一个线程节点。
Lock
在Concurrent
包中的锁都是可重入锁(线程已经持有锁,同一个线程再次获取同一个锁是可以再次获取成功的),可重入锁这种设计有效避免死锁出现。
ReentrantLock
Lock是一个接口,其定义如下
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
常用的方法是lock(),unLock()
;其中lock()是不可中断的。lockInterruptibly()
是可以中断的。
锁的公平性和非公平性
Sync
抽象类有两个实现类FairSync
和 NonfairSync
分别对应 公平性锁和非公平性锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
锁的实现基本原理
Sync
父类AbstractQueueSynchronizer(AQS)
;AQS
实现的锁的功能和Synchronized
类似的,有阻塞或者唤醒等功能。需要几个核心要素:
-
需要一个
state
变量,标记锁的状态。对state
操作是保证线程安全的,需要用到CAS
。 -
需要记录当前哪个线程持有锁。
-
需要底层支持对一个线程的阻塞或者唤醒操作。
-
需要一个队列维护所有阻塞的线程。这个队列必须是无锁队列而且需要保证线程安全。
AQS :
private volatile int state; //记录锁的状态,通过CAS修改state值
private transient Thread exclusiveOwnerThread; //记录锁被哪个线程持有
state取值不仅可以是0、1,还可以大于1,就是为了支持锁的可重入性。state = 0 时,没有线程持有锁,exclusiveOwnerThread = null;
state = 1 时,有一个线程持有锁,exclusiveOwnerThread = 该线程;
当state > 1 时,说明该线程重入了该锁;
Unsafe类中,提供了阻塞或唤醒线程的一对操作原语。park和unPark。
public native void unpark(Object var1);
public native void park(boolean var1, long var2);
当线程调用park()时候,线程就会被阻塞,在另一个线程调用unPark(Thread t)
,传入被阻塞的线程,就可以唤醒被阻塞的线程。
AQS使用双向链表和CAS实现一个阻塞队列。
公平锁和非公平锁实现差异
FairSync的acquireQueued(..)和NonfairSync类似的
判断没有锁的时候会进行队列判断,是否属于队列第一个,从而实现公平性。
读写锁
ReentrantReadWriteLock
读写锁,读线程和读线程不需要互斥。