AQS之Semaphore详解

AQS之Semaphore详解

  • 一、Semaphore类的继承关系
    • 1. AbstractQueuedSynchronizer:提供了一个同步器的框架。
    • 2. Sync:Semaphore的内部类,提供了锁的具体实现。
    • 3. FairSync:Sync的子类,实现公平锁。
    • 4. NonfairSync:Sync的子类,实现非公平锁。
  • 二、Semaphore的基本使用
    • 1. 使用场景:
    • 2. 代码实现:
    • 3. 运行结果:
    • 4. 案例分析:
  • 三、Semaphore的优缺点
    • 1. 简介:
    • 2. 优点:
      • 2.1、灵活性:
      • 2.2、可重入性:
      • 2.3、公平性:
      • 2.4、可以用于多种场景:
    • 3. 优点:
      • 3.1、使用复杂:
      • 3.2、容易造成死锁:
      • 3.3、不支持条件等待:
      • 3.4、可能出现饥饿:
      • 3.5 小结:
  • 四、源码分析
    • 1. 构造方法
      • 1.1 Semaphore(int permits)
      • 1.2 Semaphore(int permits, boolean fair)
      • 1.3 构造方法小结
    • 2. acquire方法
      • 2.1 AQS#acquireSharedInterruptibly
      • 2.2 Semaphore#tryAcquireShared
      • 2.3 AQS#doAcquireSharedInterruptibly
      • 2.4 AQS#doAcquireSharedInterruptibly#addWaiter
      • 2.5 AQS#addWaiter#enq
      • 2.6 AQS#shouldParkAfterFailedAcquire
      • 2.7 AQS#parkAndCheckInterrupt
      • 2.8 AQS#LockSupport.park
      • 2.9 流程图
    • 3. release方法
      • 3.1 AQS#releaseShared
      • 3.2 Semaphore#tryReleaseShared
      • 3.3 AQS#doReleaseShared
      • 3.4 AQS#doReleaseShared#unparkSuccessor
      • 3.5 AQS#LockSupport.unpark
      • 3.6 AQS#doReleaseShared#setHeadAndPropagate
      • 3.7 AQS#doReleaseShared#setHeadAndPropagate#isShared
      • 3.8 流程图

一、Semaphore类的继承关系

semaphore

1. AbstractQueuedSynchronizer:提供了一个同步器的框架。

  1. AQS为Semaphore提供了基本的针对共享资源的获取失败入队出队阻塞唤醒的逻辑。
  2. Semaphore通过AQS的同步状态来表示可用的许可数,并通过AQS的等待队列来管理等待获取许可的线程。
  3. 当一个线程请求获取许可时,如果许可数不足,则该线程会被阻塞并加入到AQS的等待队列中。
  4. 当有其他线程释放许可时,AQS会从等待队列中选择一个线程唤醒,使其重新尝试获取许可。
  5. 这样就实现了对共享资源的获取失败入队出队阻塞唤醒的逻辑。

2. Sync:Semaphore的内部类,提供了锁的具体实现。

  1. Sync是Semaphore的内部类,它继承自AQS并重写了其中的方法,用于实现Semaphore的同步逻辑。

3. FairSync:Sync的子类,实现公平锁。

  1. FairSync是Sync的子类,它实现了公平的获取许可的机制。当线程请求许可时,FairSync会按照FIFO的顺序选择等待的线程获取许可。

4. NonfairSync:Sync的子类,实现非公平锁。

  1. NoFairSync是Sync的子类,它实现了非公平的获取许可的机制。当线程请求许可时,NoFairSync会直接尝试获取许可,而不管是否有其他线程在等待。

二、Semaphore的基本使用

1. 使用场景:

假设有一个公共游泳池,最多允许同时有5个人在游泳,其他人需要等待。这就可以使用Semaphore来控制并发访问的线程数。
picture

2. 代码实现:

import java.util.concurrent.Semaphore;

