在本节中,学习如何使用Java中一个最基本的同步方法,使用synchronized关键字来控制并发使用一个方法或者代码块。所有synchronized语句(使用在方法或代码块上)使用对象引用。只有一个线程能够执行被相同对象引用保护的方法或者代码块。
当在方法中使用synchronized关键字时,对象引用是隐式的。当在对象的一个或多个方法上使用synchronized关键字时,只有一个执行线程可以访问所有方法。如果其它线程尝试访问声明为synchronized关键字的任何方法,此线程将被暂停直到第一个线程结束方法执行。换句话说,每个声明为synchronized关键字的方法都是一个临界区,Java只允许在同一时刻一个对象的一个临界区执行。这种情况下,own对象被用作对象引用,用this关键字来表达。静态方法则表现不同,只有一个执行线程能够访问其中一个声明为synchronized关键字的静态方法,但是不同的线程可以访问类中其他的非静态方法。请谨记这个关键点,因为两个线程可以访问两个不同的synchronized方法,如果其中一个是静态方法,而另一个是非静态方法。如果两个方法都是改变相同的数据,就会产生数据非一致性错误。这个时候,类对象被用作对象引用。
如果使用synchronized关键字保护代码块,必须给对象引用传递一个参数。通常使用this关键字引用执行方法的对象,但是也可以使用其它对象引用。正常情况下,这些对象专为此目的而创建的,需要保持这些用来同步的对象私有化。例如,在被多个线程共享的类中有两个独立的属性,就需要同步访问每一个变量。但是,如果在同一时刻一个线程访问一个属性,而另一个线程访问另一个属性,这也是对的。考虑到使用own对象(用this关键字来表达),可能需要其他的同步代码(之前提到的,this对象用来同步标记为synchronized关键字的方法)介入。
在本节中,将通过模拟一个停车场的车辆进出应用来学习如何使用synchronized关键字。模拟传感器检测过程如下:当汽车或者摩托车出入停车场时,用对象存储停放的机动车统计信息,以及控制现金流的机制。我们实现两个版本:一个不使用任何同步机制,将会看到如何获得不正确的结果;一个因为使用synchronized关键字的两种变化形式而正确运行。
准备工作
本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。
实现过程
通过如下步骤完成范例:
-
首先,实现未用任何同步机制的应用。创建名为ParkingCash的类,包含一个内部常数,以及一个存储停车费总额的变量:
public class ParkingCash { private static final int cost = 2; private long cash; public ParkingCash(){ cash = 0; }
-
实现名为vehiclePay()的方法,当机动车(汽车或摩托车)离开停车场时调用。此方法会增加cash变量值:
public void vehiclePay(){ cash += cost; }
-
最后,实现名为close()的方法,在控制台输出cash属性值,并将其初始化为0:
public void close() { System.out.printf("Closing accouting"); long totalAmmount; synchronized (this) { totalAmmount = cash; cash = 0; } System.out.printf("The total amount is : %d", totalAmmount); } }
-
创建名为ParkingStats的类,包含三个私有变量,以及初始化这些变量的构造函数:
public class ParkingStats { private long numberCars; private long numberMotorcycles; private ParkingCash cash; public ParkingStats(ParkingCash cash){ numberCars = 0; numberMotorcycles = 0; this.cash = cash; }
-
然后,实现用来执行当汽车或摩托车进出停车场时的方法。当机动车离开停车场时,cash属性值应当增加:
public void carComeIn(){ numberCars ++ ; } public void carGoOut(){ numberCars --; cash.vehiclePay(); } public void motorComeIn() { numberMotorcycles ++ ; } public void motorGoOut(){ numberMotorcycles -- ; cash.vehiclePay(); }
-
最后,实现两个方法,分别保存在停车场里的汽车和摩托车数量。
-
创建名为Sensor的类,用来模拟停车场里机动车的运动。实现Runnable接口和初始化一个ParkingStats属性的构造函数:
public class Sensor implements Runnable{ private ParkingStats stats; public Sensor(ParkingStats stats) { this.stats = stats; }
-
实现run()方法,模拟两辆汽车和一辆摩托车进入、然后离开停车场。每个传感器执行10次操作:
@Override public void run() { for(int i = 0; i < 10; i++){ stats.carComeIn(); stats.carComeIn(); try { TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } stats.motorComeIn(); try { TimeUnit.MILLISECONDS.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } stats.motorGoOut(); stats.carGoOut(); stats.carGoOut(); } }
-
最后,实现主方法。创建一个包含main()方法的Main类,需要ParkingCash对象和ParkingStats对象来管理停车过程:
public static void main(String[] args) { ParkingCash cash = new ParkingCash(); ParkingStats stats = new ParkingStats(cash); System.out.printf("Parking Simulator\n");
-
然后,创建Sensor任务。使用availableProcessors()方法(返回Java虚拟机的可用处理器数量,通常情况下等与处理器核心数量相同)来计算模拟的停车场传感器数量。创建对应的线程对象,并存储到队列中:
int numberSensors = 2 * Runtime.getRuntime().availableProcessors(); Thread threads[] = new Thread[numberSensors]; for(int i = 0 ; i < numberSensors ; i++){ Sensor sensor = new Sensor(stats); Thread thread = new Thread(sensor); thread.start(); threads[i] = thread; }
-
使用join()方法等待线程结束:
for(int i = 0 ; i < numberSensors ; i++){ try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } }
-
最后,输出停车统计信息:
System.out.printf("Number of cars: %d\n", stats.getNumberCars()); System.out.printf("Number of motorcycles: %d\n", stats.getNumberMotorcycles()); cash.close(); } }
如果使用四核处理器执行本范例的话,程序中就会有八个Sensor任务。每个任务循环运行10次,在每个循环中,三辆机动车进出停车场。所以每个Sensor任务将模拟30辆机动车进出。
如果一切正常,最终的统计结果将如下所示:
- 停车场里将没有车辆,也即是说所有进入停车场的机动车都已经离开了。
- 八个Sensor任务均被执行,每个任务模拟30辆机动车进出并且每辆机动车缴费2元,停车费总计480元。
每次执行此程序,都会得到不同的结果,并且绝大多数都是错误的。如下图所示:
因为存在竞争状态,所有线程访问不同的共享变量导致错误的结果。让我们使用同步关键字修改前述的代码来解决这些问题:
-
首先,在ParkingCash类中的vehiclePay()方法中加入同步关键字:
public synchronized void vehiclePay(){ cash += cost; }
-
然后,在close()方法中使用this关键字加入synchronized代码块:
public void close() { System.out.printf("Closing accouting"); long totalAmmount; synchronized (this) { totalAmmount = cash; cash = 0; } System.out.printf("The total amount is : %d", totalAmmount); }
-
ParkingStats类中加入两个新属性,使用构造函数初始化:
private final Object controlCars; private final Object controlMotorcycles; public ParkingStats(ParkingCash cash){ numberCars = 0; numberMotorcycles = 0; this.cash = cash; controlCars = new Object(); controlMotorcycles = new Object(); }
-
最后,修改增减汽车和摩托车数量的方法,加入synchronized关键字。controlCars对象将保护numberCars属性,controlMotorcycles对象保护numberMotorcycles属性。同时必须用相关联的引用对象同步getNumberCars()和getNumberMotorcycles()方法:
public void carComeIn(){ synchronized (controlCars) { numberCars ++ ; } } public void carGoOut(){ synchronized (controlCars) { numberCars --; } cash.vehiclePay(); } public void motorComeIn() { synchronized (controlMotorcycles) { numberMotorcycles ++ ; } } public void motorGoOut(){ synchronized (controlMotorcycles) { numberMotorcycles -- ; } cash.vehiclePay(); }
-
执行程序,与之前版本运行结果进行比较,查看不同点。
工作原理
下图是修改后的程序运行输出信息。无论运行多少次,都会得到正确的结果:
让我们查看synchronized关键字在本范例中的不同用法:
- 首先,保护vehiclePay()方法,如果两个以上的Sensor任务在同一时刻调用此方法,只有一个能够执行,其它任务排队等待。所以,最终的数量一直是正确的。
- 我们使用两个不同的对象控制访问汽车和摩托车计数器。也就是说,在同一时刻,一个Sensor任务能够修改numberCars属性,另一个Sensor任务能够修改numberMotorcycles属性。但是,同一时刻没有两个Sensor任务能够修改一个属性,所以计数器的最终数值一直是正确的。
最后,同步化getNumberCars()和getNumberMotorcycles()方法。在并发应用中使用synchronized关键字,能够确保正确访问共享数据。
如本章引言中提到的,只有一个线程可以访问使用synchronized关键字定义的对象的方法。同一个对象中,如果A线程正在运行一个synchronized方法,线程B想要执行另一个synchronized方法,线程B将会被阻塞,直到A运行结束。但是如果线程B访问同一个类中其它对象,两个线程均不会被阻塞。
当使用synchronized关键字保护代码块时,需要将对象参数化。Java虚拟机确保只有一个线程能够访问用此对象保护的所有代码块(切记我们讨论的是对象,不是类)。
TimeUnit是一个枚举类型的类,包含如下常量:DAYS、HOURS、MICROSECONDS、MILLISECONDS、MINUTES、NANOSECONDS、SECONDS,表明传递给休眠方法的时间单位。在本范例中,让线程休眠50毫秒。
扩展学习
synchronized关键字对程序性能是不利的,所以只有在并发环境中修改共享数据的方法中使用它。如果多个线程调用一个synchronized方法,同一时刻只有一个线程执行而其它保持等待。对于未使用synchronized关键字的操作,同一时刻所有线程均能够执行此操作,从而减少运行时间。。如果能够确定只有一个线程调用方法,不要使用synchronized关键字。无论如何,如果类被设计成多线程访问,返回结果就应当永远是正确的,所以必须更多的强调正确性而降低性能要求。同时,需要在方法和类中包含关联线程安全性的文档。
synchronized关键字允许递归调用。当线程使用对象中的synchronized方法时,可以调用对象的其它synchronized方法,包括正在被执行的方法,而无须再使用synchronized方法。
使用synchronized关键字,可以只保护一段代码块,而不用保护整个方法。通过这种方式使用synchronized关键字只保护访问共享数据的代码块,无须保护其它操作以获得更好的应用性能。目标是最短时间内进入临界区(同一时刻只有一个线程能够访问的代码块)。同时,在临界区内避免调用阻塞操作(例如,I/O操作)。在范例中,我们已经使用synchronized关键字来保护访问停车场里更新人数的指令,但块中离开的人数操作不需要使用共享数据。当以这种方式使用synchronized关键字时,必须将对象参数化,只有一个线程能够访问这个对象的synchronized代码(块或方法)。通常使用this关键字引用正在执行方法的对象:
更多关注
- 本章中“同步程序中使用状态”小节。