一、LockSupport工具类

JDK中的rt.jar包里的LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的。

主要方法:

  • LockSupport.park():如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被禁止参与线程调度,也就是会被阻塞挂起。在它线程调用LockSupport.unpark(Thread thread)并将当前线程作为参数时,调用park方法而被阻塞的线程会返回,另外,如果其他线程调用了阻塞先得interrupt()方法(不会抛出InterruptedException)或线程虚假唤醒,则阻塞也会返回。所以在调用park方法时最好使用循环条件判断方式。
  • LockSupport.unpark(Thread thread):(1)让thread持有与LockSupport关联的许可证(必做)。(2)如果其之前因调用park而被挂起,则将其唤醒。
  • LockSupport.parkNanos(long nanos) :与park类似,挂起nanos时间后自动返回
  • LockSupport.park(Object blocker) :与park类似。挂起后将blocker记录到线程内部(java.lang.Thread#parkBlocker)。使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过电泳getBlocker(Thread)方法来获取blocker对象的。所以JDK推荐我们使用带有blocker参数的park方法,将blocker设置为this,这样当在打印线程堆栈排查问题时就能知道是哪个类被阻塞了。
    public static void park(Object blocker) {Thread t = Thread.currentThread();// 设置blockersetBlocker(t, blocker);// 挂起线程UNSAFE.park(false, 0L);// 清除blockersetBlocker(t, null);}
  • LockSupport.parkNanos(Objcet blocker,long nanos):比LockSupport.park(Object blocker)多了超时时间,nanos是多少纳秒后
  • LockSupport.parkUtil(Object blocker,long deadline) :等同LockSupport.parkNanos,deadline是绝对时间,1970至今的毫秒值
    public static void parkUntil(Object blocker, long deadline) {Thread t = Thread.currentThread();setBlocker(t, blocker);// isAbsolute=true,time=deadline;表示到deadline时间后返回UNSAFE.park(true, deadline);setBlocker(t, null);}

例子:

import java.util.concurrent.locks.LockSupport;public class Main {public void testPark() {LockSupport.park();}public static void main(String[] args) {Main main = new Main();main.testPark();}}

使用top | grep java 找出自己运行的java进程

-bash-4.2$ top |grep java 42746 10045     20   0 2385956  20620  13352 S  0.3  2.1   0:00.30 java       

运行 jstack pid

-bash-4.2$ jstack 42746
......"main" #1 prio=5 os_prio=0 cpu=23.57ms elapsed=424.70s tid=0x00007f4e08027800 nid=0xa6fb waiting on condition  [0x00007f4e11765000]java.lang.Thread.State: WAITING (parking)at jdk.internal.misc.Unsafe.park(java.base@14.0.2/Native Method)at java.util.concurrent.locks.LockSupport.park(java.base@14.0.2/LockSupport.java:341)at Main.testPark(Main.java:6)at Main.main(Main.java:11)
......

修改park参数为this后,多了一条信息- parking to wait for  <0x00000000f0c922d8> (a Main)

-bash-4.2$ jstack 42940
......"main" #1 prio=5 os_prio=0 cpu=23.70ms elapsed=37.12s tid=0x00007f13b8027800 nid=0xa7bd waiting on condition  [0x00007f13be825000]java.lang.Thread.State: WAITING (parking)at jdk.internal.misc.Unsafe.park(java.base@14.0.2/Native Method)- parking to wait for  <0x00000000f0c922d8> (a Main)at java.util.concurrent.locks.LockSupport.park(java.base@14.0.2/LockSupport.java:211)at Main.testPark(Main.java:6)at Main.main(Main.java:11)
......

二、抽象同步队列AQS概述——AQS锁的底层支持

AQS(AbstractQueuedSynchronizer抽象同步队列):锁的底层支持,实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。(大多数开发者可能永远不会直接使用AQS)

AQS是一个FIFO(First Input First Output)双向队列。

  • AQS#head:尾节点(Node类型)
  • AQS#tail:头节点(Node类型)
  • AQS#status:状态,对于不同锁实现,该取值范围可能不同
  • Node#thread:存放进入AQS队列的线程
  • Node#SHARED:标记该线程是获取共享资源是被阻塞挂起后放入AQS的
  • Node#EXCLUSIVE:标记线程是获取独占资源时被挂起后放入AQS的
  • Node#waitStatus:当前线程状态CANCELLED(被取消了)、SIGNAL(需要被唤醒)、CONDITION(在条件队列里等待)、PROPAGATE(释放贡献资源时需要通知其他节点)
  • Node#prev:前驱节点
  • Node#next:后继节点
  • ConditionObject:条件变量,结合锁实现同步,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用await方法后被阻塞的线程

对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式共享方式

  1. 在独占方式下获取和释放资源使用的方法为:void acquire(int arg)void acquireInterruptibly(int arg)boolean release(int arg)
  2. 在共享方式下获取和释放资源的方法为:void acquireShared(int arg)void acquireSharedInterruptily(int arg)boolean releaseShared(int arg)。
  • 使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。
  • 对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时,如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。

独占方式下,获取与释放资源的流程如下:

(1)acquire(int arg):获取独占锁,先使用tryAcquire方法尝试获取资源。具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node#EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)挂起自己

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

(2)release(int arg):释放独占锁,尝试使用tryRelease操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thead)方法或AQS队列里被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

TIP:AQS类并没有提供可用的tryAcquire和tryRelease方法,这两个方法需要有具体的子类来实现。

在共享方式下,获取与释放资源的流程如下:

(1)acquireShared(int arg):获取共享锁,先使用tryAcquireShared尝试获取资源,具体是设置state,成功直接返回,失败则将当前线程封装为类型Node.SHARED的Node节点后插入到AQS阻塞队列尾部,并使用LockSoppurt.park(this)挂起自己

    public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);}

