整理同步类中的不相关属性

当使用 synchronized 关键字来保护一段代码时,必须传递一个对象引用作为参数。 一般是引用执行此方法的对象(this),但也能用其他的对象引用。一般这些对象将是为此目的而被专门创建。 例如如果您在1个类中有2个独立的属性被多个线程共享,您必须同步对每个变量的访问,但如果一个线程访问一个属性而另一个线程同时访问另一个属性也是没有问题的。

任务

实现以下场景:
一个电影院有2个放映厅和2个售票厅。售票厅卖的票是其中一个放映厅的票,但不会同时是两个放映厅的。

所以每个放映厅的空闲座位数是不相关的属性。

实现

本节的示例代码在 com.elanzone.books.noteeg.chpt2.sect03 package中

  • 数据类 (Cinema)

    • 2个long类型的属性表示2个放映厅的空位数
        private long vacanciesCinema1;
        private long vacanciesCinema2;
    
    • 增加2个Object类型的属性 (注意 final )
        private final Object controlCinema1;
        private final Object controlCinema2;
    
    • 在构造函数中初始化类的各属性
        public Cinema() {
            controlCinema1 = new Object();
            controlCinema2 = new Object();
    
            vacanciesCinema1 = 20;
            vacanciesCinema2 = 20;
        }
    
    • 实现 sellTickets1 方法为第1个放映厅售票。它使用 controlCinema1 对象来控制对此同步代码块的访问。
      作为控制对象,必须为final。不然这里会提示编译错误。
        public boolean sellTickets1(int number) {
            synchronized (controlCinema1) {
                if (number < vacanciesCinema1) {
                    vacanciesCinema1 -= number;
                    return true;
                } else {
                    return false;
                }
            }
        }
    
    • 类似以上实现 sellTickets2 方法为第2个放映厅售票
    • 实现 returnTickets1 方法为第1个放映厅退票
        public boolean returnTickets1(int number) {
            synchronized (controlCinema1) {
                vacanciesCinema1 += number;
                return true;
            }
        }
    
    • 类似以上实现 returnTickets2 方法为第2个放映厅退票
    • 实现 vacanciesCinema1 和 vacanciesCinema2 变量的 getter 方法
  • 线程类: 售票厅1 (TicketOffice1)

    • Cinema 类型属性 cinema 及构造函数
        private Cinema cinema;
    
        public TicketOffice1(Cinema cinema) {
            this.cinema = cinema;
        }
    
    • run 方法 : 模拟一些售票、退票操作
            cinema.sellTickets1(3);
            cinema.sellTickets1(2);
            cinema.sellTickets2(2);
            cinema.returnTickets1(3);
            cinema.sellTickets1(5);
            cinema.sellTickets2(2);
            cinema.sellTickets2(2);
            cinema.sellTickets2(2);
    
  • 线程类: 售票厅2 (TicketOffice2)

    • 类似 TicketOffice1 增加 Cinema 类型属性 cinema 及构造函数
    • run方法
            cinema.sellTickets2(2);
            cinema.sellTickets2(4);
            cinema.sellTickets1(2);
            cinema.sellTickets1(1);
            cinema.returnTickets2(2);
            cinema.sellTickets1(3);
            cinema.sellTickets2(2);
            cinema.sellTickets1(2);
    
  • 控制类 (Main)

    • 创建数据类 Cinema
        Cinema cinema = new Cinema();
    
    • 创建 TicketOffice1 和 TicketOffice2 对象及对应的线程
        TicketOffice1 office1 = new TicketOffice1(cinema);
        Thread thread1 = new Thread(office1, "TicketOffice1");
    
        TicketOffice2 office2 = new TicketOffice2(cinema);
        Thread thread2 = new Thread(office2, "TicketOffice2");
    
    • 启动线程后等待线程结束
        thread1.start();
        thread2.start();
    
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
    • 输出各放映厅的空位信息
        System.out.printf("Room 1 Vacancies: %d\n",cinema.getVacanciesCinema1());
        System.out.printf("Room 2 Vacancies: %d\n",cinema.getVacanciesCinema2());
    

工作原理

当使用 synchronized 关键字来保护一个代码块时,使用的参数是一个对象。 JVM保证只有一个线程能访问用此对象保护的所有代码块。 (注意是对象,不是类)

在本例中,有一个对象控制对 vacanciesCinema1 属性的访问,所以每次只有一个线程能修改这个属性; 另一个对象控制对 vacanciesCinema2 属性的访问,所以每次只有一个线程能修改它。 但是可能有2个线程同时运行,1个修改 vacanciesCinema1 属性,另一个修改 vacanciesCinema2 属性。

了解更多

synchronize 关键字还有其他重要的用途,且待后续分解。