Semaphore 的细节及用法

Semaphore 是并发工具类,我们在一些需要并发的地方会用到它。之前也有文章提到它,当时么有探讨一些细节,给出两个例子: https://blog.csdn.net/Deaht_Huimie/article/details/105628723 。 这篇文章直接从后半部分开始看就行了。今天讲些细节,算是个复盘。


Semaphore semp = new Semaphore(1); 这行代码中,Semaphore 的许可数字是1,意思是在获得许可(acquire())后,同一时间只允许一个可用,其他的就需要等待,直到第一个释放 (release())才会执行。 假如说有三条子线程同时触发,如果使用了 semp, 那么第一条执行完了才会执行第二条,第二条结束了才会执行第三条,举个栗子

    private static void test102() {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(1);
        System.out.println("start:  " + getNowDate());
        // 模拟10个客户端访问
        for (int index = 0; index < 10; index++) {
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        // 获取许可
                        semp.acquire();
                        System.out.println("Accessing: " + NO + "   " + getNowDate());
                        //模拟实际业务逻辑
                        Thread.sleep((long) (2000));
                        // 访问完后,释放
                        semp.release();
                    } catch (InterruptedException e) {
                    }
                }
            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();

    }

    public static String getNowDate() {
        Date currentTime = new Date();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
        String dateString = formatter.format(currentTime);
        return dateString;
    }


    
打印日志,如下

start:  2020-11-27 19:59:50:497

Accessing: 0   2020-11-27 19:59:50:537
Accessing: 4   2020-11-27 19:59:52:538
Accessing: 8   2020-11-27 19:59:54:538
Accessing: 3   2020-11-27 19:59:56:538
Accessing: 7   2020-11-27 19:59:58:538
Accessing: 2   2020-11-27 20:00:00:538
Accessing: 6   2020-11-27 20:00:02:539
Accessing: 1   2020-11-27 20:00:04:539
Accessing: 5   2020-11-27 20:00:06:539
Accessing: 9   2020-11-27 20:00:08:540


我们许可数字设置的是1,在开启10个子线程时,能同时执行的也是一条,可以从 start 时间和打印的第一条时间做个对比,相差无几;for循环中打印的时间,相隔基本都是2秒。我们可以判断出,首条子线程执行时,semp.acquire() 做了个许可拦截,自己继续往下执行,其他九条子线程也来了,但是由于只有一个许可,其他9条只能一起等候了,2秒后,首条执行完了,触发 semp.release() ,此时就是说它自己已经执行完了,剩下的你们赶紧,接着索引为4的抢到了,继续这个过程。打个比方,0-9 这10个小朋友一起进了一个屋子,屋子里只有一个板凳,一个坐板凳的许可证,要求他们每个人都要坐一次板凳,每次时间为2分钟,每次只能坐一个人,小朋友必须拿到许可证,然后才能坐板凳,2分钟后离开板凳,同时把许可证重新放回原处,供其他小朋友来抢许可证,获取许可证的小朋友继续坐板凳,两分钟后起立,交回许可证,供其他剩下的小朋友抢许可证......  这样解释是不是好理解一点。 acquire() 和  release() 是成对出现的,相当于一个获取许可证,一个交还许可证。

上面的例子中,acquire() 和 release() 都是在同一个线程中,如果在不同线程中呢?把 acquire() 放到主线程中,修改如下

    private static void test103() {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(1);
        System.out.println("start:  " + getNowDate());
        // 模拟10个客户端访问
        for (int index = 0; index < 10; index++) {
            try {
                semp.acquire();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        System.out.println("Accessing: " + NO + "   " + getNowDate());
                        //模拟实际业务逻辑
                        Thread.sleep((long) (2000));
                        semp.release();
                    } catch (InterruptedException e) {
                    }
                }
            };
            exec.execute(run);
            
        }
        // 退出线程池
        exec.shutdown();

    }

打印日志

