zoukankan      html  css  js  c++  java
  • 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/

  • 相关阅读:
    Add Two Numbers
    Reverse Linked List II
    Reverse Linked List
    Remove Duplicates from Sorted List
    Remove Duplicates from Sorted List II
    Partition List
    Intersection of Two Linked Lists
    4Sum
    3Sum
    2Sum
  • 原文地址:https://www.cnblogs.com/yjmyzz/p/java-semaphore-sample.html
Copyright © 2011-2022 走看看