(2)releaseShared(int arg):释放共享锁,尝试使用tryReleaseShared操作释放资源,具体是设置state。然后使用LockSoppurt.unpark(thread)级或AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryReleaseShared查看当前state是否满足自己需要,满足则线程被激活,否则海华丝会被放入AQS队列并被挂起

    public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;}

TIP:AQS同样不提供可用的tryAcquireShared和tryReleaseShared。

  • 基于AQS实现的锁除了要重写tryAcquire和tryRelease系列方法之外还需要重写isHeldExclusively方法,以判断锁被当前线程独占还是被共享
  • 独占和共享方式下的获取锁都有带Interruptibly字样的方法(如acquireInterruptibly),该类方法会对线程中断做出响应,若是线程中断则抛出异常。不带Interruptibly字样的方法不对线程中断做出响应,只要唤醒了就尝试获取锁或被挂起。

入队操作:当一个线程获取锁失败后该线程转换为Node节点,然后就会使用enq(Node node)方法将该节点插入到AQS的阻塞队列

    private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

三、抽象同步队列AQS概述——AQS条件变量的支持

  • Object的notify和wait方法是配合synchronized内置锁实现线程间同步的基础设施;
  • 条件变量的signal和await方法也是用来配合AQS锁实现线程间同步的基础设施。

它们的不同之处在于synchronized同时只能与一个共享变量的notify或await方法实现同步,而AQS的一个锁可以对应多个条件变量。

条件变量的使用与synchronized类似:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class Main {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();Condition condition = lock.newCondition();Thread thread1 = new Thread(() -> {// await之前必须获取锁lock.lock();try {// await阻塞后会释放锁condition.await();// 唤醒后会争夺锁} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});Thread thread2 = new Thread(() -> {// signal之前必须获取锁lock.lock();try {condition.signal();} finally {lock.unlock();}});thread1.start();thread2.start();}}
  • await方法:调用时会构造一个类型为Node.CONDITION的节点插入条件队列末尾,之后释放锁,并阻塞挂起
public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// (1)创建新的node节点,并插入到条件队列末尾Node node = addConditionWaiter();// (2)释放当前线程获取的锁int savedState = fullyRelease(node);int interruptMode = 0;// (3)调用park方法阻塞挂起当前线程while (!isOnSyncQueue(node)) {LockSupport.park(this);if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}......
}
  • signal方法:移除条件队列头并放入AQS阻塞队列,然后激活这个线程
public final void signal() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)// 将条件队列头移动到AQS队列doSignal(first);
}
  • addConditionWaiter方法:将阻塞的线程封装到Node,放到条件队列
private Node addConditionWaiter() {Node t = lastWaiter;......Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;
}

TIP:当多个线程同时调用lock方法获取锁时,只有一个线程获取到了锁。其他线程会被转换为Node节点插入到锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。

读完的感觉就是AQS存放的是准备就绪的线程,条件队列存放的是阻塞挂起但还未准备就绪的线程

三、抽象同步队列AQS概述——基于AQS实现自定义同步器

我们可以基于AQS自己实现一个不可重入锁。这里我们定义state:0表示锁未被持有;1表示已被持有

1、代码实现

import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;public class NonReentrantLock implements Lock, Serializable {private static class Sync extends AbstractQueuedSynchronizer {@Overrideprotected boolean isHeldExclusively() {return getState() == 1;}@Overrideprotected boolean tryAcquire(int arg) {assert arg == 1;if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}@Overrideprotected boolean tryRelease(int arg) {assert arg == 1;if (getState() == 0) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(Thread.currentThread());setState(0);return true;}Condition newCondition() {return new ConditionObject();}}private final Sync sync = new Sync();@Overridepublic void lock() {sync.acquire(1);}@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}@Overridepublic void unlock() {sync.release(1);}@Overridepublic Condition newCondition() {return sync.newCondition();}@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(time));}
}

2、使用自定义锁实现生存—消费模型