start:  2020-11-27 20:25:20:916
Accessing: 0   2020-11-27 20:25:20:967
Accessing: 1   2020-11-27 20:25:22:967
Accessing: 2   2020-11-27 20:25:24:968
Accessing: 3   2020-11-27 20:25:26:968
Accessing: 4   2020-11-27 20:25:28:968
Accessing: 5   2020-11-27 20:25:30:969
Accessing: 6   2020-11-27 20:25:32:969
Accessing: 7   2020-11-27 20:25:34:969
Accessing: 8   2020-11-27 20:25:36:969
Accessing: 9   2020-11-27 20:25:38:969

说明无影响。

现在说说 tryAcquire() 方法,它是 acquire() 和 release() 的集合体,有定时释放的功能,比如 tryAcquire(1500, TimeUnit.MILLISECONDS), 它意思是先执行  acquire() ,1.5秒后,执行 release() ,继续用上面的例子

    private static void test103() {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(1);
        System.out.println("start:  " + getNowDate());
        // 模拟10个客户端访问
        for (int index = 0; index < 10; index++) {
            try {
                semp.tryAcquire(1500, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        System.out.println("Accessing: " + NO + "   " + getNowDate());
                        //模拟实际业务逻辑
                        Thread.sleep((long) (2000));
                    } catch (InterruptedException e) {
                    }
                }
            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();

    }

打印日志

start:  2020-11-27 20:33:13:399
Accessing: 0   2020-11-27 20:33:13:423
Accessing: 1   2020-11-27 20:33:14:924
Accessing: 2   2020-11-27 20:33:16:425
Accessing: 3   2020-11-27 20:33:17:926
Accessing: 4   2020-11-27 20:33:19:426
Accessing: 5   2020-11-27 20:33:20:926
Accessing: 6   2020-11-27 20:33:22:428
Accessing: 7   2020-11-27 20:33:23:929
Accessing: 8   2020-11-27 20:33:25:430
Accessing: 9   2020-11-27 20:33:26:931


这里要注意, tryAcquire() 方法只能拦截同一个线程中的,就像上面,10个子线程是在同一个主线程执行,我们对主线程进行了许可和释放许可。如果我们把 tryAcquire() 移到 Runnable 中,诸位可以自己打印下日志看看。


tryAcquire() 是定时释放的,加入说里面设置的时间是2秒,但是在1秒就执行完了自己的逻辑,想释放许可让下一条线程进来怎么办?好说,执行 release() 方法即可。

private static void test103() {
        // 线程池
        ExecutorService exec = Executors.newCachedThreadPool();
        final Semaphore semp = new Semaphore(1);
        System.out.println("start:  " + getNowDate());
        // 模拟10个客户端访问
        for (int index = 0; index < 10; index++) {
            try {
                semp.tryAcquire(2000, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            final int NO = index;
            Runnable run = new Runnable() {
                public void run() {
                    try {
                        System.out.println("Accessing: " + NO + "   " + getNowDate());
                        //模拟实际业务逻辑
                        Thread.sleep((long) (1000));
                        semp.release();
                    } catch (InterruptedException e) {
                    }
                }
            };
            exec.execute(run);
        }
        // 退出线程池
        exec.shutdown();

    }

打印日志

start:  2020-11-27 20:45:08:792
Accessing: 0   2020-11-27 20:45:08:849
Accessing: 1   2020-11-27 20:45:09:849
Accessing: 2   2020-11-27 20:45:10:849
Accessing: 3   2020-11-27 20:45:11:850
Accessing: 4   2020-11-27 20:45:12:850
Accessing: 5   2020-11-27 20:45:13:850
Accessing: 6   2020-11-27 20:45:14:850
Accessing: 7   2020-11-27 20:45:15:851
Accessing: 8   2020-11-27 20:45:16:851
Accessing: 9   2020-11-27 20:45:17:851

看到这里,再看看文章开头链接对应的文章后半部分,此时再看估计就好理解多了。

你可能感兴趣的:(Android,知识,并发)