手记

面试官:聊聊你知道的锁

开篇思考

  1. 你知道哪些锁?

  2. 锁解决了哪些应用场景的问题?

  3. 锁的底层实现?

  4. java 中的并发包了解吗?

  5. CAS 会有哪些问题?如何解决?

  6. AQS 是并发包的基础,实现原理是什么?

  7. synchronize 是可重入锁吗?

如果上面的思考题都能直接准确回答,直接去面试吧。

1. 悲观锁

并不是某一个锁,是一个锁类型,无论是否并发竞争资源,都会锁住资源,并等待资源释放下一个线程才能获取到锁。

这明显很悲观,所以就叫悲观锁。这明显可以归纳为一种策略,只要符合这种策略的锁的具体实现,都是悲观锁的范畴。

2. 乐观锁

与悲观锁相对的,也是一个锁类型。当线程开始竞争资源时,不是立马给资源上锁,而是进行一些前后值比对,以此来操作资源。

3. 无锁

显然,就是不给资源上锁,线程可以直接获取资源。

4. 自旋锁

自旋通俗的讲就是轮询,for(;;) 很好理解,就是一直循环等待资源释放后获取锁。

5. 偏向锁

初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是

“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,

线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,

这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

6. 轻量级锁

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:

如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,

发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,

即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。

先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,

然后线程将当前锁的持有者信息修改为自己。长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,

执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,

那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。

7. 重量级锁

如果锁竞争情况严重,某个达到最大自旋次数的线程(一般10次),会将轻量级锁升级为重量级锁

(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。

当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,

synchronized直接加重量级锁,很明显现在得到了很好的优化。

8. 可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,

递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。

9. 公平锁

多个线程竞争同一把锁,如果依照先来先得的原则,那么就是一把公平锁。

10. 非公平锁

多个线程竞争锁资源,如果是抢占式的,谁都可以先上,那么显然不公平,我先来的凭什么要插队,无耻。

11. 可中断锁

字面意思是“可以响应中断的锁”。这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,

只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),

但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,

Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。如果线程A持有锁,线程B等待获取该锁。由于线程A持有锁的时间过长,

线程B不想继续等待了,我们可以让线程B中断自己或者在别的线程里中断它,这种就是可中断锁。在Java中,synchronized就是不可中断锁,

而Lock的实现类都是可中断锁,可以简单看下Lock接口。


/* Lock接口 */

public interface Lock {

void lock(); // 拿不到锁就一直等,拿到马上返回。

void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,如果等待时收到中断请求,则需要处理InterruptedException。

boolean tryLock(); // 无论拿不拿得到锁,都马上返回。拿到返回true,拿不到返回false。

boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,可以自定义等待的时间。

void unlock();

Condition newCondition();

}

12. 读写锁,互斥锁,共享锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)


/** @see ReentrantReadWriteLock

* @see Lock

* @see ReentrantLock

*

* @since 1.5

* @author Doug Lea

*/

public interface ReadWriteLock {

/**

* Returns the lock used for reading.

*

* @return the lock used for reading

*/

Lock readLock();

/**

* Returns the lock used for writing.

*

* @return the lock used for writing

*/

Lock writeLock();

}

记得之前的乐观锁策略吗?所有线程随时都可以读,仅在写之前判断值有没有被更改。

读写锁其实做的事情是一样的,但是策略稍有不同。很多情况下,线程知道自己读取数据后,是否是为了更新它。

那么何不在加锁的时候直接明确这一点呢?如果我读取值是为了更新它(SQL的for update就是这个意思),

那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待;如果我读取数据仅为了前端展示,

那么加锁时就明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取(读锁计数器+1)。

虽然读写锁感觉与乐观锁有点像,但是读写锁是悲观锁策略。因为读写锁并没有在更新前判断值有没有被修改过,

而是在加锁前决定应该用读锁还是写锁。乐观锁特指无锁编程。

JDK提供的唯一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不仅提供了读写锁,

而是都是可重入锁。 除了两个接口方法以外,ReentrantReadWriteLock还提供了一些便于外界监控其内部工作状态的方法,

这里就不一一展开。

java 中的悲观锁、乐观锁

我们在Java里使用的各种锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。

JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就一定是悲观锁。因为乐观锁不是锁,

而是一个在循环里尝试CAS的算法。

那JDK并发包里到底有没有乐观锁呢?很多。

java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。

有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。


/**

* Atomically sets the value to the given updated value

* if the current value {@code ==} the expected value.

*

    * @param expectthe expected value

    * @param updatethe new value

* @return {@code true} if successful. False return indicates that

* the actual value was not equal to the expected value.

*/

public final boolean compareAndSet(int expect, int update) {

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

}

ABA问题及解决方案

上面讲到 CAS 是通过比较替换的方式来做实际的更新。

