并发工具:Semaphore工具(二)

文章目录

    • 1 Semaphore API介绍
      • 1.1 构造
      • 1.2 tryAcquire方法
        • 1.2.1 重载一
        • 1.2.2 重载二:超时设置
        • 1.2.3 重载三:获取多张许可
      • 1.3 acquire方法
      • 1.4 acquireUninterruptibly
      • 1.5 正确使用release
        • 1.5.1 release使用不当示例和改进
        • 1.5.2 扩展Semaphore增强release
      • 1.6 其他方法
    • 3 总结

1 Semaphore API介绍

1.1 构造

/**
定义Semaphore指定许可证数量,
并且指定非公平的同步器,
因此new Semaphore(n)实际上是等价于new Semaphore(n,false)的。
**/
public Semaphore(int permits)
/**
定义Semaphore指定许可证数量的同时给定非公平或是公平同步器。
true: 公平同步器。
false:非公平
**/
public Semaphore(int permits, boolean fair)

1.2 tryAcquire方法

tryAcquire方法尝试向Semaphore获取许可证,如果此时许可证的数量少于申请的数量,则对应的线程会立即返回,结果为false表示申请失败.

1.2.1 重载一
/**
尝试获取Semaphore的许可证,该方法只会向Semaphore申请一个许可证,
在Semaphore内部的可用许可证数量大于等于1的情况下,许可证将会获取成功,
反之获取许可证则会失败,并且返回结果为false。
**/
boolean tryAcquire();

测试:

public static void main(String[] args) {
    // 定义一张许可证
    final Semaphore semaphore = new Semaphore(1, true);
    boolean res = semaphore.tryAcquire();
    // 第一次可以获取到许可证,返回true
    System.out.println("第一次获取结果:" + res); // true
    res = semaphore.tryAcquire();
    // 第二次可以获取不到许可证,因为没了,返回false
    System.out.println("第二次获取结果:" + res); // false
}
1.2.2 重载二:超时设置
/**
尝试获取一个许可证,但是增加了超时参数。
如果在超时时间内还是没有可用的许可证,那么线程就会进入阻塞状态,
直到到达超时时间或者在超时时间内有可用的证书(被其他线程释放的证书),
或者阻塞中的线程被其他线程执行了中断。
**/
boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException

测试

public static void main(String[] args) throws InterruptedException {
    final Semaphore semaphore = new Semaphore(1, true);
    // 开启一个线程获取许可证
    new Thread(()->{
        // 获取许可证
        boolean get = semaphore.tryAcquire();
        if(get){
            // 获取到了就休眠10s
            System.out.println(Thread.currentThread().getName() + " 获取到许可");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                // 10s 后释放掉
                semaphore.release();
            }
        }
    },"T1").start();

    // 休眠1s,保证T1启动并获取到许可
    TimeUnit.SECONDS.sleep(1);
    // 主线程也去获取许可证,但是设置了3s的超时
    // 3s内没有获取到,就会在这阻塞,直到获取到许可就返回
    // 超了3s没有获取到许可, 就会从阻塞中退出,返回false,表示未获取到
    boolean get = semaphore.tryAcquire(3, TimeUnit.SECONDS); // false
    System.out.println(get == false);
}

匿名线程T1首先获取到了仅有的一个许可证之后休眠了10秒的时间,紧接着主线程想要尝试获取许可证,并且指定了3秒的超时时间,很显然主线程在被阻塞了3秒的时间之后退出阻塞,但还是不能够获取到许可证书,因为匿名线程并未释放

1.2.3 重载三:获取多张许可
/**
向Semaphore尝试获取指定数目的许可证。
**/
boolean tryAcquire(int permits);
/**
向Semaphore尝试获取指定数目的许可证。
并增加超时设置
**/
boolean tryAcquire(int permits, long timeout, TimeUnit unit)

测试

public static void main(String[] args) {
    final Semaphore semaphore = new Semaphore(5, true);
    boolean b = semaphore.tryAcquire(5);
    System.out.println(b); // true

    final Semaphore semaphore2 = new Semaphore(5, true);
    // 定义5个,但是获取6个
    boolean res = semaphore2.tryAcquire(6);
    System.out.println(res); // false
}

