诚之和:如何理解Java并发之同步器设计

这篇文章主要讲解了“如何理解Java并发之同步器设计”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何理解Java并发之同步器设计”吧!

前言:

在 Java并发内存模型详情了解到多进程(线程)读取共享资源的时候存在竞争条件。

计算机中通过设计同步器来协调进程(线程)之间执行顺序。同步器作用就像登机安检人员一样可以协调旅客按顺序通过。

Java中,同步器可以理解为一个对象,它根据自身状态协调线程的执行顺序。比如锁(Lock),信号量(Semaphore),屏障(CyclicBarrier),阻塞队列(Blocking Queue)。

这些同步器在功能设计上有所不同,但是内部实现上有共通的地方。

1、同步器

同步器的设计一般包含几个方面:状态变量设计(同步器内部状态),访问条件设定,状态更新,等待方式,通知策略。

访问条件是控制线程是否能执行(访问共享对象)的条件,它往往与状态变量紧密相关。而通知策略是线程释放锁定状态后通知其它等待线程的方式,一般有以下几种情况:

  • 通知所有等待的线程。

  • 通知1个随机的N个等待线程。

  • 通知1个特定的N个等待线程

看下面例子,通过锁方式的同步器

public class Lock{
  // 状态变量 isLocked  private boolean isLocked = false;
  public synchronized void lock() throws InterruptedException{
    // 访问条件 当isLocked=false 时获得访问权限否则等待    while(isLocked){
      // 阻塞等待      wait();
    }
    //状态更新 线程获得访问权限    isLocked = true;
  }

  public synchronized void unlock(){
    //状态更新 线程释放访问权限    isLocked = false;
    // 通知策略 object.notify | object.notifyAll    notify();
  }
}

我们用计数信号量控制同时执行操作活动数。这里模拟一个连接池。

public class PoolSemaphore {
   // 状态变量 actives 计数器    private int actives = 0;
    private int max;
    public PoolSemaphore(int max) {
        this.max = max;
    }
    public synchronized void acquire() throws InterruptedException {
        //访问条件 激活数小于最大限制时,获得访问权限否则等待        while (this.actives == max) wait();
        //状态更新 线程获得访问权限        this.actives++;
        // 通知策略 object.notify | object.notifyAll        this.notify();
    }
    public synchronized void release() throws InterruptedException {
        //访问条件 激活数不为0时,获得访问权限否则等待        while (this.actives == 0) wait();
         //状态更新 线程获得访问权限        this.actives--;
        // 通知策略 object.notify | object.notifyAll        this.notify();
    }
}

1.1 原子指令

同步器设计里面,最重要的操作逻辑是“如果满足条件,以更新状态变量来标志线程获得或释放访问权限”,该操作应具备原子性

比如test-and-set 计算机原子指令,意思是进行条件判断满足则设置新值。

function Lock(boolean *lock) {
    while (test_and_set(lock) == 1);
}

另外还有很多原子指令 fetch-and-add compare-and-swap,注意这些指令需硬件支持才有效。

同步操作中,利用计算机原子指令,可以避开锁,提升效率。java中没有 test-and-set 的支持,不过 java.util.concurrent.atomic 给我们提供了很多原子类API,里面支持了 getAndSetcompareAndSet 操作。

看下面例子,主要在区别是等待方式不一样,上面是通过wait()阻塞等待,下面是无阻塞循环。

public class Lock{  // 状态变量 isLocked  private AtomicBoolean isLocked = new AtomicBoolean(false);
  public void lock() throws InterruptedException{
    // 等待方式 变为自旋等待    while(!isLocked.compareAndSet(false, true));
    //状态更新 线程获得访问权限    isLocked.set(true);
  }

  public synchronized void unlock(){
    //状态更新 线程释放访问权限    isLocked.set(false);
  }
}

1.2 关于阻塞扩展说明

阻塞意味着需要将进程或线程状态进行转存,以便还原后恢复执行。这种操作是昂贵繁重,而线程基于进程之上相对比较轻量。线程的阻塞在不同编程平台实现方式也有所不同,像Java是基于JVM运行,所以它由JVM完成实现。

在《Java Concurrency in Practice》中,作者提到

竞争性同步可能需要OS活动,这增加了成本。当争用锁时,未获取锁的线程必须阻塞。 JVM可以通过旋转等待(反复尝试获取锁直到成功)来实现阻塞,也可以通过操作系统挂起阻塞的线程来实现阻塞。哪种效率更高取决于上下文切换开销与锁定可用之前的时间之间的关系。对于短暂的等待,最好使用自旋等待;对于长时间的等待,最好使用暂停。一些JVM基于对过去等待时间的分析数据来自适应地在这两者之间进行选择,但是大多数JVM只是挂起线程等待锁定。

从上面可以看出JVM实现阻塞两种方式

  • 旋转等待(spin-waiting),简单理解是不暂停执行以循环的方式等待,适合短时间场景。

  • 通过操作系统挂起线程。

JVM中通过 -XX: +UseSpinning 开启旋转等待, -XX: PreBlockSpi =10指定最大旋转次数。

2、AQS

AQSAbstractQueuedSynchronizer简称。本节对AQS只做简单阐述,并不全面。

java.util.concurrent包中的 ReentrantLockCountDownLatchSemaphoreCyclicBarrier等都是基于是AQS同步器实现。

状态变量 是用 int state 来表示,状态的获取与更新通过以下API操作。

