浅谈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,里面支持了 getAndSet compareAndSet 操作。

看下面例子,主要在区别是等待方式不一样,上面是通过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中的AQS。

到此这篇关于浅谈Java并发之同步器设计的文章就介绍到这了,更多相关Java并发之同步器设计内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java并发编程之threadLocal

    目录 1.ThreadLocal介绍 2.ThreadLocal使用实例 3.ThreadLocal实现原理 1.ThreadLocal介绍 多个线程访问同一个共享变量时特别容易出现并发问题,特别是多线程需要对共享变量进行写入时.为了保证线程安全,一般使用者在访问共享变量的时候需要进行适当的同步,如图 同步的一般措施是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担,那么有没有一种方法可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadL

  • Java实现计算器设计

    本文实例为大家分享了Java实现计算器设计的具体代码,供大家参考,具体内容如下 需求分析 目的是实现一个基于Java的可以求解带括号加减乘除表达式的带界面的计算器. 需要知道的Java技术:Java Swing(Java图形界面设计).Java集合(栈).lambda表达式.Java基础等. 设计思路 1.实现一个Java计算器界面类 2.实现一个Java计算带括号加减乘除表达式的类 3.实现主函数调用 设计实现 Java计算器项目结构: Calculator类为计算器界面设计.Calculat

  • Java装饰器设计模式初探

    本篇随笔主要介绍用Java实现简单的装饰器设计模式: 先来看一下装饰器设计模式的类图: 从图中可以看到,我们可以装饰Component接口的任何实现类,而这些实现类也包括了装饰器本身,装饰器本身也可以再被装饰. 下面是用Java实现的简单的装饰器设计模式,提供的是从基本的加入咖啡入手,可以继续加入牛奶,巧克力,糖的装饰器系统. interface Component { void method(); } class Coffee implements Component { @Override

  • Java并发编程之线程中断

    目录 线程中断: void interrupted()方法:中断线程,例如,当线程A运行时,线程B可以调用线程A的interrupted()方法来设置线程的中断标志为true并立即返回.设置标志仅仅是为了设置标志,线程A实际并没有被中断,它会继续往下执行,如果线程A因为调用了wait()方法,join()方法或者sleep()方法而引起的阻塞挂起,这时候若线程B调用线程A的interrupted()方法,线程A回调用这些方法的地方会抛出InterruptedException异常而返回. boo

  • Java 浅谈 高并发 处理方案详解

    目录 高性能开发十大必须掌握的核心技术 I/O优化:零拷贝技术 I/O优化:多路复用技术 线程池技术 无锁编程技术 进程间通信技术 Scale-out(横向拓展) 缓存 异步 高性能.高可用.高拓展 解决方案 高性能的实践方案 高可用的实践方案 高扩展的实践方案 总结 高性能开发十大必须掌握的核心技术 我们循序渐进,从内存.磁盘I/O.网络I/O.CPU.缓存.架构.算法等多层次递进,串联起高性能开发十大必须掌握的核心技术. - I/O优化:零拷贝技术 - I/O优化:多路复用技术 - 线程池技

  • Java中转换器设计模式深入讲解

    前言 在这篇文章中,我们将讨论 Java / J2EE项目中最常用的  Converter Design Pattern.由于Java8 功能不仅提供了相应类型之间的通用双向转换方式,而且还提供了转换相同类型对象集合的常用方法,从而将样板代码减少到绝对最小值.我们使用Java8 功能编写了此模式的源代码. 目的 转换器设计模式的目的是为相应类型之间的双向转换提供一种通用的方式,允许类型无需彼此了解的简洁的实现.此外,转换器设计模式引入了双向收集映射,将样板代码减少到最小. 源代码 转换器设计模式

  • Java并发内存模型详情

    目录 1.Java内存模型 2.硬件内存架构 3.实际执行 3.1 共享对象可见性 3.2 竞争条件 Java是一门支持多线程执行的语言,要编写正确的并发程序,了解Java内存模型是重要前提.而了解硬件内存模型有助于理解程序的执行. 本文主要整理以下内容 Java内存模型 硬件内存架构 共享对象可见性 竞争条件 1.Java内存模型 Java内存模型最新修订是在Java5. JSR-176 罗列了 J2SE5.0 相关发布特性,包含其中的 JSR-133(JavaTM内存模型与线程规范),jav

  • Java装饰器设计模式_动力节点Java学院整理

    定义: 动态给一个对象添加一些额外的职责,就象在墙上刷油漆.使用Decorator模式相比用生成子类方式达到功能的扩充显得更为灵活.设计初衷:通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的. 要点: 装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为 实际上Java 的I/O API就是使用Decorator实现的. //定义被装

  • Java编程Iterator迭代器设计原理及实现代码示例

    我们知道迭代器(Iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素.那么Iterator迭代器的设计原理是什么呢?迭代器问什么定义了一个借口,而不是一个类呢? 我们假设迭代器迭代数据的功能定义为了一个类,那么,会有这样的问题.不同的集合,由于数据结构不一样,所以他们的存储方式也是不一样的.也就是说,迭代器获取的时候,获取的方式是变化的,也就是不固定的.所以把这种方式定义为具体的实现是不合理的. 无论何种集合,他们肯定都有获取的功能,而且不知道什么时候就没有数据了.所有他

  • Java并发编程之阻塞队列深入详解

    目录 1. 什么是阻塞队列 2. 阻塞队列的代码使用 3. 生产者消费者模型 (1)应用一:解耦合 (2)应用二:削峰填谷 (3)相关代码 4.阻塞队列和生产者消费者模型功能的实现 1. 什么是阻塞队列 阻塞队列是一种特殊的队列,和数据结构中普通的队列一样,也遵守先进先出的原则同时,阻塞队列是一种能保证线程安全的数据结构,并且具有以下两种特性:当队列满的时候,继续向队列中插入元素就会让队列阻塞,直到有其他线程从队列中取走元素:当队列为空的时候,继续出队列也会让队列阻塞,直到有其他线程往队列中插入

随机推荐