控制对一个资源的并发访问

信号灯是一个控制对一个或多个共享资源的访问的计数器。 当一个线程想访问共享资源之一时:

  1. 首先必须获得信号
    • 如果信号灯的内部计数器 > 0,信号灯计数器减1并允许对共享资源的访问。
      • 计数器大于 0 表示有空闲资源可用,这样线程能访问并使用其中之一。
    • 否则(计数器 = 0),信号灯让线程睡眠直到计数器 > 0
      • 计数器为 0 表示共享资源被其他线程用光了,所以要用的线程必须等待其中之一空闲。
  2. 当线程结束对共享资源的使用,必须释放信号,让其他的线程能访问共享资源
    • 此操作增加信号灯的内部计数器

任务

  • 目标 : 学会使用 Semaphore 类来实现一种名为二元信号的特殊信号灯
    • 二元信号灯保护对数量为一的共享资源的访问,其内部计数器只能有0或1两个值。
  • 内容 : 实现一个打印队列能被并发任务用来打印。
    • 此打印队列将由一个二元信号灯保护,所以同一时间只有一个线程能打印

实现

  • 数据类 : PrintQueue

    • 声明 Semaphore 对象属性 semaphore 并在构造函数中初始化 semaphore 只允许 1 个线程同时访问共享资源
        private Semaphore semaphore;
    
        public PrintQueue() {
            semaphore = new Semaphore(1);
        }
    
    • printJob 方法
      1. 调用 Semaphore 的 acquire 方法获得信号
      2. 随机睡上一段时间模拟打印
      3. 最终调用 Semaphore 的 release 方法释放信号
        public void printJob(Object document) {
            try {
                semaphore.acquire();
    
                long duration=(long)(Math.random()*10);
                System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",
                        Thread.currentThread().getName(),duration);
                Thread.sleep(duration);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    
  • 线程类 : Job

    • 声明 PrintQueue 对象属性 printQueue 并在构造函数中初始化为传入的参数
        private PrintQueue printQueue;
    
        public Job(PrintQueue printQueue) {
            this.printQueue = printQueue;
        }
    
    • run 方法: 调用 printQueue 的 printJob 方法, 在调用之前和之后输出调试信息
            System.out.printf("%s: Going to print a job\n",Thread.currentThread().getName());
            printQueue.printJob(new Object());
            System.out.printf("%s: The document has been printed\n",Thread.currentThread().getName());
    
  • 控制类 : Main

    1. 声明 PrintQueue 对象
    2. 声明多个 Job 对象及对应的线程
    3. 启动各线程
            PrintQueue printQueue = new PrintQueue();
    
            Thread[] threads = new Thread[10];
            for (int i=0; i<10; i++) {
                threads[i] = new Thread(new Job(printQueue), "Thread" + i);
            }
    
            for (int i=0; i<10; i++) {
                threads[i].start();
            }
    

工作原理

本例的关键在于 PrintQueue 类的 printJob 方法。此方法展示了使用信号灯来实现临界区并保护对一个共享资源的保护时,必须要做的:

  1. 调用 Semaphore 的 acquire() 方法获得信号
  2. 使用共享资源完成要做的事情
  3. 最终调用 Semaphore 的 release() 方法释放信号

另一个重点在于 PrintQueue 类的构造函数内对 Semaphore 对象的初始化。 给 Semaphore 的构造函数传参 1 ,创建了一个二元信号灯。内部计数器的初识值为 1,这样可以保护对一个共享资源(在本例中是打印队列)的访问。

当启动了 10 个线程,第 1 个获得了信号能访问临界区。其他的被信号灯阻塞直到获得信号的线程释放信号。 当信号被释放,信号灯选择等待线程中的一个赋予它访问临界区的权限。 所有线程都会打印它们的文件,但是是一个接一个的。

了解更多

Semaphore 类另有 2 个版本的 acquire() 方法:

  • acquireUninterruptibly():
    • acquire(): 当内部计数器的值为0,阻塞线程直到信号被释放。 在阻塞期间,线程可以被中断,acquire() 方法抛出一个 InterruptedException 异常
    • acquireUninterruptibly(): 在阻塞期间忽略线程的中断,不抛出任何异常
  • tryAcquire(): 尝试获得信号
    • 如果可获得,返回 true
    • 如果不可获得,返回 false,而不会被阻塞并等待信号的释放。
    • 程序员有责任根据返回值做适当的处理

信号灯内的公平

Java 的有多个线程被阻塞等待某同步资源(如信号)的释放的所有类中,都用到 公平 的概念。

  • 缺省模式被称为 不公平 模式 : 此模式中,当同步资源被释放,等待中的线程之一被选择获得此资源,但是选择是没有任何依据的。
  • 公平模式: 选择等待最长时间的线程

Semaphore 类的构造函数内允许传递第2个参数,此参数必须为 Boolean 值。

  • false 或不使用此参数 : 创建的信号灯处于 不公平 模式
  • true : 创建的信号灯处于 公平 模式