可见:传入的数量能否Semaphore中许可证的数量时,是不会获取成功的

1.3 acquire方法

acquire方法也是向Semaphore获取许可证,但是该方法比较偏执一些,获取不到就会一直等(陷入阻塞状态),Semaphore为我们提供了acquire方法的两种重载形式。

/**
该方法会向Semaphore获取一个许可证,如果获取不到就会一直等待,
直到Semaphore有可用的许可证为止,或者被其他线程中断。
当然,如果有可用的许可证则会立即返回。
**/
void acquire() throws InterruptedException

/**
该方法会向Semaphore获取指定数量的许可证,
如果获取不到就会一直等待,直到Semaphore有可用的相应数量的许可证为止,
或者被其他线程中断。
同样,如果有可用的permits个许可证则会立即返回。
**/
void acquire(int permits) throws InterruptedException

测试:

 public static void main(String[] args) throws InterruptedException {
     final Semaphore semaphore = new Semaphore(1, true);
     // 主线程先获取到
     try {
         semaphore.acquire();
     } catch (InterruptedException e) {
         // ignore
     }

     // 开启一个线程去获取
     new Thread(()->{
         // 这里会被阻塞,直到主线程释放掉许可
         try {
             semaphore.acquire();
         } catch (InterruptedException e) {
             // ignore
         }
         System.out.println(Thread.currentThread().getName() + " 获取到许可");
     },"T1").start();

     TimeUnit.SECONDS.sleep(10);
     // 主线程休眠10s后,释放掉需要,此时T1就可以获取道许可
     System.out.println("主线程即将释放掉许可");
     semaphore.release();
 }

可以看到acquire方法在没有可用许可证(permit)时将会一直等待,直到出现可用的许可证(permit)为止

另外:acquire会抛出InterruptedException异常,也就是说acquire是可以被中断阻塞

1.4 acquireUninterruptibly

/**
该方法会向Semaphore获取一个许可证,如果获取不到就会一直等待,
与此同时对该线程的任何中断操作都会被无视,直到Semaphore有可用的许可证为止。
当然,如果有可用的许可证则会立即返回。
**/
void acquireUninterruptibly()

void acquireUninterruptibly(int permits)

这个方法和acquire的区别:acquire会抛出InterruptedException异常,也就是说acquire是可以被中断阻塞,而acquireUninterruptibly的阻塞是不可以被中断的

1.5 正确使用release

1.5.1 release使用不当示例和改进

在一个Semaphore中,许可证的数量可用于控制在同一时间允许多少个线程对共享资源进行访问,所以许可证的数量是非常珍贵的。因此当每一个线程结束对Semaphore许可证的使用之后应该立即将其释放,允许其他线程有机会争抢许可证

/**
释放一个许可证,并且在Semaphore的内部,可用许可证的计数器会随之加一,
表明当前有一个新的许可证可被使用。
**/
void release()
/**
释放指定数量(permits)的许可证,并且在Semaphore内部,可用许可证的计数器会随之增加permits个,
表明当前又有permits个许可证可被使用。
**/
void release(int permits)

这种方法我们的第一反应是将其放到try…finally…语句块中,这样无论在任何情况下都能确保将已获得的许可证释放,但是恰恰是这样的操作会导致对Semaphore的使用不当

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @author wyaoyao
 * @date 2021/4/22 14:46
 */
public class SemaphoreExample4 {

    public static void main(String[] args) throws InterruptedException {
        // 定义一个semaphore, 许可证数量为1
        Semaphore semaphore = new Semaphore(1,true);
        // 开始一个线程获取线程
        Thread t1 = new Thread(() -> {
            // 获取许可
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " 获取到许可");
                // 休眠1小时
                TimeUnit.HOURS.sleep(1);
            } catch (InterruptedException e) {
                // ignore
            }finally {
                // 释放掉
                semaphore.release();
            }
        }, "T1");

        t1.start();

        // 休眠2s,保证t1正常启动,并获取许可
        TimeUnit.SECONDS.sleep(2);

        // 在开启一个线程t2
        Thread t2 = new Thread(() -> {
            // 获取许可
            try {
                semaphore.acquire();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + "被打断,退出阻塞");
            }finally {
                semaphore.release();
            }
        }, "T2");
        t2.start();

        // 休眠2s,保证t2正常启动
        TimeUnit.SECONDS.sleep(2);

        // 主线程中打断t2
        t2.interrupt();
        // 主线程获取许可
        semaphore.acquire();
        System.out.println(Thread.currentThread().getName() + " 获取到许可");
    }
}

