加锁目的:由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。

  加锁方式: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详解的更多相关文章

  1. (转)Java并发包基石-AQS详解

    背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...

  2. AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  3. Java并发之AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  4. 【1】AQS详解

    概述: 它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点加入到同步队列尾部(采用自旋CAS来 ...

  5. Java并发之AQS详解(转)

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  6. Java AQS详解(转)

    原文地址 一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同 ...

  7. java中synchronized的用法详解

    记下来,很重要. Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchron ...

  8. Java 中 synchronized的用法详解(四种用法)

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.本文给大家介绍java中 synchronized的用法,对本文感兴趣的朋友一起看看吧 ...

  9. Java 中 synchronized的用法详解

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 1.方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之 ...

随机推荐

  1. 第三章、HTTP报文

    1 报文流 HTTP 报文是在 HTTP 应用程序之间发送的数据块.这些数据块以一些文本形式的元信息(meta-information)开头.这些报文在客户端.服务器和代理之间流动.术语“流入”.“流 ...

  2. [题解] [BZOJ4152] The Captain

    题面 题解 将所有点根据

  3. tecplot 把散点绘成曲面图【转载】

    转载自:http://blog.sina.com.cn/s/blog_a319f5ff0101q6s8.html 找了好久,终于自己研究出来,如何使用tecplot绘制曲面图了 第一步:数据的整理 如 ...

  4. idhttp访问HTTPS

    idhttp访问HTTPS 访问一个 WEB 网站,如果采用 HTTP 的话,直接使用 TIdHTTP 这个控件,最简单的用法是: S := IdHTTP1.Get('www.qq.com'); 这里 ...

  5. Python3 编程之字符串处理

    Python3 编程之字符串处理 在编程中最常见的任务就是字符串的处理,So,学好字符串的使用非常重要 一.变量的定义规范 Python中声明变量时,要符合以下规则为准: 只能使用数字.字母.下划线组 ...

  6. [Java]借助PrintWriter类和StringWriter类,取出异常堆栈信息放入字符串中

    在程序开发中,有时我们不仅需要将异常堆栈信息打印在控制台里或是log里,可能还需要将它存在String中,再送到合适的地方,如错误页面,数据库等. 要取异常堆栈信息,具体的函数就是: /** * Ge ...

  7. python 获取昨天的日期

    from datetime import timedelta, datetime yesterday = datetime.today()+timedelta(-1) yesterday_format ...

  8. 数学建模python matlab 编程(喷泉模拟)

    在无风情况下的喷泉模拟 我的python代码 import numpy as np import random import matplotlib matplotlib.rcParams['font. ...

  9. 用alert打印js对象

    用alert查看对象: function writeObj(obj){ var description = ""; for(var i in obj){ var property= ...

  10. 分组卷积+squeezenet+mobilenet+shufflenet的参数及运算量计算

    来一发普通的二维卷积 1.输入feature map的格式为:m * m * h1 2.卷积核为 k * k 3.输出feature map的格式为: n * n * h2 参数量:k * k * h ...