从semaphore分析aqs

9 篇文章 0 订阅
3 篇文章 0 订阅

介绍

semaphore是一个java多线程并发工具包里面的一个并发工具类. 它主要用于协调多线程下的线程之间的通信. 它非常适用于限流.

使用

  • semaphore的使用很简单 , 我们模拟有一个线程池,最多只能同时被10个线程同时操作,如果超出10个, 就让后面的线程进行等待.
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;

/**
 * 业务场景: 模仿一个数据库连接池, 最多只允许同时有10个线程在同时操作
 */
public class SemaphoneDemo {

    /**
     * 限定为只能同时10个线程使用
     */
    static Semaphore use = new Semaphore(10);

    /**
     * 假设此队列为存放线程的线程池
     */
    private static LinkedBlockingDeque<Object> list = new LinkedBlockingDeque<>();

    static {
        for (int i = 0; i < 10; i++) {
            // 初始化数据库10个连接池
            list.add(new Object());
        }
    }
    /**
     * 获取连接
     *
     * @return
     */
    public static Object takeConnection() throws InterruptedException {
        // 获取许可
        use.acquire();
        Object o = list.removeFirst();
        if (o == null) {
            System.out.println("获取线程池出现错误");
        }
        return o;
    }
    /**
     * 释放连接
     */
    public static void releaseConnection(Object object) {
        list.addLast(object);
        /// 释放许可
        use.release();
        System.out.println("当前有" + use.getQueueLength() + "个线程等待数据库连接!!"
                + "可用连接数:" + use.availablePermits() + "当前集合数量为:" + list.size());
    }

    public static void main(String[] args) throws InterruptedException {

        // 使用20个线程不断从连接池获取连接
        for (int i = 0; i < 20; i++) {

            new Thread(() -> {
                for (int j = 0; j < 20; j++) {
                    Object o = null;
                    try {
                        o = takeConnection();
                        // 执行业务代码
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        releaseConnection(o);
                    }
                }

            }).start();
        }

       Thread.sleep(3000);
        System.out.println("main---------当前有" + use.getQueueLength() + "个线程等待数据库连接!!"
                + "可用连接数:" + use.availablePermits() + "当前集合数量为:" + list.size());

    }

结果分析

上述程序非常简单, 在获取连接的时候, 调用了semaphore的acquire()方法, 在归还连接的时候调用了semaphore的release()方法. 就实现了简单的限流. 只能最多有10个线程获取连接.

疑问

  • 它是如何实现阻塞的
  • 它是如何实现释放资源的

如何实现

  • aqs中有一个Node类,这个类记住了上一个节点,下一个节点,头部和尾部, 这个就是aqs同步队列,如图
    在这里插入图片描述

  • 队列的加入和取出都使用了 cas操作保证了该队列在多线程先的安全问题

  • 每次调用acquire方法时, 如果获取到许可, 就直接执行, 如果没有 , 就加入到同步队列的尾部进行, 然后进行阻塞

  • 每次调用release方法时, 释放许可,并且唤醒到头部第一个线程,

  • 执行流程
    在这里插入图片描述

源码分析

  • 类结构

AbstractQueuedSynchronizer 就是大名鼎鼎的aqs了
Sync继承自AbstractQueuedSynchronizer 实现同步锁功能
FairSync继承自Sync是公平锁的实现
NonfairSync继承自Sync是非公平的实现
在这里插入图片描述

  • acquire方法分析, 做了什么
 public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

在类Semaphore中它调用acquireSharedInterruptibly(1)方法


 public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 线程被阻断, 抛出线程阻断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        // 尝试获取一次获取许可证, 获取到了直接通行,执行自己的业务代码 
        if (tryAcquireShared(arg) < 0)
        	// 没有获取, 则加入到aqs共享线程队列
            doAcquireSharedInterruptibly(arg);
    }
  • tryAcquireShared 方法在Semaphore里面有两个实现, 一个是NonfairSync里的,一个是FairSync里的

// 调用了父类的初始化方法
 NonfairSync(int permits) {
            super(permits);
        }

 // 我们传的数字状态给到了state了 state的初始值就是我们设置的10
  Sync(int permits) {
            setState(permits);
        }

// Semaphore 默认使用了非公平锁
 public Semaphore(int permits) {
        sync = new NonfairSync(permits);
 }

  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;

			// 此if 会返回两种结果, 小于0 , 则是没有获取到许可证, 大于0 ,表示获取到了许可证
			// compareAndSetState 如果不懂请百度 google
            }
        }

  • 没有获取许可证的时候进入这个方法

 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;
                    }
                }
                // 此处进行阻塞  parkAndCheckInterrupt
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                  
                    throw new InterruptedException();
            }
        } finally {
            if (failed) // 此处代码取消该节点的状态
                cancelAcquire(node);
        }
    }

// 把当前线程加入到aqs同步队列
 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;
    }

  • 走到parkAndCheckInterrupt 这端代码就进行了阻塞, 我们看看release怎么去唤醒的
	
 public void release() {
        sync.releaseShared(1);
    }

 public final boolean releaseShared(int arg) {
 		// 释放许可
        if (tryReleaseShared(arg)) {
        	// 通知其他程序获取许可
            doReleaseShared();
            return true;
        }
        return false;
    }

// 执行释放许可

 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;
            }
        }

// 光释放许可还不行, 还需要通知给其他程序, 现在锁已经是空闲状态,可以获取了
 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;
        }
    }

总结

同步的实现关键借用了cas来实现, 而cas直接调用了cpu的指令, 来保证操作的同步性, 使用双向队列这个数据结构来存储线程,并且又使用了 LockSuppert工具类类进行唤醒和阻塞操作.

提出疑问

  • 使用其他的数据结构是否可以超越双向队列的性能?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值