锁的底层实现?java中的并发包了解吗?

开放的想法

你知道哪些锁?锁解决了哪些应用场景?锁的底层实现?你了解java中的并发包吗?CAS会遇到什么问题?怎么解决?AQS 是并发数据包的基础。实施原则是什么?同步是可重入锁吗?

如果以上问题都能直接准确的回答,直接去面试。

lock1. 悲观锁

它不是锁,而是一种锁。无论是否存在并发竞争资源,该资源都会被锁定,下一个线程会等待资源被释放来获取锁。

这显然是非常悲观的,所以称为悲观锁。这显然可以概括为一种策略,只要符合这种策略的锁的具体实现是悲观锁的范畴。

2. 乐观锁

与悲观锁相反,也是一种锁类型。当线程开始竞争资源时,它们并不会立即锁定资源,而是进行一些前后值的比较来操作资源。

3. 没有锁

显然,如果资源没有被锁定,线程可以直接获取资源。

4. 自旋锁

通俗地说,自旋就是轮询。for(;;) 很好理解,就是不断循环,等待资源释放来获取锁。

5. 偏置锁定

当第一次执行同步代码块时,锁对象变成了偏向锁(通过CAS修改对象头中的锁标志),字面意思是

“有利于第一个线程获取它”的锁。执行完同步代码块后,线程并没有主动释放偏置锁。当第二次到达同步代码块时,

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

此处无需重新锁定。如果一直只有一个线程在使用锁,显然偏向锁几乎没有额外的开销,性能极高。

6. 轻量级锁

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

如果多个线程轮流获取锁,但每次都顺利获取锁,没有阻塞,则不存在锁竞争。只有当一个线程试图获取锁时,

发现锁已经被占用,只能等待释放,然后发生锁竞争。继续轻量级锁状态下的锁竞争,没有抢到锁的线程会自旋,

即不断循环判断是否可以成功获取锁。获取锁的操作其实就是通过CAS修改对象头中的锁标志。

首先比较当前锁标志是否“释放”,如果是,则设置为“锁定”同步代码块的锁是什么,比较和设置是原子的。即使你抓住了锁,

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

不能执行任何有效的任务,这种现象称为忙等待(busy-waiting)。如果多个线程使用一个锁,但是没有发生锁争用,或者发生了非常轻微的锁争用,

然后 synchronized 使用轻量级锁来允许短期的忙碌。这是一个折衷的想法,短时间内忙于等待在用户态和内核态之间切换线程的开销。

7. 重量级锁

如果锁竞争严重,达到最大自旋次数(通常为10次)的线程会将轻量级锁升级为重量级锁

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

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

同步直接加了重量级锁,现在明显优化好了。

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 更新的意思),

然后加锁的时候直接加写锁。当我持有写锁时,其他线程需要等待读写;如果我只为前端显示读取数据,

然后在加锁的时候显式加读锁。如果其他线程也需要添加读锁,可以直接获取,无需等待(读锁计数器+1).

虽然读写锁感觉有点像乐观锁,但读写锁是一种悲观锁策略。因为读写锁不判断更新前值是否被修改过,

相反,在锁定之前决定是使用读锁还是写锁。乐观锁定特指无锁编程。

JDK 提供的 ReadWriteLock 接口的唯一实现是 ReentrantReadWriteLock。从名字可以看出,它不仅提供了读写锁,

相反,它们都是可重入锁。除了这两个接口方法之外,ReentrantReadWriteLock 还提供了一些方法供外界监控其内部工作状态,

此处不再展开。

java中的悲观锁和乐观锁

我们在 Java 中使用的几乎所有锁都是悲观锁。同步是从偏向锁、轻量级锁到重量级锁的悲观锁。

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 可能认为该值没有改变,但实际上它确实发生了变化。

对此,concurrent包下有AtomicStampedReference,提供了基于版本号判断的实现。

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

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

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

简单解释一下JUC,它是JDK中提供的一个并发工具包,java.util.concurrent。

它提供了许多并发编程中常用的实用程序类,例如原子原子操作,例如锁同步锁、fork/join等。

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

您需要使用队列来管理其他线程。这个队列不是实例化的队列,而是Node节点的双向关联。

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

FIFO先进先出队列状态控制(volatile modified shared variable state) 锁获取锁的过程:本质上是通过CAS获取状态值修改。如果没有当场获得,线程将被放入线程等待列表中释放锁。流程:修改状态值,调整等待列表。

以下代码为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):表示当前节点已经被取消。当超时或中断时(在响应中断的情况下),会被触发变为该状态,进入后节点不会改变这种状态。

CONDITION(-2):表示节点正在等待Condition。当其他线程调用Condition的signal()方法时,处于CONDITION状态的节点会从等待队列转移到同步队列,等待获取同步锁。

PROPAGATE(-3):在共享模式下,前驱节点不仅会唤醒其后继节点,还可能唤醒后继节点。

0:初始状态

同步是可重入锁吗?

这需要了解如何设计可重入锁。

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

那么任何线程都可以获得锁并调用相应的方法;当线程请求成功时,JVM会记录持有锁的线程并将计数器设置为1;

这时,当其他线程请求锁时,它们必须等待;如果持有锁的线程再次请求锁,则可以再次获得锁,计数器会递增;

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

图片[1]-锁的底层实现?java中的并发包了解吗?-老王博客

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论