1.概叙
1.1多线程下的线程安全问题(并发问题)
package com.zxx.study.base.thread;
import com.zxx.study.base.util.ZhouxxTool;
/**
* @author zhouxx
* @create 2024-07-12 10:35
*/
public class ThreadTest {
public static long count = 0;
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
ZhouxxTool.printTimeAndThread(count);
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
ZhouxxTool.printTimeAndThread(count);
});
t1.start();
t2.start();
t1.join();
t2.join();
ZhouxxTool.printTimeAndThread(count);
}
}
2024-07-12 10:41:44 564 | 1720752104567 | 12 | Thread-1 | 52839
2024-07-12 10:41:44 563 | 1720752104567 | 11 | Thread-0 | 57579
2024-07-12 10:41:44 567 | 1720752104567 | 1 | main | 57579
2024-07-12 10:42:43 835 | 1720752163839 | 12 | Thread-1 | 52881
2024-07-12 10:42:43 836 | 1720752163839 | 11 | Thread-0 | 62968
2024-07-12 10:42:43 840 | 1720752163840 | 1 | main | 62968
一般来说,t1 和 t2 都对 count 这个变量进行 count++ 这个操作,那么 count 最后的输出结果按理来说应该为 10万。但是输出的结果是不一定为 10 万,而且等于 10 万的概率非常非常非常小。
每次的运行结果都是不一样的,很大概率都是在 5 万到 10 万之间“徘徊”。也有可能会小于 5 万。
出现了以上的结果,都是因为多线程抢占式执行随机调度,从而导致的结果。
具体分析:
在 CPU 中执行 count++ 这一操作,大致需要执行三条指令:
1)load:将内存中的数据读取加载到 CPU 的寄存器中。
2)add:在寄存器中的值 +1 操作。
3)save:把寄存器中的值写回到内存中。
现在 t1 与 t2 两个线程并发的进行 count++ ,多线程的执行是随机调度,抢占式的执行模式。有可能会出现以下几种情况:
出现可能 1 或者可能 2 这两次 count++ 的最终结果都是 2 ,因此是正确的。而对于可能 3 这种情况,虽然说,t1 与 t2 都完成了 count++ 的操作,但是,对于可能 3 这种情况,在 t2 完成 count++ 之后,count 由 0 改为 1 ,再把值写回到内存之后,但是 t1 来说,同样也是把 count 由 0 改为 1 ,写回到内存中。简单来说,t1 将 t2 线程中 count 进行了一次覆盖,重新赋值,所以 t2 这个线程的操作是无效操作。
出现的情况有无数种,不可预计的。之所以说,最后出现的结果为 10 万的概率是非常非常小的,几乎没有可能吧。
对于出现小于 5 万的情况:
按理来说,进行了三次 count++ 操作,最后的结果应该为: count == 3,但是这里最后的结果:count == 1。这就是有可能出现 count 小于 5万的可能,出现数越加越小了。
以上就是属于多线程引发的线程安全问题。其根本原因就是JMM,由java内存模型决定。
具体原因:线程修改的值,先要从主内存中copy一个副本保存再本地内存,而后做的修改操作都是修改本地内存中的副本,修改完之后再同步到主内存。上面例子中两个线程因为修改count先后原因,存在相互覆盖的情况。最终值很难达到10万。
1.2 为什么需要锁?
多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况;这种资源可能是:对象、变量、文件等。
由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问,那么我们怎么解决线程并发安全问题?
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是 序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
所以锁就是一种用来解决线程安全问题的机制。
在Java中,锁的主要用途是控制多个线程对共享资源的访问,以确保线程安全。Java提供了多种锁机制,包括内置的synchronized
关键字和java.util.concurrent.locks
包下的锁接口和类。
package com.zxx.study.base.thread;
import com.zxx.study.base.util.ZhouxxTool;
/**
* @author zhouxx
* @create 2024-07-12 10:35
*/
public class ThreadTest {
public static long count = 0;
public synchronized static void add(){
count++;
};
public static long get(){
return count;
};
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
// count++;
add();
}
// ZhouxxTool.printTimeAndThread(count);
ZhouxxTool.printTimeAndThread(get());
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
// count++;
add();
}
// ZhouxxTool.printTimeAndThread(count);
ZhouxxTool.printTimeAndThread(get());
});
t1.start();
t2.start();
t1.join();
t2.join();
// ZhouxxTool.printTimeAndThread(count);
ZhouxxTool.printTimeAndThread(get());
}
}
2024-07-12 11:29:41 161 | 1720754981164 | 11 | Thread-0 | 100000
2024-07-12 11:29:41 162 | 1720754981164 | 12 | Thread-1 | 53965
2024-07-12 11:29:41 165 | 1720754981165 | 1 | main | 100000
2024-07-12 11:30:42 673 | 1720755042675 | 11 | Thread-0 | 100000
2024-07-12 11:30:42 673 | 1720755042675 | 12 | Thread-1 | 86752
2024-07-12 11:30:42 676 | 1720755042676 | 1 | main | 100000
2024-07-12 11:31:05 235 | 1720755065238 | 12 | Thread-1 | 100000
2024-07-12 11:31:05 235 | 1720755065238 | 11 | Thread-0 | 98128
2024-07-12 11:31:05 239 | 1720755065239 | 1 | main | 100000
调整一下代码,加上synchronized 锁,最终count的结果就是10万,每个线程加5万。
1)join() 是 Thread 类的方法,用于等待调用该方法的线程执行完成。当一个线程调用另一个线程的 join() 方法时,它会被阻塞,直到被调用的线程执行完成。
2)synchronized 关键字用于实现线程同步确保在同一时刻只有一个线程可以访问某个代码块或方法。
总的来说,join 用于线程之间的协作和等待,而 synchronized 用于实现线程之间的同步和互斥访问共享资源。
1.3 java中一定需要锁吗?
需要锁的条件:并行争用临界资源(即存在线程安全问题)。这里的“并行争用”即读写并行发生,或者写写并行发生。
不需要加锁的条件:不存在线程安全问题,则不用加锁。例如:并行争用临界资源,如果只是“读读”并行发生,则不用加锁。还有一个就是CPU是单核,且不支持超线程,这个从根上消除了“争用临界资源”的问题,CPU所有指令都是串行执行。
1.4 对谁用锁?
在分布式系统中,决定对哪些字段使用锁通常取决于业务场景和数据的访问模式。以下是一些常见的情况,其中可能需要使用锁来保护字段:
唯一性约束:
当某个字段需要保证唯一性时,比如用户名、电子邮件地址或数据库中的主键,你可能需要在插入或更新这些字段时使用锁。
共享资源:
如果多个服务或线程需要访问和修改同一个资源,例如库存量、座位数或任何形式的计数器,那么对这些字段的访问通常需要加锁。
一致性要求:
在某些业务场景中,数据的一致性非常重要,不允许出现并发修改导致的脏读、不可重复读或幻读等情况,这时需要对相关字段加锁。
状态转换:
当一个对象或记录有多个状态,并且状态转换需要按照特定顺序进行时,可能需要在对状态字段进行更新时使用锁。
避免竞态条件:
如果两个或多个线程的操作之间存在竞态条件,即它们的执行顺序会影响结果,那么通常需要使用锁来控制对这些关键字的访问。
事务性操作:
在执行需要原子性、一致性、隔离性和持久性(ACID属性)的操作时,可能需要对涉及的字段使用锁以确保事务的正确执行。
在实际应用中,并不是所有的字段都需要加锁。过度使用锁可能会导致系统性能下降,因此需要根据实际需求和数据访问模式来决定哪些字段需要加锁。
2.synchronized原理
2.1 synchronized具体实现
1、同步代码块采用monitorenter、monitorexit指令显式的实现。
2、同步方法则使用ACC_SYNCHRONIZED标记符隐式的实现。
通过实例来看看具体实现:
public class SynchronizedTest{
public synchronized void method1(){
System.out.println("Hello World!");
}
public void method2(){
synchronized(this){
System.out.println("Hello World!");
}
}
}
编译后的字节码
monitorenter:每一个对象都有一个monitor,一个monitor只能被一个线程拥有。当一个线程执行到monitorenter指令时会尝试获取相应对象的monitor,获取规则如下:如果monitor的进入数为0,则该线程可以进入monitor,并将monitor进入数设置为1,该线程即为monitor的拥有者。
如果当前线程已经拥有该monitor,只是重新进入,则进入monitor的进入数加1,所以synchronized关键字实现的锁是可重入的锁。
如果monitor已被其他线程拥有,则当前线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor。
monitorexit :只有拥有相应对象的monitor的线程才能执行monitorexit指令。每执行一次该指令monitor进入数减1,当进入数为0时当前线程释放monitor,此时其他阻塞的线程将可以尝试获取该monitor。
2.2 synchronized实现原理
synchronized是基于Monitor来实现同步的。
锁升级:
无锁->偏向锁->轻量级锁->重量级锁
sychronized原理:
wait/notify
Monitor从两个方面来支持线程之间的同步:互斥执行
协作
1、Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。
2、使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。
3、Class和Object都关联了一个Monitor。
线程进入同步方法中。
为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)
拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。
其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
同步方法执行完毕了,线程退出临界区,并释放监视锁。
2.3 synchronized 的实现涉及哪些底层操作系统的支持?
synchronized 的实现涉及到了 Java 虚拟机(JVM)层面的锁机制,以及底层操作系统对线程和进程同步的支持,从操作系统角度来看涉及以下内容:
线程调度:
当线程因为竞争锁而被阻塞时,操作系统会负责线程的调度。操作系统会根据不同的调度算法(如时间片轮转、优先级调度等)来决定哪个线程应该获得 CPU 时间片并执行。
互斥量(Mutex):
重量级锁的实现通常依赖于操作系统的互斥量机制。互斥量是一种同步原语,用于保护临界区资源,确保同一时间只有一个线程可以访问临界区。
信号量(Semaphore):
有时,synchronized 的实现还可能涉及到操作系统的信号量机制。信号量是一种用于控制多个线程对共享资源访问的同步原语,它可以维护一个计数器,表示可用资源的数量。
自旋锁(Spinlock):
轻量级锁在自旋等待时,可能会使用到自旋锁。自旋锁是一种特殊的锁,当线程无法获取锁时,它会持续检查锁的状态,而不是立即被阻塞。这种策略在锁被持有时间较短,且线程切换开销较大的场景下可能更有效。
内存屏障(Memory Barrier):
为了确保线程安全,JVM 在实现 synchronized 时还需要考虑内存模型。Java 内存模型(JMM)定义了一组规则,用于确保线程间的正确同步。为了实现这些规则,JVM 可能会插入内存屏障指令,以确保指令的顺序性和可见性。
2.4 synchronized
优点
2.5 类锁、对象锁
synchronized
的使用一般就是同步方法和同步代码块。synchronized的锁是基于对象实现的。
如果使用同步方法
- static:此时使用的是当前类.class作为锁(类锁)
- 非static:此时使用的是当前对象做为锁(对象锁)
public class MiTest {
public static void main(String[] args) {
// 锁的是,当前Test.class
Test.a();
Test test = new Test();
// 锁的是new出来的test对象
test.b();
}
}
class Test{
public static synchronized void a(){
System.out.println("1111");
}
public synchronized void b(){
System.out.println("2222");
}
}
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
对象锁和类锁示例
package com.zxx.study.base.thread.lock;
import com.zxx.study.base.util.ZhouxxTool;
/**
* @author zhouxx
* @create 2024-07-12 12:57
*/
public class LockTask {
// 静态方法 类锁
public synchronized static void task1() {
ZhouxxTool.printTimeAndThread("start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ZhouxxTool.printTimeAndThread( "end");
}
// 静态方法 类锁
public synchronized static void task2() {
ZhouxxTool.printTimeAndThread( "start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ZhouxxTool.printTimeAndThread( "end");
}
// 非静态方法 对象锁
public synchronized void task3() {
ZhouxxTool.printTimeAndThread( "start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ZhouxxTool.printTimeAndThread( "end");
}
static class Thread1 extends Thread {
LockTask lockTask;
public Thread1(LockTask lockTask) {
this.lockTask = lockTask;
}
@Override
public void run() {
lockTask.task1();
}
}
static class Thread2 extends Thread {
LockTask lockTask;
public Thread2(LockTask lockTask) {
this.lockTask = lockTask;
}
@Override
public void run() {
lockTask.task2();
}
}
static class Thread3 extends Thread {
LockTask lockTask;
public Thread3(LockTask lockTask) {
this.lockTask = lockTask;
}
@Override
public void run() {
lockTask.task3();
}
}
}
启动类
package com.zxx.study.base.thread.lock;
/**
* @author zhouxx
* @create 2024-07-12 13:05
*/
public class LockTaskTest {
public static void main(String[] args) {
LockTask lockTask = new LockTask();
LockTask.Thread1 thread1 = new LockTask.Thread1(lockTask);
LockTask.Thread2 thread2 = new LockTask.Thread2(lockTask);
LockTask.Thread3 thread3 = new LockTask.Thread3(lockTask);
thread1.setName("thread1");
thread2.setName("thread2");
thread3.setName("thread3");
thread1.start();
thread2.start();
thread3.start();
}
}
2024-07-12 13:06:23 642 | 1720760783642 | 11 | thread1 | start
2024-07-12 13:06:23 642 | 1720760783642 | 13 | thread3 | start
2024-07-12 13:06:24 654 | 1720760784654 | 11 | thread1 | end
2024-07-12 13:06:24 654 | 1720760784654 | 12 | thread2 | start
2024-07-12 13:06:24 654 | 1720760784654 | 13 | thread3 | end
2024-07-12 13:06:25 654 | 1720760785654 | 12 | thread2 | end
对象锁和类锁是不同的锁,所以多个线程同时执行这2个不同锁的方法时,是异步的。
在LockTask类中定义三个方法,task1方法和task2方法为类锁,task3方法为对象锁。
创建三个线程类,分别执行task1,task2,task3:虽然创建的LockTask lockTask = new LockTask();都是同一个对象,执行同一个lockTask对象的三个方法。
thread1(lockTask.task1())和thread2(lockTask.task2())两个线程执行同一个对象的不同方法,因为task1()和task2()两个方法都是静态方法,所以是同一把锁--类锁,所以thread1和thread2相互有等待。
thread1(lockTask.task1())和thread3(lockTask.task3())两个线程执行同一个对象的不同方法,因为task1()是静态方法,task3()是对象方法,所以是两把不同的锁,所以thread1和thread3没有相互有等待,并行执行。
临界资源必须统一加锁
临界资源必须统一加锁,否则加锁失效,还是存在线程安全问题。
2.6 synchronized的优化
在JDK1.5的时候,Doug Lee推出了ReentrantLock
,lock的性能远高于synchronized
,所以JDK团队就在JDK1.6中,对synchronized
做了大量的优化。
jvm锁优化参考:科普文:jvm对锁的优化-CSDN博客
3.java.util.concurrent.locks原理
1、java并发包下面的锁主要就两个,ReentrantLock(实现Lock接口) 和ReentrantReadWriteLock(实现ReadWriteLock接口)。
2、ReentrantLock类构造函数如下, sync是Sync的实例,NonfairSync(非公平锁)和FairSync(公平锁)是Sync的子类。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
3、ReentrantReadWriteLock类构造函数如下,共有三个属性,sync、readerLock、writerLock
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
我们看到ReentrantLock和ReentrantReadWriteLock都的实现都依赖于sync这个对象。sync是AbstractQueuedSynchronizer的实例。AbstractQueuedSynchronizer就是java并发包下面实现锁和线程同步的基础,AbstractQueuedSynchronizer就是大名鼎鼎的AQS队列,下文我们都用AQS来表示AbstractQueuedSynchronizer。
与synchronized不同,Lock完全用Java写成,在java这个层面是无关JVM实现的。在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。
3.1 LOCK类图
3.2 AQS(全称AbstractQueuedSynchronizer)类图
3.3 Lock和AQS的关联全图
3.4 全局看AQS实现逻辑
在AQS(AbstractQueuedSynchronizer)里有一个数据结构Node,这个数据结构有一个prev和next属性,实现了一个双向链表的功能,每一个调用lock的线程进来,如果拿不到锁就会加入这个队列进行等待,拿到了锁就是修改state变量
Java的Lock锁机制是通过Unsafe类中的原生方法CAS功能来让多线程竞争AQS类中一个volatile修饰的(int)state变量(0表示当前无主,可以竞争)。对于无法获取锁的线程则通过一个双向队列的维持,借助Unsafe中(native)park功能对这些线程设置为等待。在锁释放的时候,再去队列头通过unpark来唤醒该线程继续工作,就是这么简单,没什么神秘的。
可重入能力是重入的时候check下当前的owner是不是本线程,如果是的话,直接state+1,实现了计数,也就是可重入的能力,锁释放的时候state-1,回到0的状态。
参考:https://zhuanlan.zhihu.com/p/694367368
3.5 ReentrantLock实现原理
1、如何加锁
ReentrantLock使用方式如下
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}}
lock方法的实现原理
ReentrantLock锁的实现分为公平锁(FairSync)和非公平锁(NonFairSync),所以lock方法的实现自然有两个版本
非公平锁中和公平锁中lock的实现
非公平锁中lock的实现 | 公平锁中lock的实现 |
---|---|
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } | final void lock() { acquire(1); } |
acquire是AQS中方法,执行过程中还是调用的子类的实现方法,所以不要简单地以为公平锁和非公平锁的实现就如表格中那么小的差异。
非公平锁代码解析
- lock方法
final void lock() {
if (compareAndSetState(0, 1)) //设置AQS中state的值,如果state当前是0(无锁),那么设置state为1,加锁成功
setExclusiveOwnerThread(Thread.currentThread());// 设置独占锁的拥有线程
else
acquire(1);
}
- acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法试着加锁,主要实现在nonfairTryAcquire中
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 当前没有锁竞争,那么CAS方式加锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {// 如果当前锁的拥有者急速当前线程,因为是ReentrantLock是可重入的,所以state加1,获得锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如果tryAcquire加锁没有成功,那么那么需要将当前线程加入AQS队列,并且阻塞。先看addWaiter 方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { // 如果队列已经初始化,那么加入到队列末尾
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);// 一开队列为空,那么初始化队列,并且入队列
return node;
}
enq方法是入队列操作,改方法设计的很巧妙,多线程情况下通过CAS操作保证线程安全。该方法是一个死循环,在设置头结点和尾结点时候都用了CAS操作,这样就保证了设置头尾结点只有一个线程可以设置成功,CAS操作失败的再次进入循环,加到上一个CAS操作成功结点的后面,注意这个队列的头结点是空结点
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))// CAS操作初始化队列,头结点是空结点
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {// CAS操作设置尾结点,CAS操作失败的依次加入到队列的末尾
t.next = node;
return t;
}
}
}
}
以上只是入队列操作,那么阻塞线程的逻辑在哪里呢?我们来看acquireQueued方法
inal boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();// 获得该节点的前驱,因为队列已经初始化,那么该队列肯定不是空的了
if (p == head && tryAcquire(arg)) {// 有可能该节点已经变成了头结点,那么再次试着去获得锁,获取成功就不用阻塞了
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 不是头结点
if (shouldParkAfterFailedAcquire(p, node) &&// 设置结点的状态为阻塞
parkAndCheckInterrupt()) // 阻塞线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法,一开始Node结点的waitStatus肯定是0,那么设置结点的waitStatus为SIGNAL(-1),也就是需要unparking
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
执行到这里了,中途其实是有多次机会让现场获得锁的,但是如果还没有成功,那么我只能阻塞啦,这个就不能怪我手下无情了,好,那么我们看阻塞的代码
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这里阻塞线程调用的是 LockSupport提供的park操作,park操作的实现就需要调用底层操作系统的阻塞原语啦。
到这里我们已经把非公平锁的思路将明白了,整个代码写的可以说是滴水不漏,构思巧妙,用到了大量的CAS操作,这也是为什么说Lock是乐观锁的原因
公平锁代码解析
final void lock() {
acquire(1); //lock的代码是不是比非公平锁的代码少了一点啥,自已比较下
}
下面这个方法看起来和非公平锁中是一样的,其实差别就在tryAcquire方法的具体实现上不一样,其他的地方是一样的,tryAcquire是调用的子类FairSync中的tryAcquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法如下:
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;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
细心的同学发现这段代码仅仅比非公平锁中tryAcquire方法中多了一个判断,就是下面的方法,下面的方法就是判断队列中是不是还有其他线程结点在等待。如果没有,那么当前线程可以抢占锁,如果有那么,你需要乖乖的排到队列的最后等待并且被阻塞。
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());
}
公平锁与非公平锁总结
公平锁和非公平锁的公平性和非公平主要体现在新调用lock方法的线程是不是可以去抢占锁(也就是state=0时,可不可去设置为1),非公平锁是不是可以通过CAS操作去设置state为1的;但是公平锁需要先判断AQS队列中有没有结点阻塞,如果没有,那么该线程可以设置state为1,如果有,那么乖乖去AQS队列末尾等待并且阻塞。
这里有的同学可能会有疑问,可以错误的以为state=0那么AQS队列就是空。这个错误的。state=0,AQS可能有有很多线程结点在等待的。我们看一下unlock方法就知道了,我们前面花了很多的篇幅去讨论lock方法。unclock比较简单,我们来看下
解锁
无论公平锁还是非公平锁,unlock 都是调用的AQS中的release方法。
public void unlock() {
sync.release(1);
}
release方法中先tryRelease一下
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease中主要设置state的值,一般也就是把state=1,设置为0。表示当前是没有锁啦,你们可以来抢占啦。我们刚刚又说道state=0,AQS队列中可能是有很多结点还在等待中的。如果这个时候我们刚unlock了一下,刚刚把state设置0,但是还没有唤醒在队列上等待的线程。那么如果是非公平锁,那么新的线程很有可能就是抢到锁了(CAS把state设置为1)。
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;
}
state 也设置为0,那么我们是不是应该把阻塞在队列中的线程唤醒了,从AQS队列的头结点开始唤醒,也就是执行下面的unparkSuccessor方法,同样的调用LockSupport中unpark函数。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
一口气说了这么多,那么线程在哪里被唤醒呢?当然是在哪里阻塞在哪里唤醒,我们回到阻塞的地方。现在是在acquireQueued中parkAndCheckInterrupt方法中被阻塞的。
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们注意这里是一个死循环,当线程被唤醒,并且是头结点(我们是从第一个结点开始唤醒的),那么又调用tryAcquire方法参与锁的竞争。说的这里ReentrantLock将说的差不多了,反正就是一直竞争啊竞争争
Java中的锁实现和使用
在Java中,锁的主要用途是控制多个线程对共享资源的访问,以确保线程安全。Java提供了多种锁机制,包括内置的synchronized
关键字和java.util.concurrent.locks
包下的锁接口和类。
Lock和synchronized的区别
自动挡和手动挡的区别
Lock
: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
-
1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别
-
2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
-
3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
-
4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。
-
5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
-
6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
synchronized的优势:
-
足够清晰简单,只需要基础的同步功能时,用synchronized。
-
Lock应该确保在finally块中释放锁。如果使用synchronized,JVM确保即使出现异常,锁也能被自动释放。
-
使用Lock时,Java虚拟机很难得知哪些锁对象是由特定线程锁持有的。
ReentrantLock 和synchronized的区别
ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。
相同点:
-
1.主要解决共享变量如何安全访问的问题
-
2.都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,
-
3.保证了线程安全的两大特性:可见性、原子性。
不同点:
-
1.ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。
-
2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,ReentrantLock 为处理锁的不可用性提供了更高的灵活性
-
3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的
-
4.ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。
-
5.ReentrantLock 通过 Condition 可以绑定多个条件
锁的分类
锁机制是一种在计算机科学中广泛使用的技术,用于控制多个进程或线程对共享资源的访问。在多任务操作系统中,锁机制确保了当一个进程正在使用某个资源时,其他进程不能同时访问该资源,从而防止了数据不一致或冲突的问题。
锁机制可以分为两大类:
悲观锁(Pessimistic Locking):
悲观锁假设并发冲突会发生非常频繁,因此在整个操作过程中都会持有锁。
例如,在数据库中,悲观锁通常是通过在数据行上使用SELECT ... FOR UPDATE语句来实现的,这样其他事务就无法修改这些行直到锁被释放。
乐观锁(Optimistic Locking):
乐观锁假设并发冲突发生的概率较低,因此不会在整个操作过程中持有锁。
而是在更新数据时检查是否有其他进程同时修改了数据。如果有冲突,则拒绝更新。
乐观锁通常通过版本号或时间戳来实现,每次更新数据时都会检查版本号或时间戳是否发生了变化。
在实现锁时,还可以根据锁的范围和性质进一步分类:
互斥锁(Mutex):确保只有一个线程可以访问某个资源。
读写锁(Read-Write Lock):允许多个读操作同时进行,但写操作是独占的。
分布式锁:在分布式系统中使用的锁,用于控制跨多个机器或节点的资源访问。
Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock
Java中锁可以分为以下几种类型:
-
内部锁(Intrinsic Locks)或者内置锁:通常指的是synchronized关键字,它是Java语言的一部分,并且内置在JVM中。
-
显示锁(ReentrantLock):是java.util.concurrent.locks包中的一部分,是一个显式锁类,需要手动加锁和释放锁。
-
读写锁(ReadWriteLock):也是java.util.concurrent.locks包中的一部分,具有读锁和写锁两种,用于提高读操作的性能。
-
条件锁(Condition):与显示锁(ReentrantLock或其他显示锁实现)一起使用,提供了更细粒度的线程同步工具。
-
偏向锁、轻量级锁和重量级锁:这是锁的升级过程,是Java虚拟机(JVM)内部实现锁的优化手段。
-
分布式锁:如ZooKeeper实现的分布式锁,用于控制分布式系统中的同步访问资源。
-
公平锁和非公平锁:是显示锁(ReentrantLock)的两种类型,公平锁意味着获取锁的顺序应该与线程加锁请求的顺序相同,而非公平锁可以违反这个原则。
公平锁 / 非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock
而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。 对于Synchronized
而言,也是一种非公平锁。由于其并不像ReentrantLock
是通过AQS
的来实现线程调度,所以并没有任何办法使其变成公平锁。
可重入锁 / 不可重入锁
可重入锁
广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。ReentrantLock
和synchronized
都是可重入锁
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
不可重入锁
不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁。看到一个经典的讲解,使用自旋锁来模拟一个不可重入锁,代码如下
import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
//这句是很经典的“自旋”语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
代码也比较简单,使用原子引用来存放线程,同一线程两次调用lock()方法,如果不执行unlock()释放锁的话,第二次调用自旋的时候就会产生死锁,这个锁就不是可重入的,而实际上同一个线程不必每次都去释放锁再来获取锁,这样的调度切换是很耗资源的。
把它变成一个可重入锁:
import java.util.concurrent.atomic.AtomicReference;
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
private int state = 0;
public void lock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
state++;
return;
}
//这句是很经典的“自旋”式语法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
if (current == owner.get()) {
if (state != 0) {
state--;
} else {
owner.compareAndSet(current, null);
}
}
}
}
在执行每次操作之前,判断当前锁持有者是否是当前对象,采用state计数,不用每次去释放锁。
ReentrantLock中可重入锁实现
这里看非公平锁的锁获取方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
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;
}
在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。
独享锁 / 共享锁
独享锁和共享锁在你去读C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,它俩一个是独享一个是共享锁。
独享锁:该锁每一次只能被一个线程所持有。
共享锁:该锁可被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。
另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。 对于Synchronized而言,当然是独享锁。
互斥锁 / 读写锁
互斥锁
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源
读写锁
读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态
读写锁在Java中的具体实现就是ReadWriteLock
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。 只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。
乐观锁 / 悲观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java
中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition
机制,其实都是提供的乐观锁。在Java
中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一。
在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能。在锁上发生竞争时将通水导致这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式—-每次只能有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。
我们一般有三种方式降低锁的竞争程度: 1、减少锁的持有时间 2、降低锁的请求频率 3、使用带有协调机制的独占锁,这些机制允许更高的并发性。
在某些情况下我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这成为分段锁。
其实说的简单一点就是:
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap
所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
比如:在ConcurrentHashMap中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16)个锁来保护。假设使用合理的散列算法使关键字能够均匀的分部,那么这大约能使对锁的请求减少到越来的1/16。也正是这项技术使得ConcurrentHashMap支持多达16个并发的写入线程。
偏向锁 / 轻量级锁 / 重量级锁
锁的状态:
1.无锁状态
2.偏向锁状态
3.轻量级锁状态
4.重量级锁状态
锁的状态是通过对象监视器在对象头中的字段来表明的。 四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。 这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)。
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
自旋锁
我们知道CAS算法是乐观锁的一种实现方式,CAS算法中又涉及到自旋锁,所以这里给大家讲一下什么是自旋锁。
简单回顾一下CAS算法
CAS
是英文单词Compare and Swap
(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization
)。CAS算法涉及到三个操作数
1.需要读写的内存值 V
2.进行比较的值 A
3.拟写入的新值 B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。
什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
Java如何实现自旋锁?
下面是个简单的例子:
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
自旋锁存在的问题
1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。 2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
自旋锁的优点
1、自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快 2、非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
可重入的自旋锁和不可重入的自旋锁
文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。
而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。
为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。
public class ReentrantSpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
private int count;
public void lock() {
Thread current = Thread.currentThread();
if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
count++;
return;
}
// 如果没获取到锁,则通过CAS自旋
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur = Thread.currentThread();
if (cur == cas.get()) {
if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
count--;
} else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
cas.compareAndSet(cur, null);
}
}
}
}
自旋锁与互斥锁
1.自旋锁与互斥锁都是为了实现保护资源共享的机制。
2.无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
3获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。
自旋锁总结
1.自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
2.自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
3.自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
4.自旋锁本身无法保证公平性,同时也无法保证可重入性。
5.基于自旋锁,可以实现具备公平性和可重入性质的锁。