public class SwimmingPool {
    private static final int MAX_SWIMMERS = 5;
    private static final Semaphore semaphore = new Semaphore(MAX_SWIMMERS);

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            Thread swimmer = new Thread(new Swimmer(i));
            swimmer.start();
        }
    }

    static class Swimmer implements Runnable {
        private int id;

        public Swimmer(int id) {
            this.id = id;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire(); // 请求获取许可,如果没有可用许可则阻塞等待
                System.out.println("Swimmer " + id + " starts swimming.");
                Thread.sleep(10000); // 模拟游泳过程
                semaphore.release(); // 释放许可
                System.out.println("Swimmer " + id + " finishes swimming.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3. 运行结果:

semaphore

4. 案例分析:

  1. 在这个案例中,有10个游泳者想要在游泳池中游泳,但是游泳池最多允许同时有5个人在游泳。
  2. Semaphore的初始许可数为5,当有游泳者调用acquire()方法请求获取许可时,如果许可数不足,则该游泳者会被阻塞等待。
  3. 当有其他游泳者完成游泳并调用release()方法释放许可时,Semaphore会选择一个等待的游泳者唤醒,使其开始游泳。
  4. 通过Semaphore的控制,保证了同时游泳的人数不超过5个。

三、Semaphore的优缺点

1. 简介:

Semaphore是一种并发控制工具,用于限制同时访问某个资源或执行某个任务的线程数量。它在多线程环境中起到了线程同步和互斥的作用。

2. 优点:

2.1、灵活性:

Semaphore可以根据需要设置初始许可数,允许多个线程同时访问共享资源,或者限制同时执行的任务数量。

2.2、可重入性:

Semaphore是可重入的,同一个线程可以多次获取和释放许可。

2.3、公平性:

Semaphore可以选择是否公平地分配许可。如果设置为公平模式,那么等待时间最长的线程将优先获取许可。

2.4、可以用于多种场景:

Semaphore可以用于解决生产者-消费者问题、连接池管理、并发线程数控制等多种并发场景。

3. 优点:

3.1、使用复杂:

相对于其他线程同步和互斥的工具,Semaphore的使用相对复杂,需要手动调用acquire()和release()方法进行许可的获取和释放,容易出现逻辑错误。

3.2、容易造成死锁:

如果在使用Semaphore时没有正确地释放许可,可能会导致线程间的死锁情况,造成程序无法继续执行。

3.3、不支持条件等待:

与ReentrantLock相比,Semaphore不支持线程的条件等待和通知,无法使用wait()和notify()方法进行线程间的通信。

3.4、可能出现饥饿:

在公平模式下,许可的获取是按照线程等待的先后顺序进行的,这可能导致某些线程一直无法获取到许可,出现饥饿现象。

3.5 小结:

综上所述,Semaphore是一种功能强大的并发控制工具,能够灵活地管理共享资源的访问和任务的执行。然而,由于其使用复杂、可能导致死锁、不支持条件等待以及可能出现饥饿等缺点,开发人员在使用Semaphore时需要仔细考虑和处理这些问题。

四、源码分析

1. 构造方法

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
	sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

1.1 Semaphore(int permits)

  • 参数permits表示同时可以访问的线程数目。如果permits的值为1,表示Semaphore对象可以用作互斥锁。
  • 当permits的值大于1时,Semaphore对象可以用作资源池的控制器,限制可以访问资源的线程的数量。

1.2 Semaphore(int permits, boolean fair)

  • 参数fair表示是否采用公平锁策略。如果为true,表示Semaphore对象采用公平锁策略,即先进入等待队列的线程将先获得许可;如果为false,表示Semaphore对象采用非公平锁策略,线程获取许可的顺序是不确定的。

1.3 构造方法小结

  • 使用Semaphore的构造方法,可以创建一个具有指定许可数目和锁策略的Semaphore对象。
  • 根据不同的应用场景,可以选择适合的构造方法来创建Semaphore对象,然后使用acquire()和release()方法来获取和释放许可。

2. acquire方法

外部调用加锁方法acquire(),(支持可中断)
中断概念如果不清楚的话,可以参考Java之线程中断

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

2.1 AQS#acquireSharedInterruptibly

acquire方法内部会调用acquireSharedInterruptibly方法,可以看到
AQS给我们提供了模板方法: tryAcquireShared

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

注意,这里没有实现加锁的逻辑(模板方法留给子类实现)
子类

2.2 Semaphore#tryAcquireShared

调用子类Semaphore的tryAcquireShared方法(这里以非公平为例)

protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
}
//非公平
final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
//公平
protected int tryAcquireShared(int acquires) {
    for (;;) {
        if (hasQueuedPredecessors())
            return -1;
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}
  1. 获取剩余资源许可int available = getState();
  2. 得到剩余许可remaining
  3. 小于0直接返回,否则,说明有许可,CAS操作保证线程安全获取锁
  4. CAS成功则获取锁,执行业务代码。
  5. 如果没有获取锁tryAcquireShared(arg) < 0,回到acquireSharedInterruptibly方法
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

2.3 AQS#doAcquireSharedInterruptibly

没有可用资源,只能调用doAcquireSharedInterruptibly(走入队逻辑)

private void doAcquireSharedInterruptibly(int arg)
   throws InterruptedException {
   final Node node = addWaiter(Node.SHARED);
   boolean failed = true;
   try {
       for (;;) {
           final Node p = node.predecessor();
           if (p == head) {
               int r = tryAcquireShared(arg);
               if (r >= 0) {
                   setHeadAndPropagate(node, r);
                   p.next = null; // help GC
                   failed = false;
                   return;
               }
           }
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               throw new InterruptedException();
       }
   } finally {
       if (failed)
           cancelAcquire(node);
   }
}

2.4 AQS#doAcquireSharedInterruptibly#addWaiter

addWaiter方法: 添加到等待队列

  1. 构造Node结点
  2. 第一个入队的线程需要调用enq(node);
  3. 创建队列
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

创建一个需要入队的结点,队列不为空的情况下,CAS操作将其设置为tail尾结点。

2.5 AQS#addWaiter#enq

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  1. 队列没有一个结点,那么第一个线程需要构建队列。
  2. 开始入队,尾插法
    2.1 node.prev = t; 修改当前结点的前驱指针
    2.2 compareAndSetTail(t, node) 将当前结点CAS置为tail尾结点
    2.3 t.next = node;修改当前结点的后继指针
  3. addWaiter方法结束,入队完成。
  4. final Node p = node.predecessor();获取当前结点的前驱结点
  5. 如果前驱结点是head,又会调用tryAcquireShared(子类),尝试获取资源
  6. 如果没有可用许可,调用shouldParkAfterFailedAcquire准备阻塞

2.6 AQS#shouldParkAfterFailedAcquire

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

准备阻塞:将当前结点的前驱结点pred.waitStatus(CAS)置为-1 SIGNAL 可唤醒

static final int SIGNAL    = -1;

-1 表示后继的线程结点需要被唤醒

2.7 AQS#parkAndCheckInterrupt

调用parkAndCheckInterrupt()方法

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

2.8 AQS#LockSupport.park

最后一步,LockSupport.park(this),真正阻塞,将当前线程挂起。

2.9 流程图

acquire

3. release方法

外部调用释放锁方法:release()方法

public void release() {
    sync.releaseShared(1);
}

3.1 AQS#releaseShared

通过内部类sync调用AQS模板方法:releaseShared()方法

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

tryReleaseShared方法AQS内部没有实现,留给子类实现。
模板

3.2 Semaphore#tryReleaseShared

来到Semaphore类tryReleaseShared 子类提供具体实现释放锁的逻辑。

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

tryReleaseShared大致逻辑:

  1. 获取当前state变量值,把资源的许可数量+1
  2. 通过CAS+for循环保证操作更新成功。
  3. tryReleaseShared结束(释放资源)

3.3 AQS#doReleaseShared

tryReleaseShared结束来到,AQS的doReleaseShared方法。
调用doReleaseShared,唤醒阻塞队列线程。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

如果头结点head等于Node.SIGNAL,CAS置为0,唤醒前的准备工作。

3.4 AQS#doReleaseShared#unparkSuccessor

doReleaseShared内部调用unparkSuccessor方法

private void unparkSuccessor(Node node) {
      
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        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);
    }

获取到Node s = node.next;头结点的下一个结点要唤醒的结点

3.5 AQS#LockSupport.unpark

执行LockSupport.unpark(s.thread)方法
唤醒阻塞线程

3.6 AQS#doReleaseShared#setHeadAndPropagate

紧接着被唤醒的线程会来到doAcquireSharedInterruptibly方法的for循环里
刚好等于头结点,可以尝试获取资源

if (p == head) {
    int r = tryAcquireShared(arg);
    if (r >= 0) {
        setHeadAndPropagate(node, r);
        p.next = null; // help GC
        failed = false;
        return;
    }
}

获取head结点,将当前结点设置为head结点,释放之前的头结点,让gc回收。

3.7 AQS#doReleaseShared#setHeadAndPropagate#isShared

到这里还没完,setHeadAndPropagate方法里会调用isShared() 判断当前链表是否是共享模式

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

如果前驱节点正好是head,就可以尝试获取资源,获取成功就可以执行业务逻辑,只要资源数(state>0)充足,可以一直唤醒。

3.8 流程图

release

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/33103.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ARHUD驾车导航技术概览

ARHUD(Augmented Reality Head Up Display)&#xff0c;即增强现实与抬头显示的结合&#xff0c;是一种将渲染元素投影在真实世界的技术&#xff0c;也是目前用户理解成本最低的展示方式。 HUD功能第一次应用是在二战中&#xff0c;被应用在枪械和战斗机上&#xff0c;80年代初…

React hooks文档笔记(三) 状态

状态 一、如何设计组件状态的步骤二、状态构造原则1. 组相关状态2. 避免矛盾/互斥状态3. 避免多余状态4. 不要把props放进state&#xff0c;除非你特别想要阻止更新 三、状态保存/重置1. 相同位置的相同组件保留状态2. 同一位置不同元素reset状态 一、如何设计组件状态的步骤 …

组装电脑U盘重装Win10系统教程图解

当您需要对组装电脑进行重新安装Win10操作系统时&#xff0c;使用U盘是一种方便而有效的方法&#xff0c;U盘重装系统不仅可以帮助您解决各种系统问题&#xff0c;还能提供一个干净、稳定的系统环境。无论您是初学者还是有一定经验的用户&#xff0c;本教程将提供清晰的组装电脑…

游戏革命2023:AIGC拯救游戏厂商

文明史即工具史&#xff0c;纵观人类社会的演化&#xff0c;每一次的加速迭代&#xff0c;都有赖于关键性的技术突破。 前有蒸汽机到电力普及的生产力大爆发&#xff0c;以及计算机、互联网的诞生打开新世界&#xff0c;如今AIGC将再次推动先进技术工具的变革。 随着ChatGPT的…

观察者模式(二十)

相信自己&#xff0c;请一定要相信自己 上一章简单介绍了迭代器模式(十九), 如果没有看过, 请观看上一章 一. 观察者模式 引用 菜鸟教程里面 观察者模式介绍: https://www.runoob.com/design-pattern/observer-pattern.html 当对象间存在一对多关系时&#xff0c;则使用观察…

CSS之定位

作用&#xff1a;灵活的改变盒子在网页中的位置 实现&#xff1a; 1.定位模式&#xff1a;position 2.边偏移&#xff1a;设置盒子的位置 leftrighttopbottom 相对定位 position: relative 特点&#xff1a; 不脱标&#xff0c;占用自己原来位置显示模式特点保持不变设…

OpenStack(4)--NameSpace实现不同项目(租户)重叠网段

openstack通过namespace将不同项目&#xff08;租户&#xff09;的网络隔离&#xff0c;每个项目的管理员都需要对项目网络进行规划建设&#xff0c;这就导致不同项目之间会重复使用到某些网段&#xff0c;例如192.168.X.X就是管理员习惯使用的网段。 上一次我们新建位于vxlan…

【TCP/IP】多进程服务器的实现(进阶) - 多进程服务器模型及代码实现

经过前面的铺垫&#xff0c;我们已经具备实现并发服务器的基础了&#xff0c;接下来让我们尝试将之前的单任务回声服务器改装成多任务并发模式吧&#xff01; 多任务回声服务器模型 在编写代码前&#xff0c;先让我们大致将多任务&#xff08;回声&#xff09;服务器的模型抽象…

通过USB和wifi连接真机编写第一个脚本

目录 一、连接手机 1、通过usb数据线连接手机 2、无线连接手机 二、编写第一个脚本 一、连接手机 1、通过usb数据线连接手机 数据线连接手机并允许调试 cmd命令行执行&#xff1a; adb devices 如果没有显示device信息&#xff0c;请检查&#xff1a; 手机是否开启usb调…

配置了git config --global credential.helper store后,还是弹出输入密码框

使用http协议拉取代码时,每次pull/push都会弹出账号密码框,可以使用git的配置credential.helper来保存每次输入的账号密码到硬盘上,命令git config --global credential.helper store,store表示存到硬盘中,但是按照这样操作后git pull还是弹出密码框,通过git config --list发现…

【雕爷学编程】Arduino动手做(137)---MT8870语音解码

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

云原生之深入解析Dapr安全性之访问控制策略

一、服务调用范围访问策略 ① 跨命名空间的服务调用 Dapr 通过服务调用 API 提供端到端的安全性&#xff0c;能够使用 Dapr 对应用程序进行身份验证并设置端点访问策略&#xff1a; Dapr 应用程序可以被限定在特定的命名空间&#xff0c;以实现部署和安全&#xff0c;当然仍然…

Istio 什么是服务网格

什么是服务网格 服务网格(Service Mesh)这个术语通常用于描述构成这些应用程序的微服务网络以及应用之间的交互。随着规模和复杂性的增长&#xff0c;服务网格越来越难以理解和管理。 它的需求包括服务发现、负载均衡、故障恢复、指标收集和监控以及通常更加复杂的运维需求&am…

oracle字符集

1、查看oracle字符集 如果操作系统或者客户端的字符集设置和数据库设置不一样就会出现乱码 查询NLS_LANG即操作系统环境变量要设为 NLS_LANGUAGE_NLS_TERRITORY**.NLS_CHARACTERSET**&#xff0c;如&#xff1a; export NLS_LANG“AMERICAN_AMERICA.AL32UTF8”

Hadoop环境搭建

一、简介 1.1、概念 Hadoop是一个由Apache基金会所创建的分布式系统基础架构&#xff0c;主要解决海量数据的存储和海量数据的分析计算问题&#xff0c;从广义上来说hadoop是数据存储分包器&#xff0c;可以存储大量的数据。 1.2、优势 Hadoop具有高可靠性&#xff08;Hado…

electron+vue3+ts+vite

首先使用vite工具创建一个vue3ts的项目 npm create vite创建好vuets项目后启动项目 cd electron-vue3-ts-vitenpm installnpm run dev 访问http://127.0.0.1:5173/地址可以看到项目已经启动成功 安装Electron 接下来我们安装electron&#xff0c;使用以下命令 npm i -D el…

CV什么时候能迎来ChatGPT时刻?

卷友们好&#xff0c;我是rumor。 最近看了几篇CV的工作&#xff0c;肉眼就感受到了CVer们对于大一统模型的“焦虑”。 这份焦虑让他们开始尝试统一一切&#xff0c;比如&#xff1a; 统一复杂的自动驾驶任务的优化目标[1]&#xff0c;来自今年CVPR最佳论文。统一典型的CV任务&…

360手机 360手机刷机最高安卓版本参考

360手机 360手机刷机最高安卓版本参考 参考&#xff1a;360手机-360刷机360刷机包twrp、root 360刷机包360手机刷机&#xff1a;360rom.github.io 【360手机(最高)安卓版本】 以下列举为常见360手机机型&#xff1b;其它早期系列&#xff0c;一般为Android4-6左右360手机UI界…

doker安装RabbitMQ以及用java连接

目录 doker安装&#xff1a; RabitMq安装&#xff1a; java链接 doker安装&#xff1a; 参考链接&#xff08;非常详细&#xff09;&#xff1a; docker安装以及部署_docker bu shuminio_春风与麋鹿的博客-CSDN博客 安装好后开启doker //启动docker服务 systemctl start do…

保偏产品系列丨5款保偏光纤产品简介

保偏光纤应用日益扩大&#xff0c;特别是在干涉型传感器等测量方面&#xff0c;利用保偏光纤的光无源器件起着非常重要的作用&#xff0c;种类也很多。 本文来介绍5款保偏光纤系列产品以及它们的性能&#xff0c;欢迎收藏转发哦&#xff01; 01、保偏光纤跳线-TLPMPC 保偏光纤跳…