ReentranLock和Synchronized
AQS
参考资料:https://blog.youkuaiyun.com/qq_35190492/article/details/104691668
( AbstractQueuedSynchronizer 抽象队列同步器 ) 是java juc并发工具类的基础,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)
1. AQS基本的数据结构是FIFO双向队列,如下图
从图中可以看出来,AbstractQueuedSynchronizer内部维护了一个Node节点类和一个ConditionObject内部类。Node内部类是一个双向的FIFO队列,用来保存阻塞中的线程以及获取同步状态的线程,而ConditionObject对应的是Lock中的等待和通知机制
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
static final class Node {
static final int CANCELLED = 1; //当前节点被取消。
static final int SIGNAL = -1; //表示当前节点的的后继节点将要或者已经被阻塞,在当前节点释放的时候需要unpark(唤醒)后继节点。
static final int CONDITION = -2; //表示当前节点在等待condition,即在condition队列中。
static final int PROPAGATE = -3; //表示releaseShared需要被传播给后续节点(仅在共享模式下使用)。
//等待状态(默认0:无状态,表示当前节点在队列中等待获取锁。)
volatile int waitStatus;
volatile Node prev;
volatile Node next;
//当前节点代表的线程
volatile Thread thread;
Node nextWaiter;
Node() {}
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
}
private transient volatile Node head;
private transient volatile Node tail;
//独占变量state(通过CAS和volatile保证了线程安全问题)
//state的值表示其状态:如果是0,那么当前还没有线程独占此变量;否在就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁。大于1表示锁重入
private volatile int state;
private void setHead(Node node) {
head = node;
node.thread = null; //防止内存泄漏
node.prev = null;
}
public final void acquire(int arg) {
//尝试获取锁(AQS在无竞争条件下,不会new出head和tail节点。),失败则自旋,阻塞当前线程,重新获取锁,直到获取锁成功
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //Node.EXCLUSIVE:标记指示节点在独占模式下等待
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//ReentrantLock实现的公平锁
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取AQS中的state变量
int c = getState();
//值为0,那么当前独占性变量还未被线程占有
if (c == 0) {
//自旋获取锁(在没有其他等待时间更长的线程时:!hasQueuedPredecessors())
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//将本线程设置为独占性变量所有者线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果该线程已经获取了锁,那么根据重入性原理,将state值进行加1,表示多次lock
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
//通过调用addWaiter函数,AQS将当前线程加入到了等待队列尾部,但是还没有阻塞当前线程的执行
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(); //如果没有头尾节点,建立空节点,并赋值给头尾节点
}
}
}
//获取锁失败,则插入队尾,并自旋,阻塞线程,重新获取锁
//队列中的线程获取锁的条件(公平锁):上一个节点是头节点,并且成功tryAcquire(arg)
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
//如果当前线程成功获取锁,则将当前线程设置为头节点,否则一直自旋
for (;;) {
//返回上一个节点,如果为空,则抛出nullpointerexception
final Node p = node.predecessor();
//如果上一个节点是头结点,并且当前线程获取到锁,将当前结点设置为头结点,并返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node)) //将当前线程节点的prev指向有效节点
interrupted |= parkAndCheckInterrupt(); //阻塞线程
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
//唤醒head节点的其他节点
unparkSuccessor(h);
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//ReentrantLock实现的公平锁
abstract static class Sync extends AbstractQueuedSynchronizer {
//释放独占性变量,就是将status的值减1(0:完全释放锁)
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;
}
}
//释放锁后需要唤醒其他线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
//判断head节点的next节点是否为空或者是否是取消状态:如果是,则找其他节点。
if (s == null || s.waitStatus > 0) {
s = null;
//从队列尾部向前遍历找到最前面的一个waitStatus<=0的节点
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
//唤醒找到的节点线程
LockSupport.unpark(s.thread);
}
}
2. AQS获取锁–aquire方法流程图
ReentrantLock基于AQS的实现,可重入锁
abstract static class Sync extends AbstractQueuedSynchronizer{}//抽象类
static final class NonfairSync extends Sync{}//非公平锁继承Sync类
static final class FairSync extends Sync{}//公平锁继承Sync类
图解如下,注:reentrantLock默认是非公平锁
NonFairSync非公平锁–默认是非公平锁
非公平锁定义:如果有任意新的线程妄图获得锁,都是有很大概率直接获得锁(等待队列中的只有头部有机会去竞争,其他都处于等待唤醒)
static final class NonfairSync extends Sync {
final void lock() {
//通过cas尝试获取锁,取锁成功则将
if (compareAndSetState(0, 1))
//设置当前线程为独占锁的拥有者
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//acquires 是1
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//获取state值
if (c == 0) {
//如果当前没有线程占用锁 通过CAS更新state标志位
//(这里也是非公平锁的一个原因 不判断前面是否有其它节点 直接CAS尝试拿锁)
if (compareAndSetState(0, acquires)) {
//将当前线程设置为独占锁的拥有者
setExclusiveOwnerThread(current);
return true;
}
}
//如果state不为0 并且当前线程就是独占锁的拥有者(表示重入)
else if (current == getExclusiveOwnerThread()) {
//累加state
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//新节点入队方法
private Node addWaiter(Node mode) {
Node node = new Node(mode);
for (;;) {//通过阻塞的方式将当前的node入队
Node oldTail = tail;//获取尾结点
if (oldTail != null) {
//将新节点插入到队列尾部
U.putObject(node, Node.PREV, oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
//队列为空 初始化队列
initializeSyncQueue();
}
}
}
}
FairSync公平锁
final void lock() {
acquire(1);
}
//acquire的实现与非公平锁相同,唯一区别是多了hasQueuedPredecessors()方法,即判断头结点后面一个结点是否为当前线程
//看一下公平锁的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;
}
//判断头结点后的节点的线程是否为当前线程,这是和非公平锁唯一的区别
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());
}
Synchronized----锁升级(非公平锁)
参考资料:https://www.cnblogs.com/myseries/p/12213997.html
锁实际上是加在对象上的,那么被加了锁的对象我们称之为锁对象,在java中,任何一个对象都能成为锁对象。
为了让大家更好着理解虚拟机是如何知道这个对象就是一个锁对象的,我们下面简单介绍一下java中一个对象的结构。
java对象在内存中的存储结构主要有一下三个部分:
- 对象头
- 实例数据
- 填充数据
这里强调一下,对象头里的数据主要是一些运行时的数据。
其简单的结构如下
LockObject lockObject = new LockObject();//随便创建一个对象
synchronized(lockObject){
//代码
}
锁升级过程
参考资料:https://blog.youkuaiyun.com/qq_35190492/article/details/104691668
膨胀过程:无锁(锁对象初始化时)-> 偏向锁(有线程请求锁) -> 轻量级锁(多线程轻度竞争)-> 重量级锁(线程过多或长耗时操作,线程自旋过度消耗cpu);
无锁
当创建一个对象LockObject时,该对象的部分Markword关键数据如下
从图中可以看出,锁的标志位是“01”即表明是偏向锁,状态是“0”,表示该对象还没有被加上偏向锁(“1”是表示被加上偏向锁)。 该对象被创建出来的那一刻,就有了偏向锁的标志位,这也说明了所有对象都是可偏向的,但所有对象的状态都为“0”,也同时说明所有被创建的对象的偏向锁并没有生效。
偏向锁
不过,当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操作,将线程ID插入到Markword中,同时修改偏向锁的标志位。
所谓临界区(critical section): 就是只允许一个线程进去执行操作的区域,即同步代码块。CAS是一个原子性操作
此时的Mark word的结构信息如下:
上图表明,已经有线程获取到该偏向锁,并将线程ID置入对象头部相应标志位中
偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏心的偏。它的意思就是说,这个锁会偏向于第一个获得它的线程,在接下来的执行过程中,假如该锁没有被其他线程所获取,没有其他线程来竞争该锁,那么持有偏向锁的线程将永远不需要进行同步操作。
也就是说:
在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:
- Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.
- 如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.
- 如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。
- 如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。
如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。
可以看出,偏向锁是针对于一个线程而言的,线程获得锁之后就不会再有解锁等操作了,这样可以省略很多开销。假如有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。
锁撤销
由于偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费还是挺大的,其大概的过程如下:
在一个安全点停止拥有锁的线程。
遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状态。
唤醒当前线程,将当前锁升级成轻量级锁。
所以,如果某些同步代码块大多数情况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭
轻量级锁
轻量级锁主要有两种
- 自旋锁
- 自适应自旋锁
自旋锁
所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。
自旋锁的一些问题
如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu,这会让人很难受。
本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。
解决方法:
基于这个问题,必须给线程空循环设置一个次数,当线程超过了这个次数,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改。
自适应自旋锁
所谓自适应自旋锁:线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
其大概原理是这样的:
假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。
另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。
轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待,串行执行。
重量级锁
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
缺点:线程拿不到锁,就会挂起,等待锁释放。这个过程存在线程上下文切换
CAS乐观锁
乐观锁和悲观锁是两种思想:
悲观锁的实现方式是加锁,如java中Lock和synchronized,数据库mysql的排它锁。
乐观锁的实现方式主要两种:cas机制和版本号机制。故乐观锁是不加锁的。
CAS缺点
- ABA问题
- 高竞争下CPU开销问题,比如拿不到锁一直cas导致cpu开销大
- 功能限制,如cas只能保证单个变量的原子性,这意味着:(1)原子性不一定保证线程安全,如java和volatile配合保证线程安全;(2)当涉及多个变量时cas无能为力