Synchronized&Lock&AQS详解
加锁目的:由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。
加锁方式:java锁分为两种--显示锁和隐示锁,本质区别在于显示锁需要的是程序员自己手动的进行加锁与解锁如ReentrantLock需要进行lock与unlock。而隐式锁则是Synchronized,jvm内置锁,jvm进行操作加锁与解锁。
Synchronized关键字
每个对象创建后都会存在一个Monitor(监视器锁),它的实现依赖底层的系统的Mutex Lock(互斥锁)实现,是重量级锁,但是在java1.6版本之后,jvm内置锁进行了一系列的优化,如:锁粗化、锁消除、偏向锁、轻量级锁、重量级锁等。
Synchronized锁编译成字节码后,会发现jvm底层使用了monitorenter与monitorexit来进行加锁与解锁

我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?
答案是锁状态是被记录在每个对象的对象头(Mark Word)中,下面我们一起认识一下对象的内存布局
对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等
- 实例数据:即创建对象时,对象中成员变量,方法等
- 对齐填充:对象的大小必须是8字节的整数倍

AQS具备特性
- 阻塞等待队列
- 公平/非公平
- 可重入
- 共享/独占
- 允许中断
例如Java.concurrent.util当中同步器的实现如Lock,Latch,Barrier等,都是基于AQS框架实现,一般通过定义内部类Sync继承AQS,将同步器所有调用都映射到Sync对应的方法
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
该NonfairSync为ReentranLock内部类,默认非公平锁,非公平锁与公平锁的区别在于,当正在争抢的锁释放时,谁会抢到锁。我们再看一下 公平锁的lock()方法,就明白了。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
}
前面看到非公平锁首先去尝试设置state状态是否可以成功,这里的state为Node节点的属性,默认为0就是没有人抢到锁的情况,加一次锁就会state进行CAS操作+1,因为它是可重入的锁没所以每加一次都会state+1,没释放一次都会-1,直到state为0时,才会轮到下一个线程进行抢锁。我们再看一下acquire(1)方法,就明白了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我来讲接一下:tryAcquire(arg)是尝试获取锁,并将state状态由0变为+1,如果失败说明锁已经被别人抢走了,需要下一步操作就是addWaiter
addWaiter()方法就是创建一个双向链表的结构的队列,看到它是一种Node.EXCLUSIVE模式创建的,为独占模式
acquireQueued()方法让第一个节点去争抢锁,如果失败则会将该线程直接中断阻塞,如果当前节点的前一个节点为头结点也就是第一个节点抢到锁了将会把当前节点作为新的头结点并返回值
tryAcquire(arg)公平锁与非公平锁实现不一样,公平锁多判断了一步就是如果我的同步队列也就是等待队列里有其他等待的节点,将不会让当前的新线程去获取锁。 释放锁的时候是一样的逻辑
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
也是会改变state值的状态,当变为0时,Node还有一个属性就是exclusiveOwnerThread,它指向的是正在使用锁的线程,state释放为初始状态的时候,exclusiveOwnerThread将会置位null;去唤醒头结点。取消阻塞公平与非公平就在于去抢锁的时候判断是不一样的。 BlockingQueue实现原理
我们以ArrayBlockingQueue为例讲解为什么AQS需要使用同步队列与条件队列两个队列;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
源码中我们创建阻塞队列需要创建容量初始大小以及默认非公平锁,底层看到使用的是独占锁ReentrantLock以及两个条件队列Condition;
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
当我们往队列中添加元数时,则会将元素放入到数组中,并且唤醒阻塞线程;如果数组填满的时候,则会将当前阻塞,我们看看await方法;
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
我来讲解下上面的方法:
addConditionWaiter:将当前条件队列从后遍历进行删除已经没有用的节点,并且将当前线程添加到条件队列当中;返回当前节点。
fullyRelease:将当前线程的锁全部释放掉,并且当前的独占线程置位null后,唤醒队列的头节点,但是目前我们的队列还没有任何阻塞节点,所以只是释放了锁。
isOnSyncQueue:查看是否当前节点已经在同步队列中。
checkInterruptWhileWaiting:查看点前节点是否被中断,如果没有则将当前节点添加到同步队列当中。
acquireQueued:同步队列中头节点重新获取锁并返回。
unlinkCancelledWaiters:删除条件队列中被中断的节点。
reportInterruptAfterWait:中断当前线程。
我们不难发现,当前节点没有添加成功会先添加到条件队列中,然后释放持有的独占锁,并且判断是否已经加入到了同步队列中去,没有的话线程阻塞到这里。一旦被释放就会
立即添加到同步队列中。然后做一些后续处理。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
这里的await跟上面的步骤是一样的,只不过他会释放上一个阻塞的线程,让它添加到同步队列中。