int getState()void setState(int newState)boolean compareAndSetState(int expect, int update)

该状态值在不同API中有不同表示意义。比如ReentrantLock中表示持有锁的线程获取锁的次数,Semaphore表示剩余许可数。

关于等待方式和通知策略的设计

AQS通过维护一个FIFO同步队列(Sync queue)来进行同步管理。当多线程争用共享资源时被阻塞入队。而线程阻塞与唤醒是通过 LockSupport.park/unpark API实现。

它定义了两种资源共享方式。

  • Exclusive(独占,只有一个线程能执行,如ReentrantLock

  • Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch

每个节点包含waitStatus(节点状态),prev(前继),next(后继),thread(入队时线程),nextWaitercondition队列的后继节点)

waitStatus 有以下取值

  • CANCELLED(1) 表示线程已取消。当发生超时或中断,节点状态变为取消,之后状态不再改变。

  • SIGNAL(-1) 表示后继节点等待前继的唤醒。后继节点入队时,会将前继状态更新为SIGNAL。

  • CONDITION(-2) 表示线程在Condition queue 里面等待。当其他线程调用了Condition.signal()方法后,CONDITION状态的节点将从 Condition queue 转移到 Sync queue,等待获取锁。

  • PROPAGATE(-3) 在共享模式下,当前节点释放后,确保有效通知后继节点。

  • (0) 节点加入队列时的默认状态。

AQS 几个关键 API

  • tryAcquire(int) 独占方式下,尝试去获取资源。成功返回true,否则false

  • tryRelease(int) 独占方式下,尝试释放资源,成功返回true,否则false

  • tryAcquireShared(int) 共享方式下,尝试获取资源。返回负数为失败,零和正数为成功并表示剩余资源。

  • tryReleaseShared(int) 共享方式下,尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则false

  • isHeldExclusively() 判断线程是否正在独占资源。

2.1 acquire(int arg)

public final void acquire(int arg) {
    if (
      // 尝试直接去获取资源,如果成功则直接返回      !tryAcquire(arg)
        &&
        //线程阻塞在同步队列等待获取资源。等待过程中被中断,则返回true,否则false        acquireQueued(
          // 标记该线程为独占方式,并加入同步队列尾部。          addWaiter(Node.EXCLUSIVE), arg)
       )
        selfInterrupt();
}

2.2 release(int 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;
}private void unparkSuccessor(Node node) {
  ....
     Node s = node.next; // 找到后继节点        if (s == null || s.waitStatus > 0) {//无后继或节点已取消            s = null;
           // 找到有效的等待节点             for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread); // 唤醒线程    }

感谢各位的阅读,以上就是“如何理解Java并发之同步器设计”的内容了,经过本文的学习后,相信大家对如何理解Java并发之同步器设计这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。

(0)

相关推荐

  • 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)

    前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...

  • ReentrantLock源码分析

    转自:https://blog.csdn.net/qq_37682665/article/details/114363445 目录 ReentrantLock 使用 核心源码解析 时序图 类图 Ree ...

  • 深入剖析AQS和CAS,看了都说好

    作者丨黎杜来源丨黎杜编程 前言 不知不觉写文章已经快半年了,本来之前写文章只是为了自己总结知识,不知不觉中关注的朋友越来越多了. 现在写文章不单单只是为了考虑自己能看懂,还要考虑各位读者大大是否能看懂 ...

  • 诚之和:如何理解Java通过加密技术保护源代码的方法

    这篇文章主要讲解了"如何理解Java通过加密技术保护源代码的方法",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"如何理解J ...

  • 诚之和:如何理解MySQL用户中的百分号%是否包含localhost

    这篇文章主要介绍"如何理解MySQL用户中的百分号%是否包含localhost",在日常操作中,相信很多人在如何理解MySQL用户中的百分号%是否包含localhost问题上存在疑 ...

  • 诚之和:如何理解NacosRibbonClientConfiguration

    今天就跟大家聊聊有关如何理解NacosRibbonClientConfiguration,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获. 序 本 ...

  • 诚之和:如何理解微服务架构下的高可用和高性能设计

    这篇文章主要讲解了"如何理解微服务架构下的高可用和高性能设计",文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习"如何理解微服务 ...

  • 诚之和:怎么理解php包装迭代器

    本篇内容介绍了"怎么理解php包装迭代器"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有 ...

  • 诚之和:如何理解Clang编译器优化触发的Crash

    本篇内容主要讲解"如何理解Clang编译器优化触发的Crash",感兴趣的朋友不妨来看看.本文介绍的方法操作简单快捷,实用性强.下面就让小编来带大家学习"如何理解Clan ...

  • 诚之和:如何理解Python基础中的for循环语句

    如何理解Python基础中的for循环语句,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题. Python for循环可以遍历任何序列的项目 ...

  • 诚之和:使用Java的数组和链表模拟栈的实现

    栈属于数据结构的一种,也是比较常用的一种数据结构.下面文章,将通过Java的数组和链表的形式模拟栈结构的实现,来帮助大家对于栈的理解. 一.何为栈? 栈(stack)又名堆栈,它是一种运算受限的线性表 ...

  • 诚之和:教你使用Java实现树形菜单对象 实例代码解析

    本文实例为大家分享了java实现树形菜单对象的具体代码,供大家参考,具体内容如下 1.SysMenu package com.zy.shiro.domain;import com.baomidou.m ...