目录
4. 锁的四种状态, synchronized中的锁如何变化
乐观锁 Optimistic Concurrency Control
AQS(AbstractQueuedSynchronizer.class)
1. 什么是锁
在并发环境下。多个线程会对同一个资源进行争抢,可能会导致数据不一致问题。因此可以使用锁机制,通过一种抽象的锁来对资源进行锁定。
2. java锁机制
java中,每个对象有一把锁,这把锁存放在对象头中。
java对象包含了三个部分:
对象头(存放对象运行时信息)、实例数据、对齐填充字节(为满足java对象的大小必须是8比特的倍数这一条件而设计的)
对象头中包含两部分:mark work(32bit)
3. synchronize原理
synchronized编译后生成monitor enter和monitor exit两个字节码指令进行线程同步。
使用javac和javap对java代码进行编译和反编译
这样就可以看到可读性较高的字节码,如下:
这里的monitor常被理解为监视器或者管程
synchronized可能存在性能问题,因为synchronized编译后生成monitor enter和monitor exit两个字节码指令,monitor依赖于操作系统的mutex lock来实现的。
java线程实际上是对操作系统线程的映射,所以每次挂起或者唤醒一个线程,都要切换操作系统的内核态,这种操作是比较重量级的,在一些情况下,甚至切换时间超出了任务的执行时间。
这样的话,使用 synchronized会对性能产生严重影响。
java6开始,synchronized 进行了优化,引入了偏向锁、轻量级锁。
因此,锁有四种状态(锁只能升级,不能降级):
无锁、偏向锁、轻量级锁、重量级锁
4. 锁的四种状态, synchronized中的锁如何变化
偏向锁
顾名思义,就是让对象认识线程,只为一个线程提供数据。
如果对象发现有多个线程在竞争数据,那么锁会升级为轻量级锁。
轻量级锁
一旦自旋等待的线程数超过一个,那么轻量级锁将会升级为重量级锁。
自旋锁(补充说明)
可以理解为一种轮询。线程在不断循环验证目标对象的锁是否被释放,如果释放则获取锁,否则则进行下一轮循环。
这种方式区别于被操作系统挂起阻塞,因为如果对象的锁很快就会被释放的话,自选就不需要进行系统中断和现场恢复,所以他的效率更高。
自旋相当于cpu空转,如果长时间自旋将会浪费cpu资源,于是出现了“适应性自旋”的优化,即自旋时间不再固定,而是由上一次在同一个锁上的自旋时间以及锁状态,这两个条件来决定自旋时间。
重量级锁
此时需要使用monitor来对资源进行控制。
5、无锁编程
用过AQS吗,有具体的例子可以说说吗?
了解CAS吗,谈一谈你对CAS的理解吧?
看过JUC的源码吗?聊一聊具体的实现吧?
假设现在有多个线程想要操作同一个资源对象,很多人的第一反应就是使用互斥锁
互斥锁
互斥锁的同步方式是悲观的,即操作系统认为,如果不严格控制线程调用,就一定会产生异常。因此互斥锁将会将资源锁定,只供一个线程调用。
但是互斥锁并不是万能的,比如一些情况下,大部分都是读操作,此时没有必要在每次读取时都锁定资源。
或者一些情况下,同步代码块执行的耗时远远小于线程切换的耗时,此时也不适合使用互斥锁。
CAS compare and swap
当资源可用时,有两个线程认为资源当前状态是可用时,他们各会产生两个值:
- old value代表之前读到的资源对象的状态值
- new value 代表想要资源对象的状态值
如下图所示,资源的状态为 0 ,此时两个线程A和B都希望把资源的状态改为1,然后占用该资源。
假设A线程率先获得了时间片,他将old value与资源的状态进行compare,发现一致,于是将资源的状态值设置为new value;
而B线程落后了一步,此时资源的状态值已被修改,此时B线程在compare的时候发现与自己预期的old value不一致,所以放弃swap操作。
在现实生活中,我们通常会让B线程进行自旋等待,自旋就是使其不断的重试CAS操作,通常会配置自旋次数来方式死循环。
疑问:
1、CAS分为compare和swap两步操作,多线程下难以保证一致性?
CAS必须是原子性的,即比较old value和更新new value这两步必须在同时只能由一条线程进行操作。
2、如何实现CAS的原子性?
各种不同架构的CPU都提供了指令级别的CAS原子操作
乐观锁 Optimistic Concurrency Control
通过CAS来实现同步的工具,由于不会锁定资源,并且总是乐观的认为资源没有被修改过,并且每次都会自己主动尝试去compare状态值,这种同步机制被称为乐观锁。
乐观锁使用了无锁的同步机制,实际上没有用到锁
java中如何利用CAS特性进行无锁编程
AtomicInteger类底层使用CAS实现同步计数器。
下面代码中使用AtomicInteger实现三个线程累加到1000,而不产生线程安全问题。
public class Main {
static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (num.get() < 1000) {
System.out.println("thread name:" +
Thread.currentThread().getName() + ":" +
num.incrementAndGet());
}
}
});
t.start();
}
}
}
我们进入源码来看下AtomicInteger 是如何实现无锁同步的。
/**
* Atomically increments the current value,
* with memory effects as specified by {@link VarHandle#getAndAdd}.
*
* <p>Equivalent to {@code addAndGet(1)}.
*
* @return the updated value
*/
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
/**
* Atomically adds the given value to the current value of a field
* or array element within the given object {@code o}
* at the given {@code offset}.
*
* @param o object/array to update the field/element in
* @param offset field/element offset
* @param delta the value to add
* @return the previous value
* @since 1.8
*/
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
/**
* Atomically updates Java variable to {@code x} if it is currently
* holding {@code expected}.
*
* <p>This operation has memory semantics of a {@code volatile} read
* and write. Corresponds to C11 atomic_compare_exchange_strong.
*
* @return {@code true} if successful
*/
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
CAS的具体实现是在Unsafe.java中。Unsafe.java主要用于执行一些底层的,与平台相关的方法。
上面代码中 compareAndSetInt 方法使用了native修饰符,说明这个方法是一个本地方法,与具体的平台实现相关。
自旋的次数可以通过启动参数进行配置,默认值为10。
AQS(AbstractQueuedSynchronizer.class)
多线程中竞争的资源以对象的形式进行封装,而CAS只能原始的修改内存上的一个值。该如何利用CAS去同步对象,就需要进一步的抽象。
JAVA是如何利用CAS进行对象同步的呢?
此时可以先思考一下:如何设计一个同步管理框架
1. 通用性,下层实现透明的同步机制,同时与上层业务解耦
2. 利用CAS,原子地修改共享标记位
3. 等待队列
AQS成员变量
这里的状态字段state没有设置成boolean类型,是因为线程或是锁的两种模式为:独占和共享
- 独占模式: 一旦被占用,其他线程都不能占用。
- 共享模式: 一旦被占用, 其他共享模式下的线程能占用。
所以state表示的是线程占用的数量,因此使用了int。
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
* 这里的状态没有设置成boolean,是因为线程或是锁的两种模式为:独占和共享
* 独占模式: 一旦被占用,其他线程都不能占用。
* 共享模式: 一旦被占用, 其他共享模式下的线程能占用。
* 所以state表示的是线程占用的数量,因此使用了int
*/
private volatile int state;
当有线程没有获取到资源时,有可能会选择排队,这里使用了链表对等待的线程进行排队,队列使用FIFO的思想。数据类型为Node。上面的head和tail属性表示队列的头和尾。
队列中的节点有两种模式:独占和共享
AQS独占模式
int state > 0 代表锁被占用,只能被单个节点占用
= 0 代表锁被释放
独占模式下,锁只能被一个线程获取,其他线程必须等待。
源码分析
对于Node对象,他主要存储了如下信息:
- 1. 线程对象
- 2. 节点在队列里的等待状态(waitStatus)
- 3. 前后指针(prev、next)等信息
源码如下所示:
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;
/**
* Link to predecessor node that current node/thread relies on
* for checking waitStatus. Assigned during enqueuing, and nulled
* out (for sake of GC) only upon dequeuing. Also, upon
* cancellation of a predecessor, we short-circuit while
* finding a non-cancelled one, which will always exist
* because the head node is never cancelled: A node becomes
* head only as a result of successful acquire. A
* cancelled thread never succeeds in acquiring, and a thread only
* cancels itself, not any other node.
*/
volatile Node prev;
/**
* Link to the successor node that the current node/thread
* unparks upon release. Assigned during enqueuing, adjusted
* when bypassing cancelled predecessors, and nulled out (for
* sake of GC) when dequeued. The enq operation does not
* assign next field of a predecessor until after attachment,
* so seeing a null next field does not necessarily mean that
* node is at end of queue. However, if a next field appears
* to be null, we can scan prev's from the tail to
* double-check. The next field of cancelled nodes is set to
* point to the node itself instead of null, to make life
* easier for isOnSyncQueue.
*/
volatile Node next;
/**
* The thread that enqueued this node. Initialized on
* construction and nulled out after use.
*/
volatile Thread thread;
/**
* Link to next node waiting on condition, or the special
* value SHARED. Because condition queues are accessed only
* when holding in exclusive mode, we just need a simple
* linked queue to hold nodes while they are waiting on
* conditions. They are then transferred to the queue to
* re-acquire. And because conditions can only be exclusive,
* we save a field by using special value to indicate shared
* mode.
*/
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() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/** Establishes initial head or SHARED marker. */
Node() {}
/** Constructor used by addWaiter. */
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
/** Constructor used by addConditionWaiter. */
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
/** CASes waitStatus field. */
final boolean compareAndSetWaitStatus(int expect, int update) {
return WAITSTATUS.compareAndSet(this, expect, update);
}
/** CASes next field. */
final boolean compareAndSetNext(Node expect, Node update) {
return NEXT.compareAndSet(this, expect, update);
}
final void setPrevRelaxed(Node p) {
PREV.set(this, p);
}
// VarHandle mechanics
private static final VarHandle NEXT;
private static final VarHandle PREV;
private static final VarHandle THREAD;
private static final VarHandle WAITSTATUS;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
NEXT = l.findVarHandle(Node.class, "next", Node.class);
PREV = l.findVarHandle(Node.class, "prev", Node.class);
THREAD = l.findVarHandle(Node.class, "thread", Thread.class);
WAITSTATUS = l.findVarHandle(Node.class, "waitStatus", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
}
线程在获取锁时可能会有两种行为
- 1. 尝试获取锁,然后立即返回结果——对应AQS中的tryAcquire方法
- 2. 获取锁,愿意进入队列等待,直到获取——对应AQS中的acquire方法
tryAcquire方法被protected修饰,参数是一个int值,代表了对status的修改,返回值是一个boolean类型,代表是否成功获得锁。
/**
* Attempts to acquire in exclusive mode. This method should query
* if the state of the object permits it to be acquired in the
* exclusive mode, and if so to acquire it.
*
* <p>This method is always invoked by the thread performing
* acquire. If this method reports failure, the acquire method
* may queue the thread, if it is not already queued, until it is
* signalled by a release from some other thread. This can be used
* to implement method {@link Lock#tryLock()}.
*
* <p>The default
* implementation throws {@link UnsupportedOperationException}.
*
* @param arg the acquire argument. This value is always the one
* passed to an acquire method, or is the value saved on entry
* to a condition wait. The value is otherwise uninterpreted
* and can represent anything you like.
* @return {@code true} if successful. Upon success, this object has
* been acquired.
* @throws IllegalMonitorStateException if acquiring would place this
* synchronizer in an illegal state. This exception must be
* thrown in a consistent fashion for synchronization to work
* correctly.
* @throws UnsupportedOperationException if exclusive mode is not supported
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
上层业务可以重写次方法,实现获取锁之后的相关业务:
如果选择等待锁,可以使用acquire方法,而不是自己实现复杂的排队逻辑。
如下所示,acquire的修饰符为public和final,意思是继承类可以直接调用我这个方法,而且不允许继承类擅自override,意思是这个方法一定能获取锁。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
代码中首先尝试获取锁,当获取不到锁时,会创建一个等待者,并将等待者加入到等待队列。
首先看下这个addWaiter方法,进入方法后,首先创建一个Node节点,然后判断下队尾是否为空,非空,将节点加到队尾的后面,并返回节点。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
然后看下acquireQueued方法,看下等待队列是如何被消费的:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
// 如果线程的前置节点为头节点,且尝试获取锁成功,则进行返回
// AQS 中,头节点为一个虚节点,即头节点并不是当前需要获取锁的节点
// 当第二个节点获取锁之后,他就会变成头节点,头节点就会出队
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
// 如果线程需要被挂起
// 这里没有选择自旋等待,而是判断是否挂起,可以防止性能问题
// 理想情况下,需要将那些没有资格获取锁的节点挂起,再在适合的时间进行唤醒
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) { // 出现异常,则取消节点的等待,并进行清理工作
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
我们看下如何判断线程是否该被挂起:
下面代码中的四种状态,就是AQS源码中一开始列举实体属性时列举的状态。
shouldParkAfterFailedAcquire方法根据线程的状态信息,来判断线程是否需要被挂起。
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
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.
*/
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt方法对线程执行了挂起操作
/**
* Convenience method to park and then check if interrupted.
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //挂起这个线程
return Thread.interrupted(); // 返回线程的中断标志位,并将其赋值为false
}
通过上述分析,AQS等待队列的执行效果如下:
如果当前线程所在的节点处于头节点的后面一个,那么他将会不断尝试获取锁
否则进行判断是否需要被挂起。
线程会被挂起的条件——线程的前驱节点不是头节点,并且waitStatus为SINGAL
这样可以保证head之后只会有一个节点在通过CAS获取锁,队列里其他线程都已被挂起或者正在被挂起,最大程度的避免无用的自旋消耗CPU
那么什么时候线程会被唤醒?
当线程使用完资源只会,会释放锁,并唤醒其他线程去获取锁。
这里使用到的方法是tryRelease和release方法。
从下方源码中可以看到:如果继承类上层业务没有去override这个tryRelease方法,则会直接抛出异常。
/**
* Attempts to set the state to reflect a release in exclusive
* mode.
*
* <p>This method is always invoked by the thread performing release.
*
* <p>The default implementation throws
* {@link UnsupportedOperationException}.
*
* @param arg the release argument. This value is always the one
* passed to a release method, or the current state value upon
* entry to a condition wait. The value is otherwise
* uninterpreted and can represent anything you like.
* @return {@code true} if this object is now in a fully released
* state, so that any waiting threads may attempt to acquire;
* and {@code false} otherwise.
* @throws IllegalMonitorStateException if releasing would place this
* synchronizer in an illegal state. This exception must be
* thrown in a consistent fashion for synchronization to work
* correctly.
* @throws UnsupportedOperationException if exclusive mode is not supported
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #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;
}
假如线程尝试释放锁成功,那么下一步就会唤醒其他线程,可以看下上面源码中的unparkSuccessor方法:
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
* 传入的参数为head,该方法用于唤醒head后面的node,使其自旋的获取锁
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0) {
// 将head的watiStates设置为0,才不会影响其他函数的判断
node.compareAndSetWaitStatus(ws, 0);
}
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
* 从尾节点开始向前搜索,找到最靠前的(head除外)并且waitStatus值 <= 0 的节点,
* 对其进行 LockSupport.unpark 操作,即唤醒该线程
* 线程一旦被唤醒,那么他将进行尝试获取锁,此时便形成了一个能够良好工作的闭环
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread);
}
共享模式
锁可以被多个线程获取,表现为state值的增加,用完需要一一释放。
int state > 0 代表锁被占用,可以被多个节点占用
= 0 代表锁被释放
补充
JAVA中断
在Java中的挂起和中断是两个不同维度的概念。
JAVA中的中断,作用与线程对象,他并不会使得线程被挂起,而是会根据线程当前的活动状态来产生不同的效果。
- 假设当前线程处于等待状态,那么对该线程进行interrupt,会使其抛出中断异常
- 当线程处于运行状态,那么对该线程进行interrupt,只会改变线程中断的状态值,并不会影响该线程继续运行。