Java的最基本的同步方式,即使用synchronized关键字来控制一个方法的并发访问。 每一个用synchronized关键字声明的方法都是临界区。在Java中,同一个对象的临界区,在同一时间只有一个允许被访问。
静态方法则有不同的行为。用synchronized关键字声明的静态方法,同时只能够被一个执行线程访问,但是其他线程可以访问这个对象的非静态的synchronized方法。必须非常谨慎这一点,因为两个线程可以同时访问一个对象的两个不同的synchronized方法,即其中一个是静态synchronized方法,另一个是非静态synchronized方法。如果两个方法都改变了相同的数据,将会出现数据不一致的错误。
先来看第一个示例,在java中,同一个对象的临界区,在同一时间只有一个允许被访问(都是非静态的synchronized方法):
package concurrency; public class Main8 { public static void main(String[] args) { 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 { //join()方法等待这两个线程运行完成 companyThread.join(); bankThread.join(); System.out.printf("Account : Final Balance: %f\n", account.getBalance()); } catch (InterruptedException e) { e.printStackTrace(); } } } /*帐户*/ class Account{ private double balance; /*将传入的数据加到余额balance中*/ public synchronized void addAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp += amount; balance = tmp; } /*将传入的数据从余额balance中扣除*/ public synchronized void subtractAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp -= amount; balance = tmp; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } } /*银行*/ class Bank implements Runnable{ private Account account; public Bank(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.subtractAmount(1000); } } } /*公司*/ class Company implements Runnable{ private Account account; public Company(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.addAmount(1000); } } }
你已经开发了一个银行账户的模拟应用,它能够对余额进行充值和扣除。这个程序通过调用100次addAmount()方法对帐户进行充值,每次存入1000;然后通过调用100次subtractAmount()方法对帐户余额进行扣除,每次扣除1000;我们期望帐户的最终余额与起初余额相等,我们通过synchronized关键字实现了。
如果想查看共享数据的并发访问问题,只需要将addAmount()和subtractAmount()方法声明中的synchronized关键字删除即可。在没有synchronized关键字的情况下,打印出来的余额值并不一致。如果多次运行这个程序,你将获取不同的结果。因为JVM并不保证线程的执行顺序,所以每次运行的时候,线程将以不同的顺序读取并且修改帐户的余额,造成最终结果也是不一样的。
一个对象的方法采用synchronized关键字进行声明,只能被一个线程访问。如果线程A正在执行一个同步方法syncMethodA(),线程B要执行这个对象的其他同步方法syncMethodB(),线程B将被阻塞直到线程A访问完。但如果线程B访问的是同一个类的不同对象,那么两个线程都不会被阻塞。
示例2,演示同一个对象上的静态synchronized方法与非静态synchronized方法可以在同一时间点被多个线程访问的问题,验证一下。
package concurrency; public class Main8 { public static void main(String[] args) { 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 { //join()方法等待这两个线程运行完成 companyThread.join(); bankThread.join(); System.out.printf("Account : Final Balance: %f\n", account.getBalance()); } catch (InterruptedException e) { e.printStackTrace(); } } } /*帐户*/ class Account{ /*这里改为静态变量*/ private static double balance = 0; /*将传入的数据加到余额balance中,注意是用static修饰过的*/ public static synchronized void addAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp += amount; balance = tmp; } /*将传入的数据从余额balance中扣除*/ public synchronized void subtractAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp -= amount; balance = tmp; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } } /*银行*/ class Bank implements Runnable{ private Account account; public Bank(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.subtractAmount(1000); } } } /*公司*/ class Company implements Runnable{ private Account account; public Company(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.addAmount(1000); } } }
我只是把上个例子中的,balance加了static关键字修改,addAmount()方法也可以static关键字修饰。执行结果大家可以自己测试一下,每次执行都是不一样的结果!
synchronized关键字会降低应用程序的性能,因此只能在并发情景中需要修改共享数据的方法上使用它。如果多个线程访问同一个synchronized方法,则只有一个线程可以访问,其他线程将等待。如果方法声明没有使用synchronized关键字,所有的线程都能在同一时间执行这个方法,因而总运行时间降低。如果已知一个方法不会被一个以上线程调用,则无需使用synchronized关键字声明之。
可以递归调用被synchronized声明的方法。当线程访问一个对象的同步方法时,它还可以调用这个对象的其他的同步方法,也包含正在执行的方法,而不必再次去获取这个方法的访问权。
我们可以通过synchronized关键字来保护代码块(而不是整个方法)的访问。应该这样利用synchronized关键字:方法的其余部分保持在synchronized代码块之外,以获取更好的性能。临界区(即同一时间只能被一个线程访问的代码块)的访问应该尽可能的短。例如在获取一幢楼人数的操作中,我们只使用synchronized关键字来保护对人数更新的指令,并让其他操作不使用共享数据。当这样使用synchronized关键字时,必须把对象引用作为传入参数。同一时间只有一个线程被允许访问这个synchronized代码。通常来说,我们使用this关键字来引用正在执行的方法所属的对象:
synchronized(this){ //Java code }