本节介绍如何使用 synchronized 关键字来控制对一个方法的并发访问。 只有一个执行线程将访问一个对象中被声明为 synchronized 的方法中的一个。 如果其他的线程要访问同一对象的任一声明为 synchronized 的方法,将被挂起直到第一个线程结束方法的执行。
也就是说,所有声明为 synchronized 的方法全体是一个临界区,Java只允许一个对象的一个临界区执行。
静态方法的行为不一样。只有一个执行线程将访问声明为 synchronized 的静态方法中的一个,但是其他线程可访问该类对象的其他非静态方法。 小心!因为2个线程能访问不同的 synchronized 方法,如果一个是静态另一个不是静态方法时。如果两个方法改变同一个数据,会有数据不一致的错误。
本节的示例代码在 com.elanzone.books.noteeg.chpt2.sect02 package中
数据类 (Account)
private double balance;
public synchronized void addAmount(double amount) { double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp += amount; balance = tmp; }
public synchronized void subtractAmount(double amount) { double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp -= amount; balance = tmp; }
线程类 Bank : 模拟 ATM 机不断减少账户余额
private Account account; public Bank(Account account) { this.account = account; }
for (int i = 0; i < 100; i++) { account.subtractAmount(1000); }
线程类 Company : 模拟公司不断增加账户余额
private Account account; public Company(Account account) { this.account = account; }
for (int i = 0; i < 100; i++) { account.addAmount(1000); }
控制类 (Main)
Account account = new Account(); account.setBalance(1000);
Company company = new Company(account); Thread companyThread = new Thread(company);
Bank bank = new Bank(account); Thread bankThread = new Thread(bank);
System.out.printf("Account : Initial Balance: %f\n", account.getBalance()); companyThread.start(); bankThread.start();
try { companyThread.join(); bankThread.join(); System.out.printf("Account : Final Balance: %f\n", account.getBalance()); } catch (InterruptedException e) { e.printStackTrace(); }
本例中,程序调用了 100 次 addAmount,每次增加 1000 账户余额;调用了 100 次 subtractAmount 方法每次减少 1000 账户余额。 可以预期最终和初始余额是一样的。
例子中还尝试模拟了一种错误场景。 它使用了 tmp 临时变量来保存账户余额,增加临时变量的值,然后再设置账户的余额。并且还用 sleep 方法使正在执行此方法的线程睡上 10 毫秒。 这样如果另外一个线程执行了此方法,它将修改账户的余额,从而引发错误。 是 synchronized 关键字机制避免了此错误。
如果您想看到并发访问共享数据的问题,可以删掉 addAmount 和 subtractAmount 方法的 synchronized 关键字再运行此程序。 如果没有 synchronized 关键字,当一个线程在读取了账户余额后睡眠时,另一个方法将读取账户余额,这样两个方法将修改同样的余额,其中一个操作将不会体现在最终结果中。
如果多次运行此程序,每次获得的结果可能会不一样。 JVM不保证线程的执行顺序。这样每次您允许时,线程将以不同的顺序读取、修改账户余额,这样最终结果将不一样。 您可以加上 synchronized 关键字再多跑几次,您将看到每次都会输出确定的结果。
使用 synchronized 关键字,能保证在并发应用中对共享数据正确地访问。
如本节中所提到的,只有一个线程能访问一个对象的使用 synchronized 关键字声明的方法。 如果一个线程(A)在执行一个 synchronized 方法,另一个线程(B)希望执行同一个对象的其他 synchronized 方法,将被阻塞直到线程(A)结束。 但是如果线程(B)访问的是同一个类的另一个对象,则相互之间没有影响,谁也不会被阻塞。
synchronized 关键字对应用程序的性能不利
只在并发环境中要修改共享数据的方法上使用它。
您可以递归调用 synchronized 方法
当线程能够访问一个对象的 synchronized 方法,它也能调用该对象的其他 synchronized 方法,包括在执行的方法本身。 它不必再次获得访问 synchronized 方法的权限。
可使用 synchronized 关键字来保护对一段代码块的访问,而不是整个方法。
使用此方法保护对共享数据的访问,让其余操作在此代码块外,能获得更好的性能。此方法的目标是让临界区尽可能的短。 此方法需要拿一个对象引用作为参数。只有一个线程能访问该对象的 synchronized 代码(代码块或方法)。通常引用的是执行此方法的对象。
synchronized (this) { // Java code }