-- Start
当多个线程对同一个对象进行读写操作时将引发并发问题, 学过数据库的人都知道并发将导致脏读和丢失更新等错误, 本文将介绍 Java 是如何解决并发问题的.
同步方法(synchronized)
解决并发问题的方法之一是将并发读写的操作放到一个同步方法中, 下面是一个简单的例子.
public class Test {
public static void main(String[] args) throws Exception {
Bank icbc = new Bank(); // 工商银行
Account account = new Account(icbc, 1); // 在工商银行开户, 并存入 1 块钱
// 路人甲在 ATM 1 给我转帐, 每次转帐 1 块钱, 连续转帐 10 次
new Thread(new ATM(account, 1, 10), "ATM 1").start();
// 路人乙在 ATM 2 给我转帐, 每次转帐 2 块钱, 连续转帐 5 次
new Thread(new ATM(account, 2, 5), "ATM 2").start();
// 我从 ATM 3 开始取钱, 每次取 3块, 连续取5次
new Thread(new ATM(account, -3, 5), "ATM 3").start();
// 路人丙在 ATM 4 给我转帐, 每次转帐 4 块钱, 连续转帐 5 次
new Thread(new ATM(account, 4, 5), "ATM 4").start();
// 我老婆从 ATM 5 开始取钱, 每次取5块, 连续取7次
new Thread(new ATM(account, -5, 5), "ATM 5").start();
}
}
class ATM implements Runnable {
private Account account; // 账户
private int tradeAmount; // 交易额
private int tradeTimes; // 交易次数
// 构造方法
public ATM(Account account, int tradeAmount, int tradeTimes) {
this.account = account;
this.tradeAmount = tradeAmount;
this.tradeTimes = tradeTimes;
}
public void run() {
for (int i = 0; i < tradeTimes; i++) {
account.getBank().tradeAmount(account, tradeAmount);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
class Account {
private Bank bank;
private int amount;
public Account(Bank bank, int amount) {
this.bank = bank;
this.amount = amount;
}
// Getter and Setter
public Bank getBank() {
return bank;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
class Bank {
// 同步方法
public synchronized void tradeAmount(Account account, int amount) {
account.setAmount(amount + account.getAmount());
System.out.println("您在 " + Thread.currentThread().getName() + " 交易了 " + amount + ", 您账户的账户余额是 " + account.getAmount());
}
}
同步代码块
解决并发问题的另一种方式是将并发读写的操作放到一个同步代码块中, 例如, 可以将上面的 tradeAmount 方法改成下面的形式.
class Bank {
public void tradeAmount(Account account, int amount) {
// 同步代码块需要一个对象锁
// 在 Java 中, 由于每一个对象都有一个对象锁
// 所以此处任何一个对象都可以, 通常我们使用 this
synchronized (this) {
account.setAmount(amount + account.getAmount());
System.out.println("您在 " + Thread.currentThread().getName() + " 交易了 " + amount + ", 您账户的账户余额是 " + account.getAmount());
}
}
}
volatile 域
有时候, 为了读写一个域而使用同步似乎开销太大了, 我们可以使用 volatile 关键字修饰一个域, 虚拟机将保证对 volatile 域的并发读写.
锁(Lock)
JDK 1.5 提供了一种全新的方式来解决并发问题, 我们可以使用锁来保护我们的代码. 例如, 可以将上面的 tradeAmount 方法改成下面的形式.
class Bank {
private final Lock l; // 定义一个锁, 每个对象只维护一个锁
public Bank() {
l = new ReentrantLock();
}
public void tradeAmount(Account account, int amount) {
l.lock();
try {
account.setAmount(amount + account.getAmount());
System.out.println("您在 " + Thread.currentThread().getName() + " 交易了 " + amount + ", 您账户的账户余额是 " + account.getAmount());
} finally {
l.unlock();
}
}
}
读/写锁(ReadWriteLock)
上面提到的关于解决并发访问的所有方式都有一个共同点, 那就是互斥, 也就说当一个线程正在访问某个资源时, 其他任何线程都必须等待. 事实上, 如果一个线程正在读取某个资源, 那么其他线程也应该能够读取这个资源, 如果一个线程正在对某个资源进行写入操作, 那么其他线程应该既不能读也不能写. Java 的 ReadWriteLock 类就提供了这样的功能.
下面我们通过增加查询账户余额来演示了如何使用 读/写锁.
class Bank {
final ReadWriteLock rwLock;
final Lock readLock;
final Lock writeLock;
public Bank() {
rwLock = new ReentrantReadWriteLock();
readLock = rwLock.readLock();
writeLock = rwLock.writeLock();
}
public void tradeAmount(Account account, int amount) throws Exception {
writeLock.lock();
try {
account.setAmount(amount + account.getAmount());
System.out.println("您在 " + Thread.currentThread().getName() + " 交易了 " + amount + ", 您账户的账户余额是 " + account.getAmount());
} finally {
writeLock.unlock();
}
}
public Integer getBalance(Account account) throws Exception {
readLock.lock();
try {
return account.getAmount();
} finally {
readLock.unlock();
}
}
}
本地线程变量(ThreadLocal)
假设银行为了审计的需要想追踪一下每台ATM机的交易额, 我们可以采用如下的方式.
class Bank {
private Map<String, Integer> atmAudit = new HashMap<String, Integer>();
// 同步方法
public synchronized void tradeAmount(Account account, int amount) {
account.setAmount(amount + account.getAmount());
System.out.println("您在 " + Thread.currentThread().getName() + " 交易了 " + amount + ", 您账户的账户余额是 " + account.getAmount());
// 审计
String atm = Thread.currentThread().getName();
int atmAmount = atmAudit.get(atm) == null ? 0 : atmAudit.get(atm);
atmAudit.put(atm, atmAmount + amount);
}
}
事实上, 上面的做法完全没有必要, ThreadLocal 类为我们提供了类似的功能, 我们可以将上面的程序修改为如下.
class Bank {
private ThreadLocal<Integer> atmAudit = new ThreadLocal<Integer>();
// 同步方法
public synchronized void tradeAmount(Account account, int amount) {
account.setAmount(amount + account.getAmount());
System.out.println("您在 " + Thread.currentThread().getName() + " 交易了 " + amount + ", 您账户的账户余额是 " + account.getAmount());
// 审计
int atmAmount = atmAudit.get() == null ? 0 : atmAudit.get();
atmAudit.set(atmAmount + amount);
}
}
信号(Semaphore)
JDK 1.5 新加入了一个称为 Semaphore 的类, 下面的例子演示了如何限制多个线程对同一服务的访问.
import java.util.concurrent.Semaphore;
public class Test {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
Service.getInstance().serve();
}
}).start();
}
}
}
class Service {
private final Semaphore available = new Semaphore(3); // 最多为 3 人同时提供服务
private static Service service = new Service();
private Service() {
}
public static Service getInstance() {
return service;
}
public void serve() {
try {
available.acquire(); // 请求许可, 如果当前无许可则等待
System.out.println("-- serve --");
Thread.sleep(1000);
available.release(); // 释放许可
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
--- 更多参见: Java 精萃
-- 声 明:转载请注明出处
-- Last Updated on 2012-07-06
-- Written by ShangBo on 2012-06-22
-- End