同步方法

Java 9并发编程指南 目录

nchronized语句(使用在方法或代码块上)使用对象引用。只有一个线程能够执行被相同对象引用保护的方法或者代码块。

当在方法中使用synchronized关键字时,对象引用是隐式的。当在对象的一个或多个方法上使用synchronized关键字时,只有一个执行线程可以访问所有方法。如果其它线程尝试访问声明为synchronized关键字的任何方法,此线程将被暂停直到第一个线程结束方法执行。换句话说,每个声明为synchronized关键字的方法都是一个临界区,Java只允许在同一时刻一个对象的一个临界区执行。这种情况下,own对象被用作对象引用,用this关键字来表达。静态方法则表现不同,只有一个执行线程能够访问其中一个声明为synchronized关键字的静态方法,但是不同的线程可以访问类中其他的非静态方法。请谨记这个关键点,因为两个线程可以访问两个不同的synchronized方法,如果其中一个是静态方法,而另一个是非静态方法。如果两个方法都是改变相同的数据,就会产生数据非一致性错误。这个时候,类对象被用作对象引用。

如果使用synchronized关键字保护代码块,必须给对象引用传递一个参数。通常使用this关键字引用执行方法的对象,但是也可以使用其它对象引用。正常情况下,这些对象专为此目的而创建的,需要保持这些用来同步的对象私有化。例如,在被多个线程共享的类中有两个独立的属性,就需要同步访问每一个变量。但是,如果在同一时刻一个线程访问一个属性,而另一个线程访问另一个属性,这也是对的。考虑到使用own对象(用this关键字来表达),可能需要其他的同步代码(之前提到的,this对象用来同步标记为synchronized关键字的方法)介入。

在本节中,将通过模拟一个停车场的车辆进出应用来学习如何使用synchronized关键字。模拟传感器检测过程如下:当汽车或者摩托车出入停车场时,用对象存储停放的机动车统计信息,以及控制现金流的机制。我们实现两个版本:一个不使用任何同步机制,将会看到如何获得不正确的结果;一个因为使用synchronized关键字的两种变化形式而正确运行。

准备工作

本范例通过Eclipse开发工具实现。如果使用诸如NetBeans的开发工具,打开并创建一个新的Java项目。

实现过程

通过如下步骤完成范例:

  1. 首先,实现未用任何同步机制的应用。创建名为ParkingCash的类,包含一个内部常数,以及一个存储停车费总额的变量:

    public class ParkingCash {
    	private static final int cost = 2;
    	private long cash;
    	
    	public ParkingCash(){
    		cash = 0;
    	}
    
  2. 实现名为vehiclePay()的方法,当机动车(汽车或摩托车)离开停车场时调用。此方法会增加cash变量值:

    	public void vehiclePay(){
    		cash += cost;
    	}
    
  3. 最后,实现名为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);
    	}
    }
    
  4. 创建名为ParkingStats的类,包含三个私有变量,以及初始化这些变量的构造函数:

    public class ParkingStats {
    
    	private long numberCars;
    	private long numberMotorcycles;
    	private ParkingCash cash;
    	
    	public ParkingStats(ParkingCash cash){
    		numberCars = 0;
    		numberMotorcycles = 0;
    		this.cash = cash;
    	}
    
  5. 然后,实现用来执行当汽车或摩托车进出停车场时的方法。当机动车离开停车场时,cash属性值应当增加:

    	public void carComeIn(){
    		numberCars ++ ;
    	}
    
    	public void carGoOut(){
    		numberCars --;
    		cash.vehiclePay();
    	}
    	
    	public void motorComeIn() {
    		numberMotorcycles ++ ;
    	}
    	
    	public void motorGoOut(){
    		numberMotorcycles -- ;
    		cash.vehiclePay();
    	}
    
  6. 最后,实现两个方法,分别保存在停车场里的汽车和摩托车数量。

  7. 创建名为Sensor的类,用来模拟停车场里机动车的运动。实现Runnable接口和初始化一个ParkingStats属性的构造函数:

    public class Sensor implements Runnable{
    	private ParkingStats stats;
    	public Sensor(ParkingStats stats) {
    		this.stats = stats;
    	}
    
  8. 实现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();
    		}
    	}
    
  9. 最后,实现主方法。创建一个包含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");
    
  10. 然后,创建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;
              }
    
  11. 使用join()方法等待线程结束:

    		for(int i = 0 ; i < numberSensors ; i++){
    			try {
    				threads[i].join();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    
  12. 最后,输出停车统计信息:

    		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元。

每次执行此程序,都会得到不同的结果,并且绝大多数都是错误的。如下图所示:

pics/02_01.jpg

因为存在竞争状态,所有线程访问不同的共享变量导致错误的结果。让我们使用同步关键字修改前述的代码来解决这些问题:

  1. 首先,在ParkingCash类中的vehiclePay()方法中加入同步关键字:

    	public synchronized void vehiclePay(){
    		cash += cost;
    	}
    
  2. 然后,在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);
    	}
    
  3. 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();
    	}
    
  4. 最后,修改增减汽车和摩托车数量的方法,加入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();
    	}
    
  5. 执行程序,与之前版本运行结果进行比较,查看不同点。

工作原理

下图是修改后的程序运行输出信息。无论运行多少次,都会得到正确的结果:

pics/02_02.jpg

让我们查看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关键字引用正在执行方法的对象:

更多关注

  • 本章中“同步程序中使用状态”小节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值