• java并发编程学习:用 Semaphore (信号量)控制并发资源


    并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章。

    今天先来学习Semaphore(信号量),字面上看,根本不知道这东西是干啥的,借用 并发工具类(三)控制并发线程数的Semaphore一文中的交通红绿信号灯的例子来理解一下:

    一条4车道的主干道,假设100米长,每辆车假设占用的长度为10米(考虑到前后车距),也就是说这条道上满负载运行的话,最多只能容纳4*(100/10)=40辆车,如果有120辆车要通过的话(为简单起见,一波40辆,分成3波),就必须要红绿信号灯来调度了,对于最前面的一波来讲,它们看到的是绿灯,允许通过,第一波全进入道路后,红绿灯变成红色,表示后面的2波,要停下来等候第1波车辆全通过,然后红绿灯才会变成绿色,让第2波通过,如此运转下去....

    这跟多线程并发有啥关系呢?Semaphore就是红绿信号灯,3波车辆就是3个并发的线程,而主干道就是多个线程要并发访问的公用资源,由于资源有限,所以必须通过Semaphore来控制线程对资源的访问,否则就变成资源竞争,严重的话会导致死锁等问题。

    下面用一个示例演示,假设有N个并发线程都要打印文件,但是打印机只有1台,先来一个打印队列类:

    package yjmyzz.lesson01;
    
    import java.util.concurrent.Semaphore;
    
    public class PrintQueue {
    
        private final Semaphore semaphore;
    
        public PrintQueue() {
            semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
        }
    
        public void printJob(Object document) {
            try {
                semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))
    
                long duration = (long) (1 + Math.random() * 10);
                System.out.printf("%s: PrintQueue: Printing a Job during %d seconds
    ", Thread.currentThread().getName(), duration);
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
            }
        }
    }
    

    由于是在多线程环境中,真正运行的作业处理,得继承自Runnable(或Callable)

    package yjmyzz.lesson01;
    
    public class Job implements Runnable {
    
        private PrintQueue printQueue;
    
        public Job(PrintQueue printQueue) {
            this.printQueue = printQueue;
        }
    
        public void run() {
            System.out.printf("%s: Going to print a job
    ", Thread.currentThread().getName());
            printQueue.printJob(new Object());
            System.out.printf("%s: The document has been printed
    ", Thread.currentThread().getName());
        }
    }
    

    好了,测试一把:

    package yjmyzz.lesson01;
    
    public class Main {
        public static void main(String args[]) {
    
            PrintQueue printQueue = new PrintQueue();
    
            int threadCount = 3;
    
            Thread thread[] = new Thread[threadCount];
            for (int i = 0; i < threadCount; i++) {
                thread[i] = new Thread(new Job(printQueue), "Thread" + i);
            }
    
            for (int i = 0; i < threadCount; i++) {
                thread[i].start();
            }
        }
    }
    

    输出:

    Thread0: Going to print a job
    Thread2: Going to print a job
    Thread1: Going to print a job
    Thread0: PrintQueue: Printing a Job during 7 seconds
    Thread0: The document has been printed
    Thread2: PrintQueue: Printing a Job during 5 seconds
    Thread2: The document has been printed
    Thread1: PrintQueue: Printing a Job during 1 seconds
    Thread1: The document has been printed

    从输出上看,线程0打印完成后,线程2才开始打印,然后才是线程1,没有出现一哄而上,抢占打印机的情况。这样可能没啥感觉,我们把PrintQueue如果去掉Semaphore的部分,变成下面这样:

    package yjmyzz.lesson01;
    
    public class PrintQueue {
    
        //private final Semaphore semaphore;
    
        public PrintQueue() {
            //semaphore = new Semaphore(1);//限定了共享资源只能有1个(相当于只有一把钥匙)
        }
    
        public void printJob(Object document) {
            try {
                //semaphore.acquire();//取得对共享资源的访问权(即拿到了钥匙))
                long duration = (long) (1 + Math.random() * 10);
                System.out.printf("%s: PrintQueue: Printing a Job during %d seconds
    ", Thread.currentThread().getName(), duration);
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //semaphore.release();//钥匙用完了,要还回去,这样其它线程才能继续有序的拿到钥匙,访问资源
            }
        }
    }
    

    这回的输出:

    Thread0: Going to print a job
    Thread2: Going to print a job
    Thread1: Going to print a job
    Thread2: PrintQueue: Printing a Job during 4 seconds
    Thread1: PrintQueue: Printing a Job during 8 seconds
    Thread0: PrintQueue: Printing a Job during 1 seconds
    Thread0: The document has been printed
    Thread2: The document has been printed
    Thread1: The document has been printed

    可以发现,3个线程全都一拥而上,同时开始打印,也不管打印机是否空闲,实际应用中,这样必然出问题。

    好的,继续,突然有一天,公司有钱了,又买了2台打印机,这样就有3台打印机了,这时候怎么办呢?简单的把PrintQueue构造器中的

        public PrintQueue() {
            semaphore = new Semaphore(3);
        }
    

    就行了吗?仔细想想,就会发现问题,代码中并没有哪里能告诉线程哪个打印机正在打印,哪个打印机当前空闲,所以仍然有可能出现N个线程(N<=3)同时抢一台打印机的情况(即:如果把控制权当成钥匙的话,相当于有可能3个人各领取到了1把钥匙,但是这3把钥匙是相同的,3个人都看中了同一个箱子,都要用手中的钥匙去抢着开箱)。

    所以得改进一下:

    package yjmyzz.lesson02;
    
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class PrintQueue {
    
        private boolean freePrinters[];//用来存放打印机的状态,true表示空闲,false表示正在打印
    
        private Lock lockPrinters;//增加了锁,保证多个线程,只能获取得锁,才能查询哪台打印机空闲的
    
        private final Semaphore semaphore;
    
    
        public PrintQueue() {
            int printerNum = 3;//假设有3台打印机
            semaphore = new Semaphore(printerNum);
            freePrinters = new boolean[printerNum];
    
            for (int i = 0; i < printerNum; i++) {
                freePrinters[i] = true;//初始化时,默认所有打印机都空闲
            }
            lockPrinters = new ReentrantLock();
        }
    
    
        private int getPrinter() {
            int ret = -1;
            try {
                lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
                for (int i = 0; i < freePrinters.length; i++) {
                    //遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
                    // 并设置该打印机为繁忙状态(因为马上就要用它)
                    if (freePrinters[i]) {
                        ret = i;
                        freePrinters[i] = false;
                        break;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //最后别忘记了解锁,这样后面的线程才能上来领号
                lockPrinters.unlock();
            }
            return ret;
        }
    
        public void printJob(Object document) {
            try {
                semaphore.acquire();
    
                int assignedPrinter = getPrinter();//领号
                long duration = (long) (1 + Math.random() * 10);
                System.out.printf("%s: PrintQueue: Printing a Job in Printer%d during %d seconds
    ", Thread.currentThread().getName(),
                        assignedPrinter, duration);
                Thread.sleep(duration);
                freePrinters[assignedPrinter] = true;//打印完以后,将该打印机重新恢复为空闲状态
    
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }
    

    测试一下,这回把线程数增加到5,输出结果类似下面这样:

    Thread0: Going to print a job
    Thread4: Going to print a job
    Thread3: Going to print a job
    Thread2: Going to print a job
    Thread1: Going to print a job
    Thread4: PrintQueue: Printing a Job in Printer1 during 7 seconds
    Thread0: PrintQueue: Printing a Job in Printer0 during 4 seconds
    Thread3: PrintQueue: Printing a Job in Printer2 during 8 seconds
    Thread0: The document has been printed
    Thread2: PrintQueue: Printing a Job in Printer0 during 1 seconds
    Thread2: The document has been printed
    Thread4: The document has been printed
    Thread1: PrintQueue: Printing a Job in Printer0 during 1 seconds
    Thread3: The document has been printed
    Thread1: The document has been printed

    从输出结果可以看出,一次最多只能有3个线程使用这3台打印机,而且每个线程使用的打印机互不冲突,打印完成后,空闲的打印机会给其它线程继续使用,继续折腾,如果把getPrinter()中加锁的部分去掉,即:

        private int getPrinter() {
            int ret = -1;
            try {
                //lockPrinters.lock();//先加锁,保证1次只能有1个线程来获取空闲的打印机
                for (int i = 0; i < freePrinters.length; i++) {
                    //遍历所有打印机的状态,发现有第1个空闲的打印机后,领取号码,
                    // 并设置该打印机为繁忙状态(因为马上就要用它)
                    if (freePrinters[i]) {
                        ret = i;
                        freePrinters[i] = false;
                        break;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                //最后别忘记了解锁,这样后面的线程才能上来领号
                //lockPrinters.unlock();
            }
            return ret;
        }
    

    再跑一下,结果如何,为了放大冲突,这回开到15个线程来抢3台打印机,输出如下:

    Thread0: Going to print a job
    Thread14: Going to print a job
    Thread13: Going to print a job
    Thread12: Going to print a job
    Thread11: Going to print a job
    Thread10: Going to print a job
    Thread9: Going to print a job
    Thread8: Going to print a job
    Thread7: Going to print a job
    Thread6: Going to print a job
    Thread5: Going to print a job
    Thread4: Going to print a job
    Thread3: Going to print a job
    Thread2: Going to print a job
    Thread1: Going to print a job
    Thread0: PrintQueue: Printing a Job in Printer0 during 29 seconds
    Thread14: PrintQueue: Printing a Job in Printer0 during 92 seconds
    Thread13: PrintQueue: Printing a Job in Printer1 during 66 seconds
    Thread0: The document has been printed
    Thread12: PrintQueue: Printing a Job in Printer0 during 86 seconds
    Thread13: The document has been printed
    Thread11: PrintQueue: Printing a Job in Printer1 during 1 seconds
    Thread11: The document has been printed
    Thread10: PrintQueue: Printing a Job in Printer1 during 58 seconds
    Thread14: The document has been printed
    Thread9: PrintQueue: Printing a Job in Printer0 during 92 seconds
    Thread12: The document has been printed
    Thread8: PrintQueue: Printing a Job in Printer0 during 59 seconds
    Thread10: The document has been printed
    Thread7: PrintQueue: Printing a Job in Printer1 during 51 seconds
    Thread8: The document has been printed
    Thread6: PrintQueue: Printing a Job in Printer0 during 33 seconds
    Thread7: The document has been printed
    Thread5: PrintQueue: Printing a Job in Printer1 during 2 seconds
    Thread9: The document has been printed
    Thread3: PrintQueue: Printing a Job in Printer1 during 85 seconds
    Thread4: PrintQueue: Printing a Job in Printer0 during 61 seconds
    Thread5: The document has been printed
    Thread6: The document has been printed
    Thread2: PrintQueue: Printing a Job in Printer0 during 66 seconds
    Thread4: The document has been printed
    Thread1: PrintQueue: Printing a Job in Printer0 during 9 seconds
    Thread1: The document has been printed
    Thread3: The document has been printed
    Thread2: The document has been printed

    注意红色的部分:Thread0与Thread14同时分配到了Printer0上了,出现了多个线程同时抢一个资源的情况。


    参考文章:

    http://ifeve.com/thread-synchronization-utilities-2/

    http://ifeve.com/thread-synchronization-utilities-3/

    http://ifeve.com/concurrency-semaphore/

  • 相关阅读:
    批量重命令文件名称的几种方式
    SecureCRT常用快捷键
    ipv4地址从最后一位按步长递增
    win32
    GDI根据位图和透明度创建蒙版
    git bash的一些使用
    win32
    win32
    win32
    QT
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/java-semaphore-sample.html
Copyright © 2020-2023  润新知