这里存在一个问题,就是一个值从A变为B,又从B变回了A。这种情况下,CAS可能会认为值没有发生过变化,但实际上是有变化的。

对此,并发包下有AtomicStampedReference提供根据版本号判断的实现。

基本思路就是通过版本号来控制,这个也是乐观锁的常用解决方案。

数据库同样可以通过这种版本号的控制方式来实现乐观锁。

AbstractQueuedSynchronizer (AQS )抽象的队列式同步器,并发包的基础

简单解释一下J.U.C,是JDK中提供的并发工具包,java.util.concurrent。

里面提供了很多并发编程中很常用的实用工具类,比如atomic原子操作、比如lock同步锁、fork/join等。

AQS 的核心功能,就是用来将当前工作的线程设置为占有资源状态,并将资源状态设置为锁定,如果其他线程继续访问共享资源,

则需要使用队列将其他线程进行管理,这个队列并不是实例化的某种队列,只是一个 Node 节点的双向关联。

下面只列出一些 AQS 核心的东西,后面有机会详细解读 AQS

  1. FIFO 先入先出队列

  2. 状态控制(volatile修饰共享变量state)

  3. lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中

  4. lock释放锁的过程:修改状态值,调整等待链表。

下面这段代码就是 AQS 静态内部类,Node:


static final class Node {

/** Marker to indicate a node is waiting in shared mode */

static final Node SHARED = new Node();

/** Marker to indicate a node is waiting in exclusive mode */

static final Node EXCLUSIVE = null;

/** waitStatus value to indicate thread has cancelled */

static final int CANCELLED =  1;

/** waitStatus value to indicate successor's thread needs unparking */

static final int SIGNAL    = -1;

/** waitStatus value to indicate thread is waiting on condition */

static final int CONDITION = -2;

/**

* waitStatus value to indicate the next acquireShared should

* unconditionally propagate

*/

static final int PROPAGATE = -3;

/**

* Status field, taking on only the values:

*  SIGNAL:    The successor of this node is (or will soon be)

*              blocked (via park), so the current node must

*              unpark its successor when it releases or

*              cancels. To avoid races, acquire methods must

*              first indicate they need a signal,

*              then retry the atomic acquire, and then,

*              on failure, block.

*  CANCELLED:  This node is cancelled due to timeout or interrupt.

*              Nodes never leave this state. In particular,

*              a thread with cancelled node never again blocks.

*  CONDITION:  This node is currently on a condition queue.

*              It will not be used as a sync queue node

*              until transferred, at which time the status

*              will be set to 0. (Use of this value here has

*              nothing to do with the other uses of the

*              field, but simplifies mechanics.)

*  PROPAGATE:  A releaseShared should be propagated to other

*              nodes. This is set (for head node only) in

*              doReleaseShared to ensure propagation

*              continues, even if other operations have

*              since intervened.

*  0:          None of the above

*

* The values are arranged numerically to simplify use.

* Non-negative values mean that a node doesn't need to

* signal. So, most code doesn't need to check for particular

* values, just for sign.

*

* The field is initialized to 0 for normal sync nodes, and

* CONDITION for condition nodes.  It is modified using CAS

* (or when possible, unconditional volatile writes).

*/

volatile int waitStatus;

volatile Node prev;

volatile Node next;

/**

* The thread that enqueued this node.  Initialized on

* construction and nulled out after use.

*/

volatile Thread thread;

Node nextWaiter;

/**

* Returns true if node is waiting in shared mode.

*/

final boolean isShared() {

return nextWaiter == SHARED;

}

/**

* Returns previous node, or throws NullPointerException if null.

* Use when predecessor cannot be null.  The null check could

* be elided, but is present to help the VM.

*

* @return the predecessor of this node

*/

final Node predecessor() throws NullPointerException {

Node p = prev;

if (p == null)

throw new NullPointerException();

else

return p;

}

Node() {    // Used to establish initial head or SHARED marker

}

Node(Thread thread, Node mode) {    // Used by addWaiter

this.nextWaiter = mode;

this.thread = thread;

}

Node(Thread thread, int waitStatus) { // Used by Condition

this.waitStatus = waitStatus;

this.thread = thread;

}

}

重点看下这个volatile int waitStatus;,这是一个volatile 修饰的int 状态类型,volatile 就是确保该变量是对其他线程可见的,

是java 内存模型中的重要概念,不理解的可以去看下 JMM 。

SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

0:初始状态

synchronize 是不是可重入锁

这个需要先理解可重入锁是怎么设计的。

重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,

那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;

此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;

当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

程序领域(id:think-holy)
作者:holy 程序汪一只

9人推荐
随时随地看视频
慕课网APP

热门评论

收藏了,很不错的文章

不错的文章,锁一直是难题

查看全部评论