简单分析这段代码:
首先可以百分之百地确认当前的JVM有三个非守护线程(t1、t2以及主线程(main线程))
线程t1将会首先获取Semaphore的一个许可证,并且在一个小时之后将其释放,
线程t2启动之后将会被阻塞(由于当前没有可用的许可证,因此执行acquire()方法的t2线程将会陷入阻塞等待可用的许可证)很快,在主线程中线程t2被中断,此时t2退出阻塞
此时主线程去获取许可,按照理论上理解,这里主线程也会被阻塞,因为t1没有释放掉许可,在休眠中(1个小时)

运行代码:输出结果却发现,主线程竟然获取到了许可,并没有等到t1释放许可

T1 获取到许可
T2被打断,退出阻塞
main 获取到许可

原因很简单:问题就在于t2线程中的finally语句中代码semaphore.release();,,当线程t2被其他线程中断或者因自身原因出现异常的时候,它释放了原本不属于自己的许可证,导致在Semaphore内部的可用许可证计数器增多,其他线程才有机会获取到原本不该属于它的许可证。

可以在t2finally中加一行打印验证一下:

// 在开启一个线程t2
  Thread t2 = new Thread(() -> {
      // 获取许可
      try {
          semaphore.acquire();
      } catch (InterruptedException e) {
          System.out.println(Thread.currentThread().getName() + "被打断,退出阻塞");
      }finally {
          System.out.println(Thread.currentThread().getName() + "将要释放了许可");
          semaphore.release();
      }
  }, "T2");
  t2.start();

在运行,发现就是在打断t2的时候,触发了t2中的finally中的语句

T1 获取到许可
T2被打断,退出阻塞
T2将要释放了许可
main 获取到许可

如何修改呢:

// 在开启一个线程t2
Thread t2 = new Thread(() -> {
    // 获取许可
    try {
        semaphore.acquire();
    } catch (InterruptedException e) {
        System.out.println(Thread.currentThread().getName() + "被打断,退出阻塞");
        // 若出现异常则不再往下进行
        return;
    }

    try {
        // 程序运行到此处,说明已经成功获取了许可证,
        // 因此在finally语句块中对其进行释放就是理所当然的了
        System.out.println(Thread.currentThread().getName() + " 获取到许可");
    }finally {
        System.out.println(Thread.currentThread().getName() + "将要释放了许可");
        semaphore.release();
    }
}, "T2");
t2.start();

当线程t2被中断之后,它就无法再进行许可证的释放操作了,因此主线程也将不会再意外获取到许可证,这种方式是确保能够解决许可证被正确释放的思路之一

1.5.2 扩展Semaphore增强release
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import static java.lang.Thread.currentThread;

/**
 * @author wyaoyao
 * @date 2021/4/22 15:20
 */
public class MySemaphore extends Semaphore {
    /**
     * 定义线程安全的、存放Thread类型的队列
     */
    private final ConcurrentLinkedQueue<Thread> queue =
            new ConcurrentLinkedQueue<>();

    public MySemaphore(int permits) {
        super(permits);
    }

    public MySemaphore(int permits, boolean fair) {
        super(permits, fair);
    }

    /**
     * 重写acquire
     *
     * @throws InterruptedException
     */
    @Override
    public void acquire() throws InterruptedException {
        // 调用父类
        super.acquire();
        // 获取到许可,那就记录下来,线程成功获取许可证,将其放入队列中
        this.queue.add(currentThread());
    }

    @Override
    public boolean tryAcquire() {
        // 调用父类
        final boolean acquired = super.tryAcquire();
        if (acquired) {
            // 线程成功获取许可证,将其放入队列中
            this.queue.add(currentThread());
        }
        return acquired;
    }

