为了防止两个线程并发修改同一共享资源------java的多线程支持引入了同步监视器
基于同步监视器Java有两种方法可以实现线程同步:同步代码块和同步方法
同步代码块
使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(obj)
{
...
//此处的代码就是同步代码块
}
上面语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
注意:任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
通常推荐使用可能被并发访问的共享资源充当同步监视器。
假设我们创建一个银行账户,模拟两个线程对同一账户取钱。由于两个取钱线程可能会并发访问修改账户的余额balance成员变量。所以在该情形下,我们应该考虑使用账户(account)作为监视器。账户的取钱线程模拟代码如下:
public class DrawThread extends Thread //账户的取钱线程
{
//模拟用户账户
private Account account;
//当前取钱线程所希望取的钱数
private double drawAmount;
//构造器
public DrawThread(String name,Account account,double drawAmount)
{
super(name);
this.account=account;
this.drawAmount=drawAmount;
}
//当多个线程修改同一共享数据时,将涉及数据安全问题
public void run()
{
//使用account作为同步监视器,任何线程进入下面同步代码块之前
//必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
//这种做法符合:"加锁->修改->释放锁"的逻辑
synchronized(account)
{
//账户余额大于取钱数目
if(account.getBalance>=drawAmount)
{
//吐出钞票
System.out.println(getName()+"取钱成功!吐出钞票:"+drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance()-drawAmount);
System.out.println("\t余额为:"+account.getBalance());
}
else
{
System.out.println(getName()+"取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}
上面程序使用synchronized将run()方法里的方法体修改成同步代码块,该同步代码块的同步监视器是account对象,这样的做法符合"加锁->修改->释放锁"的逻辑,任何线程在修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成后,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区),所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全。
同步方法
与同步代码块对应的,Java的多线程安全支持还提供了同步方法,用synchronized修饰一个方法,就可以把一个普通方法变成同步方法。对于synchronized修饰的实例方法(非static方法)而言,无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
对于账户类Account而言,当两个线程同时修改Account对象的余额balance成员变量的值时,程序就会出现异常。下面将Account类对balance的访问设置成线程安全的,那么只要把修改balance的方法变成同步方法即可。演示代码如下。
public class Account
{
//封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
public Account(){}
//构造器
public Account(String accountNo,double balance)
{
this.accountNo=accountNo;
this.balance=balance;
}
//AccountNo的getter和setter方法
public String getAccountNo()
{
return accountNo;
}
public void setAccountNo(String accountNo)
{
this.accountNo=accountNo;
}
//因为账户余额不允许随意修改,所以只为balance提供getter方法
public double getBalance()
{
return balance;
}
//提供一个线程安全的draw()方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
//账户余额大于取钱数目
if(balance>=drawAmount)
{
//吐出钞票
System.out.println(Thread.currentThread().getName()+"取钱成功!吐出钞票:"+drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
balance-=drawAmount;
System.out.println("\t余额为:"+balance);
}
else
{
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}
}
上面代码提供了一个线程安全的Account类,Account类中提供了draw()方法,取消了setBalance()方法,所以在取钱线程中不需要再实现取钱逻辑,所以在其run()方法中只需要调用Account对象的draw()方法即可。run()方法代码如下:
public void run()
{
//直接调用account对象的draw()方法来执行取钱操作
//同步方法的同步监视器是this,this代表调用draw()方法的对象
//也就是说,线程进入draw()方法之前,必须先对account对象加锁
account.draw(drawAmount);
}
上面的DrawThread类无需自己实现取钱操作,而是直接调用account的draw()方法来执行取钱操作。由于当前Account类的draw()方法为同步方法,又同步方法的同步监视器是this,而this总代表调用该方法的实例——在上面示例中,调用draw()方法的实例是account,因此多个线程并发修改同一份account之前,必须先对account对象加锁。这也符合了"加锁->修改->释放锁"的逻辑。