ps:关注一下本人公众号,每周都有新更新哦!
Synchronized&Lock&AQS详解的更多相关文章
- (转)Java并发包基石-AQS详解
背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...
- AQS详解
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- Java并发之AQS详解
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- 【1】AQS详解
概述: 它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点加入到同步队列尾部(采用自旋CAS来 ...
- Java并发之AQS详解(转)
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- Java AQS详解(转)
原文地址 一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同 ...
- java中synchronized的用法详解
记下来,很重要. Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchron ...
- Java 中 synchronized的用法详解(四种用法)
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.本文给大家介绍java中 synchronized的用法,对本文感兴趣的朋友一起看看吧 ...
- Java 中 synchronized的用法详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 1.方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之 ...
随机推荐
- P2051 [AHOI2009]中国象棋——DP(我是谁,我在哪,为什么)
象棋,给你棋盘大小,然后放炮(炮的数量不限),不能让炮打到其他的炮,问方案数: 数据n,m<=200; 状态压缩似乎能做,但是我不会: 因为只要状态数,所以不必纠结每种状态的具体情况: 可以想出 ...
- pandas入门之DataFrame
创建DataFrame - DataFrame是一个[表格型]的数据结构.DataFrame由按一定顺序排列的多列数据组成.设计初衷是将Series的使用场景从一维拓展到多维.DataFrame既有行 ...
- c++ 生成容器元素生成随机数
// random_shuffle example #include <iostream> // cout #include <algorithm> // random_shu ...
- smarty {for}{forelse}
{for} {for}{forelse}用于创建一个简单的循环. 下面的几种方式都是支持的: {for $var=$start to $end}步长1的简单循环. {for $var=$start t ...
- 依靠MySQL(frm、MYD、MYI)数据文件恢复
该方法并不是适用于所有MySQL数据库 此次恢复是朋友那边的数据查不了了,请求我支援,出于各种心理原因,我答应试试,于是就有了这篇文章和这次经历,废话不多说.... 物理条件:宿机是Ubuntu16. ...
- 阶段5 3.微服务项目【学成在线】_day04 页面静态化_02-freemarker介绍
- PAT 甲级1025 PAT Ranking (25 分)(结构体排序,第一次超时了,一次sort即可小技巧优化)
题意: 给定一次PAT测试的成绩,要求输出考生的编号,总排名,考场编号以及考场排名. 分析: 题意很简单嘛,一开始上来就,一组组输入,一组组排序并记录组内排名,然后再来个总排序并算总排名,结果发现最后 ...
- HBase管理与监控——统计表行数
背景 HBase统计 RowCount 的方法有好几种,并且执行效率差别巨大,以下3种方法效率依次提高. 一.hbase-shell的count命令 这是最简单直接的操作,但是执行效率非常低,适用 ...
- 给mysql创建用户
给mysql 创建用户过程: 1.进入mysql cmd下 2.输入密码:123 3.选择使用的数据库:use myeshop 4.创建新用户grant usage on *.* to 'cctvse ...
- Vue 组件基础完整示例2
简介此页面可以直接复制运行,包含以下应用: Vue slot插槽使用Vue v-model使用Vue props使用父子组件数据传递element-ui使用HTML方式注册子组件,可以将子组件数据写在 ...