import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;public class Main {final static NonReentrantLock lock = new NonReentrantLock();final static Condition notFull = lock.newCondition();final static Condition notEmpty = lock.newCondition();final static Queue<String> queue = new LinkedBlockingQueue<>();final static int queueSize = 10;public static void main(String[] args) {Thread producer = new Thread(() -> {lock.lock();try {while (queue.size() == queueSize) {notEmpty.await();}queue.add("ele");notFull.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});Thread consumer = new Thread(() -> {lock.lock();try {while (0 == queue.size()) {notFull.await();}String ele = queue.poll();notEmpty.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});producer.start();consumer.start();}
}

并发编程笔记——第六章 Java并发包中锁原理剖析相关推荐

  1. Java7并发编程指南——第六章:并发集合

    Java7并发编程指南--第六章:并发集合 @(并发和IO流) Java7并发编程指南第六章并发集合 思维导图 项目代码 思维导图 项目代码 GitHub:Java7ConcurrencyCookbo ...

  2. Java并发编程(五):Java线程安全性中的对象发布和逸出

    发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外 ...

  3. Github已星标180K又一神作,阿里巴巴内部并发编程笔记,难道Java真的凉了

    Java 线程 共享模型 原理篇 === 模式篇 === 应用篇 === 效率 使用多线程充分利用 CPU 限制 限制对CPU的使用 限制对共享资源的使用 单位时间内限流 互斥 悲观互斥 乐观重视 同 ...

  4. java并发编程笔记_java并发编程笔记(一)——并发编程简介

    java并发编程笔记(一)--简介 线程不安全的类示例 public class CountExample1 { // 请求总数 public static int clientTotal = 500 ...

  5. 《疯狂Java讲义》学习笔记 第六章 面向对象(下)

    <疯狂Java讲义>学习笔记 第六章 面向对象(下) 6.1包装类 基本数据类型 包装类 byte Byte short Short int Integer long Long char ...

  6. Java并发编程笔记之 CountDownLatch闭锁的源码分析

    转 自: Java并发编程笔记之 CountDownLatch闭锁的源码分析 ​ JUC 中倒数计数器 CountDownLatch 的使用与原理分析,当需要等待多个线程执行完毕后在做一件事情时候 C ...

  7. 《Go语言圣经》学习笔记 第六章 方法

    <Go语言圣经>学习笔记 第六章 方法 目录 方法声明 基于指针对象的方法 通过嵌入结构体来扩展类型 方法值和方法表达式 示例:Bit数组 封装 注:学习<Go语言圣经>笔记, ...

  8. 《JAVA并发编程的艺术》之Java内存模型

    <JAVA并发编程的艺术>之Java内存模型 文章目录 <JAVA并发编程的艺术>之Java内存模型 Java内存模型的基础 并发编程模型的两个关键问题 Java内存模型的抽象 ...

  9. Java 学习笔记:第一章 Java入门

    Java 学习笔记:第一章 Java入门 1.1 计算机语言发展史以及未来方向 1.2 常见编程语言介绍 C语言 C++ 语言 Java语言 PHP 语言 Object-C和Swift 语言 Java ...

最新文章

  1. 使用c语言标准库中的时间函数
  2. Entity 监听器
  3. c3p0数据源配置抛出Could not load driverClass com.mysql.jdbc.Driver的解决方案
  4. jQuery ajax contentType processData 笔记
  5. HTML--三种样式插入方法--链接---表格---列表
  6. vmware workstation 上创建的centos 7.2 ,新添加一块网卡。无法找到配置文件。
  7. 我的blog开张了,希望大家能多多赏光啊
  8. 一周以来的工作总结--oracle分区的迁移
  9. 设计模式之Builder模式 (C++实现)
  10. 面对一个全新的环境,作为一个Mysql DBA,首先应该了解什么?
  11. sklearn 中的 Iris 数据集
  12. 申报软件著作权时,怎样快捷计算源代码行数
  13. GANs是如何创造出高分辨率的图像的
  14. linux使用tar命令,Linux的tar命令使用简介
  15. 服务器pci数据捕获和信号处理控制器驱动,PCI数据捕获和信号处理控制器是哪个驱动...
  16. 面试方法-麦可利兰的能力素质模型
  17. C#获取SharePoint列表数据
  18. FPGA芯片供电总结
  19. Linux gtk 路由,在linux下使用gtk的gdk
  20. Java咖啡馆---第一杯咖啡

热门文章

  1. CTFHub-网站源码-wp
  2. 教你怎样混社会(转)
  3. 2020-09-22关于dialog 问题
  4. 情人节相关的公众号图文这样排版,看过的都说美!
  5. 【学习笔记】Java 开发手册(嵩山版)
  6. 51单片机电机测速程序c语言,单片机电机测速程序
  7. 计算机毕业设计JavaBS高校教师考勤系统(源码+系统+mysql数据库+lw文档)
  8. 华为防火墙(VRRP)
  9. 如何解决vmware虚拟机下ubuntu无法连接WiFi问题
  10. oracle中update,insert,delete的高级用法