线程同步是为了让多个线程在共享数据时,保持数据的一致性。举个例子,有两个人同时取钱,假设用户账户余额是1000,第一个用户取钱800,在第一个用户取钱的同时,第二个用户取钱600。银行规定,用户不允许透支,当余额不足时,应该取钱失败。我们先来看一下,如果线程不同步,会出现什么情况。代码如下:
public class SynchronizeApp { /** * @param args */ public static void main(String[] args) { // 获得账户 Account account = new Account(); account.setCardNo("95559"); account.setBalance(1000); // 用户1取款800 DrawMoney user1 = new DrawMoney(account, 800); // 用户1取款600 DrawMoney user2 = new DrawMoney(account, 600); user1.start(); user2.start(); } } class DrawMoney extends Thread { private Account account; private double amount; public DrawMoney(Account account, double amount) { this.account = account; this.amount = amount; } @Override public void run() { account.draw(amount); } } class Account { private String cardNo; private double balance; public String getCardNo() { return cardNo; } public void setCardNo(String cardNo) { this.cardNo = cardNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } /** * 用户取款 * * @param amount * ,取款数量 */ public void draw(double amount) { if (amount > balance) { System.out.println("金额不足!"); } else { try { // 模拟取款过程 Thread.sleep(100); balance = balance - amount; System.out.println("成功取款" + amount + "元, 最新余额为" + balance); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果如下:
可见,如果没有线程同步,当两个线程同时取款时,就会出现数据错误。第二个线程取款时,读取到的账户余额是1000,所以可以执行取款操作,但是进行实际取款时,账户余额被第一个线程修改,实际余额是200,所以取出600后最新余额是-400,同样第一个用户也出现了数据错误,余额是1000,取款800后却变成了-400。这种情况是不允许的。
线程同步有两种方式,第一种是用synchronized关键字,第二种是用lock对象。
使用synchronized关键字可以对方法和代码块进行同步,使用synchronized关键字对方法进行同步时,将synchronized关键字放在方法返回类型前面,synchronized自动锁定当前对象。上边取款操使用synchronized关键字同步方法的代码如下:
class Account { private String cardNo; private double balance; public String getCardNo() { return cardNo; } public void setCardNo(String cardNo) { this.cardNo = cardNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } /** * 用户取款 * * @param amount * ,取款数量 */ public synchronized void draw(double amount) { if (amount > balance) { System.out.println("金额不足!"); } else { try { // 模拟取款过程 Thread.sleep(100); balance = balance - amount; System.out.println("成功取款" + amount + "元, 最新余额为" + balance); } catch (InterruptedException e) { e.printStackTrace(); } } } }
再次运行程序,得到如下结果:
使用synchronized同步代码块的代码如下:
/** * 用户取款 * * @param amount * ,取款数量 */ public void draw(double amount) { synchronized (this) { if (amount > balance) { System.out.println("金额不足!"); } else { try { // 模拟取款过程 Thread.sleep(100); balance = balance - amount; System.out.println("成功取款" + amount + "元, 最新余额为" + balance); } catch (InterruptedException e) { e.printStackTrace(); } } } }
还可以使用同步锁来对代码进行同步。使用同步锁时,先调用Lock对象的lock方法,代码执行完毕后,再调用Lock对象的unlock方法。在lock和unlock之间的代码是同步的,同一时间段内只能有一个线程能访问。为了保证能释放锁,把unlock方法放在finally语句块中是比较安全的,代码如下:
private final ReentrantLock lock = new ReentrantLock(); /** * 用户取款 * * @param amount * ,取款数量 */ public void draw(double amount) { lock.lock(); try { if (amount > balance) { System.out.println("金额不足!"); } else { // 模拟取款过程 Thread.sleep(100); balance = balance - amount; System.out.println("成功取款" + amount + "元, 最新余额为" + balance); } } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }
运行代码,结果和使用synchronized同步方法的的运行结果一样。