    @Override
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
        // 调用父类
        final boolean acquired = super.tryAcquire(timeout, unit);
        if (acquired) {
            // 线程成功获取许可证,将其放入队列中
            this.queue.add(currentThread());
        }
        return acquired;
    }

    @Override
    public void release() {
        final Thread currentThread = currentThread();
        // 当队列中不存在该线程时,调用release方法将会被忽略
        if (!this.queue.contains(currentThread)) {
            return;
        }
        super.release();
        // 成功释放,并且将当前线程从队列中剔除
        this.queue.remove(currentThread);
    }

    @Override
    public void acquire(int permits) throws InterruptedException{
        super.acquire(permits);
        // 线程成功获取许可证,将其放入队列中
        this.queue.add(currentThread());
    }
    @Override
    public void acquireUninterruptibly(int permits){
        super.acquireUninterruptibly(permits);
        // 线程成功获取许可证,将其放入队列中
        this.queue.add(currentThread());
    }

    @Override
    public boolean tryAcquire(int permits) {
        boolean acquired = super.tryAcquire(permits);
        if (acquired) {
            // 线程成功获取许可证,将其放入队列中
            this.queue.add(currentThread());
        }
        return acquired;
    }
    @Override
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
            throws InterruptedException{
        boolean acquired = super.tryAcquire(permits, timeout, unit);
        if (acquired) {
            // 线程成功获取许可证,将其放入队列中
            this.queue.add(currentThread());
        }
        return acquired;
    }

    @Override
    public void release(int permits){
        final Thread currentThread = currentThread();
        // 当队列中不存在该线程时,调用release方法将会被忽略
        if (!this.queue.contains(currentThread)){
            return;
        }
        super.release(permits);
        // 成功释放,并且将当前线程从队列中剔除
        this.queue.remove(currentThread);
    }
}

MySemaphore类是扩展自Semaphore的一个子类,该类中有一个重要的队列,该队列为线程安全的队列,那么,为什么要使用线程安全的队列呢?因为对MySemaphore的操作是由多个线程进行的。该队列主要用于管理操作Semaphore的线程引用,成功获取到许可证的线程将会被加入该队列之中,同时只有在该队列中的线程才有资格进行许可证的释放动作。这样你就不用担心try…finally语句块的使用会引起没有获取到许可证的线程释放许可证的逻辑错误了。

注意:
通常情况下,我们扩展的Semaphore的确可以进行正确释放许可证的操作,但是仍然存在一些违规操作(无论是从语法还是API的调用上看都没问题,但是仍会导致出现错误)导致release错误的情况发生,比如下面的场景。
某线程获取了一个许可证,但是它在释放的过程中释放了多于一个数量的许可证,当然通常情况下我们不会编写这样错漏百出的代码。由于篇幅的原因,这里就不再进行进一步的扩充了,希望读者可以自己去完成这样一个功能。

1.6 其他方法

/**
对Semaphore许可证的争抢采用公平还是非公平的方式,
对应到内部的实现类为FairSync(公平)和NonfairSync(非公平)。
**/
boolean isFair();
/**
当前的Semaphore还有多少个可用的许可证。
**/
int availablePermits();
/**
排干Semaphore的所有许可证,
以后的线程将无法获取到许可证,已经获取到许可证的线程将不受影响。
**/
int drainPermits();
/**
当前是否有线程由于要获取Semaphore许可证而进入阻塞?(该值为预估值。)
**/
boolean hasQueuedThreads();
/**
如果有线程由于获取Semaphore许可证而进入阻塞,
那么它们的个数是多少呢?(该值为预估值。)
**/
int getQueueLength();

3 总结

虽然Semaphore可以控制多个线程对共享资源进行访问,但是对于共享资源的临界区以及线程安全性,Semaphore并不会提供任何保证。比如,你有5个线程想要同时操作某个资源,那么该资源的操作线程安全性则需要额外的实现。另外,如果采取尝试的方式也就是不阻塞的方式获取许可证,务必要做到对结果的判断,否则就会出现尝试失败但程序依然去执行对共享资源的操作,这样做的后果也是非常严重的。

你可能感兴趣的:(JUC-java并发包)