文章目录
是什么
可重入锁,用来控制多个线程访问共享资源的方式
使用范例
Lock lock = new ReentrantLock();
lock.lock();
try {
dosomething();
} finally {
lock.unlock();
}
在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。
继承结构
很简单,就只是继承了Lock接口
接口结构
Lock是个顶级接口,构建了锁的生态环境
- void lock();
获取锁。
如果该锁不可用,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到获得该锁为止
- void lockInterruptibly() throws InterruptedException;
可以被中断的获取锁,该方法响应中断,在锁的获取中可以中断线程,抛出异常并且释放锁或放弃获取锁
- boolean tryLock();
尝试非阻塞的方式获取锁,能获取则立即获取返回true,否则返回false
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
超时获取锁
规定时间内获得了锁,返回true
规定时间内没获得锁,返回false
线程被中断,返回false
- void unlock();
释放锁。
- Condition newCondition();
condition源码阅读
返回绑定到当前实例的新Condition实例。
在调用Conditionawait()之前,锁必须由当前线程持有。在Conditionawait()的调用后在进入等待状态之前自动释放该锁,并在等待返回之前重新获取该锁。
Condition实例的确切操作取决于Lock实现,并且必须由该实现记录
Lock相对于synchronized关键字增加的主要特性
特性 | 方法 | 描述 |
---|---|---|
非阻塞获取锁 | tryLock() | 尝试获取锁,如果锁无其他线程持有,则成功获取锁 |
能被中断获取锁 | lockInterruptibly() | 获取到锁的线程可以被中断,抛出异常并且释放锁 |
超时获取锁 | tryLock(long time, TimeUnit unit) | 一定的时间内获取锁,时间段内获取不到则返回false |
ReentrantLock 内部结构
- 图中可以明显的可以看到ReentrantLock实现了Lock接口。
- 内部聚合Sync,Sync继承AbstractQueuedSynchronizer(AQS)
- 并内部类FairSync,NonFairSync继承Sync,分别代表公平锁,与非公平锁。
- 总体上来说ReentrantLock依靠于AQS实现锁,其内部又将将锁分为公平锁于非公平锁。
AQS
是什么
队列同步器AbstractQueuedSynchronizer(AQS),首先他是个抽象类,用来构建锁或者其他同步组件的基础框架,是面向锁的构建。锁通过继承同步器并实现它的抽象方法来构建锁。
工作原理
- 其内部用一个volatile修饰变量来代表锁的状态
比如1代表有线程获取了锁,0代表没有线程获取锁。其因为被volatile修饰,每个线程总能读取到他最新修改的数据,在获取锁时又使用CAS算法保证其从0->1之间的修改的原子性。(volatile与CAS会在后文着重介绍) - 内部用一个队列来维持竞争锁的线程
没获取到锁的线程会进入队伍开始自旋检查是否可以尝试获得锁,或者进入休眠状态。当线程释放锁后,会唤醒后继线程竞争锁。
一个简单的独占锁的实现
类实现Lock接口,内部聚合AQS的子类Sync。将锁的所有方法代理到Sync即可。
public class Mutex implements Lock {
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 尝试获取锁,获取不到立马返回false
*/
@Override
protected boolean tryAcquire(int arg) {
//CAS 如果锁的状态为0,设置为1
if (compareAndSetState(0, 1)) {
//设置拥有锁的线程为CAS成功的线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试释放锁,获取不到立马返回false
*/
@Override
protected boolean tryRelease(int arg) {
//锁已经被释放了,抛出错误
if (getState() == 0) {
throw new
IllegalMonitorStateException();
}
//设置拥有锁的线程为Null
setExclusiveOwnerThread(null);
//volatile变量设置为0表示没有线程占用锁,具有线程可见性原子性
setState(0);
return true;
}
/**
* 锁是否处于占用状态
* getState()获取volatile变量的值
*
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
}
继承关系
- AbstractOwnableSynchronizer
用来简单表示当前锁属于哪个线程,其内部只有Thread 属性,和get set方法
一个结构
- 内置的FIFO队列
用来保存竞争锁,没有竞争到锁的的线程
两个基石
- volatile
保持线程可见性 - CAS
保证了读-改-写的原子性
volatile
volatile关键字 可见性 有序性 原子性 内存语义
保证了线程可见性,当线程A修改了volatile共享变量会立即刷新到主内存中,线程B读取volatile时会将从主内存中获取。
但是比如i++这种复合操作是不能保证原子性。线程A,B同时从主内存读取i的值,再进行++操作,最后赋值给i,再写回主内存,最终两个线程各执行一次++操作,但是内存中的值只加1。
CAS
Java的CAS会使用现代处理器上提供的高效机器级别的原子指令,指令以原子方式对内存执行读-改-写操作。
CAS有3个操作数,内存地址,旧的预期值A,要修改的新值B。当且仅当预期值A和内存地址中的值相同时,将内存值修改为B,否则什么都不做(这个读-改-写是原子的)。
伪代码
do{
int i = 内存地址存的值
int next = i++;
}while(!CAS(内存地址存,i,next))
CAS 多线程实现i++
public class Increment {
private static final Unsafe unsafe;
private static final long valueOffset;
public int value = 0;
static {
try {
//利用反射机制获取Unsafe类(该类不允许JDK以外的代码使用)
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//使用CAS完成i++
public final int incrementAndGet() {
for (;;) {
int current = getValue();
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
}
}
//CAS方法
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public int getValue() {
return value;
}
}
public static void main(String[] args) {
Increment increment = new Increment();
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 1000; j++) {
increment.incrementAndGet();
}
System.out.println(Thread.currentThread().getName()+":"+increment.getValue());
}
}.start();
}
}
控制台输出
Thread-1:1000
Thread-0:2000
Thread-2:3000
Thread-3:4000
Thread-7:6000
Thread-6:6000
Thread-5:7000
Thread-9:8000
Thread-4:9000
Thread-8:10000
10个线程,每个线程自增1000次,value的值最后都会成为10000。
CAS在修改内存中的值时,会先跟自己缓存中数据进行比较。如果预期值一样说明并没有线程修改过这个值,那么就修改(整个CAS的过程具有原子性)。
三个方法
这个三个方法是提供子类使用的,来完成对volatile修饰的状态的读取与修改的。
- getState()
获取当前同步状态 - setState(int newState)
设置当前同步状态 - compareAndSetState(int expect,int update)
使用CAS设置当前状态,该方法能够保证状态设置的原子性。
CAS volatile
其中三个方法正是使用了CAS volatile的如下属性,CAS volatile甚至是整个并发包实现的基石。
- CAS保证了多线程复合操作数据(读-改-写)修改的原子性
- volatile保证了线程可见性,与指令有序性(使用内存屏障,禁止了指令之间的重排序)
concurrent包实现结构,以为CAS volatile为基石
利用CAS volatile特性构建一系列帮助实现并发的工具,再根据工具实现其他的功能组件。
AQS的代码实现
核心属性
- private transient volatile Node head;
同步队列的头节点(一个结构内置的FIFO队列)
- private transient volatile Node tail;
同步队列的尾节点(一个结构内置的FIFO队列)
- private volatile int state;
同步状态,volatile保证线程可见性(两个基石中的一个)
工作原理
为什么头节点是空节点呢
在构造同步队列时(尾节点为NULL说明同步队列为空),会先构造一个空的节点放到同步队列中。
在线程获取到锁的时候,也会将代表自身线程的节点设置为头节点,并且线程内容置为空,这个后续能看到。
核心方法public final void acquire(int arg),获取锁的过程
tryAcquire()是空方法,留给锁自身实现,如果tryAcquire返回false没获得锁的情况下。
从内层往外看,先看addWaiter(Node.EXCLUSIVE)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
将当前线程构建成一个节点,并放入队列。
@参数模式Node.EXCLUSIVE表示独占,Node.SHARED表示共享
@返回构建完成的节点
private Node addWaiter(Node mode) {
//构建节点
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速加入到尾节点
//注意,可能存在多个线程正在竞争锁,多个线程都失败了同时加入尾节点,所以需要使用CAS方法加入尾节点
Node pred = tail;
//尾节点不为空的情况下,上文说到在构造同步队列时,会先构造一个空的节点放到同步队列中。
if (pred != null) {
node.prev = pred;
//如果CAS失败,说明还有其他线程在入队
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//快速CAS入队失败,调用enq方法入队
//如果同步队列时空,会先构造一个空的节点放到同步队列中。
enq(node);
return node;
}
将节点插入队列,经典的循环CAS,将元素入队。只要元素不入队就不返回。
如果同步队列中没内容,初始化的工作也会在这里完成:会创建一个空线程的节点,放入同步队列作为头节点
private Node enq(final Node node) {
//循环CAS,将元素入队
for (;;) {
Node t = tail;
// 初始化同步队列,创建一个空线程的节点,放入同步队列作为头节点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
到目前为止竞争锁失败的线程都进入到了队列中(锁的竞争发生在tryAcquire(arg)由子类利用三个方法自己实现)。
继续往下看这个方法acquireQueued(Node node)
先看他接受的参数,正是使用入队的线程构建的Node节点。
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;
}
//没获取成功(前驱节点非头节点或者CAS竞争失败)
//shouldParkAfterFailedAcquire()检查自己是否该休眠
//parkAndCheckInterrupt 休眠线程并返回中断状态
//shouldParkAfterFailedAcquire()放回false说明现在不是休眠的时机,进入下次自旋
//返回true说明适合休眠,调用 parkAndCheckInterrupt()进行休眠
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
先说下这个方法干嘛用的,检查自己在什么时候适合休眠。适合休眠的情况下返回true,不适合则返回false,执行parkAndCheckInterrupt()完成线程阻塞,并且返回线程的中断情况。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态。
//为什么是获取前驱节点的动态呢,如果前驱节点都没变动
//自己也不需要变动
//像是排队打饭,如果队伍动了一下,那就看看自己能不能打到饭,如果前一个人都没动,那就直接休息。
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 前驱节点被自己标记过了,说明前驱节点没有发生变动,可以进入休眠
*/
return true;
//判断前驱节点是否被取消了
if (ws > 0) {
/*
* 只有状态为CANCELLED,才会大于0
* 前驱节点(线程)被取消了,那么就需要跳过这个前驱节点,
* 使得节点的前驱为前驱的前驱,直到前驱的状态不为CANCELLED再把队列链起来
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 如果前驱节点没有被取消,尝试CAS前驱节点的信号标志为SIGNAL,表示当前节点标记过这个节点了。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//前驱节点没有标记过
//进入下一次自旋,如果下一次自旋还是没获得锁,
//上述标记前驱节点的CAS又成功了,再次进该方法,再判断ws == Node.SIGNAL
//则会返回true,线程进入休眠状态。
return false;
}
适合休眠的情况下返回true,再看下休眠的方法
private final boolean parkAndCheckInterrupt() {
//阻塞,等到调用,LockSupport.unpark(thread);才会从阻塞状态返回
LockSupport.park(this);
return Thread.interrupted();
}
如果不懂LockSupport.park(this);得可以先看个例子
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("线程执行");
LockSupport.park(this);
System.out.println("线程从park中唤醒");
}
};
t1.start();
new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"sleep(1000)");
LockSupport.unpark(t1);
System.out.println("线程执行, LockSupport.unpark(t1);");
}
}.start();
}
控制台输出
线程执行
Thread-1sleep(1000)
线程执行, LockSupport.unpark(t1);
线程从park中唤醒
在自旋中获取锁成功的情况下
- 将自己设置为头节点
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
- 返回线程的中断情况
在shouldParkAfterFailedAcquire返回true后,会进行线程休眠,被唤醒后会返回线程的在这个时间段的中断情况。如果线程被中断了随后调用selfInterrupt()标识该线程被中断了。
再看释放锁
public final boolean release(int arg) {
//尝试释放锁,一样的还是由子类完成的方法
if (tryRelease(arg)) {
//头节点是获取锁成功的节点
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒节点的后继者(如果存在)
unparkSuccessor(h);
return true;
}
return false;
}
使用LockSupport.unpark(s.thread)唤醒后继节点。
private void unparkSuccessor(Node node) {
//SIGNAL,CONDITION,PROPAGATE状态需要尝试扭转成0
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)
//线程非取消,不是CANCELLED状态
if (t.waitStatus <= 0)
s = t;
}
//后继不为Null,唤醒后继
if (s != null)
LockSupport.unpark(s.thread);
}
ReentrantLock如何使用AQS
是不是眼熟,还是这张图,分析完AQS接下来,需要看看内部类Sync是怎么继承AQS并且重写其中的部分方法的。
Sync继承结构
妥妥的,只要是实现同步都是继承AQS
Sync内部方法
会发现并没有实现AQS中要求重写的tryAcquire,只有其中的tryRelease释放锁方法。并且还是个抽象类接着往下看其子类
核心方法
- boolean nonfairTryAcquire(int acquires)
非公平锁的方式获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//AQS获取锁的状态
int c = getState();
//没有线程获取锁
if (c == 0) {
//以CAS的方式保证原子性,当同步状态时0时,设置锁被线程获取。
if (compareAndSetState(0, acquires)) {
//设置归属线程为自己
setExclusiveOwnerThread(current);
return true;
}
}
//有线程获取锁,需要判断一下这个线程是不是自身
else if (current == getExclusiveOwnerThread()) {
//可重入锁,当锁位自身获得时再次进入同步状态+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//为什么不需要采用CAS了,很简单线程已经持有了锁,只有可能我一个线程发生改动
setState(nextc);
return true;
}
return false;
}
- boolean tryRelease(int releases)
protected final boolean tryRelease(int releases) {
//准备同步状态减一
int c = getState() - releases;
//非拥有的锁的线程不可操作
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//同步状态-1后判断是否需要释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//更新同步状态
setState(c);
return free;
}
NonfairSync
非公平锁,代码比较短,直接上代码。
/**
1. 非公平锁
*/
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
- 先尝试快速的CAS改变同步状态,0未有线程获取锁,->1线程获取锁。CAS方法正是AQS提供修改同步状态的方法
- 成功:将锁的归属线程设置为自己(AQS的分类提供的方法)
- 失败:锁已经被其他线程获取了,调用acquire(1);方法获取锁(依旧是AQS的方法,上面分析过)
- 在AQS中acquire会调用由子类实现的tryAcquire()方法,对就是下面的tryAcquire()在这个方法中也只是调用SYNC的nonfairTryAcquire方法
FairSync
与上述非公平锁结构一致,细微的区别在于lock时并没有直接尝试CAS修改锁的状态,而是直接调用AQS的acquire(),tryAcquire也是自身实现,而不是父类Sync实现,可能与ReentrantLock默认是非公平锁有关。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//没有线程获得锁
if (c == 0) {
//查询是否有任何线程在等待获取比当前线程更长的时间。
//没有的情况下才会尝试CAS获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//判断是否是本线程获得锁,是的话同步状态+1,跟非公平锁一样
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
查询是否有其他线程在的时间比当前线程长,越早入队等待的时间越长。
只有该方法返回false,线程才会尝试CAS获取锁.
同步队列头指针与尾指针指向相同的节点
或者头指针指向的线程的后继不为Null,并且不与自身线程相等,证明没有其他等待更久的线程
才会返回false
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s = h.next;
return h != t &&
(( s == null || s.thread != Thread.currentThread());
}
将方法代理到Sync上
通过构造方法指定锁的类型,默认是非公平锁,参数传入true指定公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
lock方法,很简单直接代理到sync上去。
public void lock() {
sync.lock();
}
unlock方法,与lock一样直接代理到sync上去。
public void unlock() {
sync.release(1);
}
为何说是可以重入锁
首先明确锁的使用,调用了几次lock就得释放几次锁,并不是说加锁多次,最后释放一次锁就可以了。
首先看个例子
线程在调用Lock后,还能继续调用lock成功。如果只调用一次unlock();其他线程也不能获得锁。
public class LockTest {
private static Lock lock = new ReentrantLock();
public void incre(){
lock.lock();
System.out.println(Thread.currentThread().getName()+"第一次加锁");
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"第二次加锁");
}finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+"释放锁一次");
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
Thread t1 = new Thread() {
@Override
public void run() {
lockTest.incre();
}
};
t1.start();
new Thread() {
@Override
public void run() {
lockTest.incre();
}
}.start();
}
}
控制台输出:由于线程0,只释放了一次锁,还持有锁,所以线程1已经获取不到锁。
Thread-0第一次加锁
Thread-0第二次加锁
Thread-0释放锁一次
在看代码实现层面
线程调用一次lock(),如果拥有锁的线程与当前线程是同一个线程。就会将同步状态的值+1
//判断是否是本线程获得锁,是的话同步状态+1,跟非公平锁一样
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
线程在调用一次unlock,也只是将同步状态的值进行-1。调用两次lock,同步状态的值为2,调用一次unlock-1,同步状态的值不为0。不会放回true,也不会唤醒后继线程
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;
}
public final boolean release(int arg) {
//getState() - releases不等于0,不会唤醒后继线程
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在什么时候会尝试获取锁
非公平锁
第一次:线程调用Lock,直接先CAS(0, 1)尝试获取锁,
第二次:失败:会再判断一下当前同步状态是否为0,为0的话CAS尝试将同步状态更改为1
第三次:失败:放入同步队列尾巴后,开始循环判断前驱节点是否是头节点,是的话就会再次判断一下当前同步状态是否为0,为0的话CAS尝试将同步状态更改为1。否则线程会检查是否是合适的时机进入休眠状态(CAS标记前驱节点,下次循环后前驱节点还是被标记的则表示合适,前驱节点都没动,自己也不动),如果合适线程进入休眠状态。
公平锁
第一次:线程调用Lock,会再判断一下当前同步状态是否为0,为0的话,再判断当前线程是否是队列中等待时长最久的,是的话CAS尝试将同步状态更改为1
第三次:失败:放入同步队列尾巴后,开始循环判断前驱节点是否是头节点,是的话就会再次尝试第一步。否则线程会检查是否是合适的时机进入休眠状态(CAS标记前驱节点,下次循环后前驱节点还是被标记的则表示合适,前驱节点都没动,自己也不动),如果合适线程进入休眠状态。
公平锁与非公平锁的区别
从上图中可以看到非公平锁,多了两次直接CAS的机会(途中橙色部分)。那么就有可能在不需要进入队列时就获取到锁,反而哪些已经在队列中需要在更晚些获取到锁。
- 公平锁
多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁,就算在没进入队列时也要判断是否其他等待时间比自己长的线程,如果有就入队。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞。 - 非公平锁
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:整体的效率会提高,会减少唤起线程的数量。
缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
举个例子
食堂排队买饭
- 公平:
进来反正你就是要排队,要判断队伍中没人了才轮到你打饭。你插队阿姨就不给你饭 - 不公平
可以不用排队,先去窗口碰碰运气,说不定你就竞争过队伍第一个那个人(直接CAS竞争成功),阿姨直接先给你打饭。如果竞争失败再来排队