参考:
下面学习 Java
中线程同步,线程通信的概念和使用
主要内容:
- 为什么线程需要同步
ReentrantLock
和Condition
synchronized
为什么线程需要同步
竞争条件:线程共享进程资源,当多线程对同一个对象进行访问时,根据各线程访问进程的次序,可能会得到一个错误的结果
《Java核心技术 卷I 14.5 同步》中给出了一个银行账户的例子
首先定义 Bank
类,保存银行账户信息:
public class Bank {
private double[] accounts = null;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < n; i++) {
accounts[i] = initialBalance;
}
}
public void transfer(int from, int to, double amount) throws InterruptedException {
if (accounts[from] < amount) return;
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
public double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
public int size() {
return accounts.length;
}
}
构造函数初始化银行账户数以及每个账户金额
方法 transfer
用于转账操作
方法 getTotalBalance
返回当前银行总金额
方法 size
返回账户数
然后创建类 TransferRunnable
实现接口 Runnable
,在 run
方法中每隔不定时间,执行银行账户转账操作:
public class TransferRunnable implements Runnable {
private Bank bank;
private int fromAccount;
private double maxAmount;
private int DELAY = 10;
public TransferRunnable(Bank bank, int fromAccount, double maxAmount) {
this.bank = bank;
this.fromAccount = fromAccount;
this.maxAmount = maxAmount;
}
@Override
public void run() {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = maxAmount * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
}
}
}
最后创建测试类,定义账户数和初始余额,并创建多个线程:
public class BankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static void main(String[] args) {
Bank b = new Bank(NACCOUNTS, INITIAL_BALANCE);
int i;
for (i = 0; i < NACCOUNTS; i++) {
TransferRunnable r = new TransferRunnable(b, i, INITIAL_BALANCE);
Thread t = new Thread(r);
t.start();
}
}
}
因为线程实现的是账户之间的转账操作,所以不论执行多少次,银行账户总金额应该保持不变,不过在运行上述程序后,总金额会发生变化
原因:多线程同时对账户进行操作时,并没有实现原子操作,无法保证 原子性
解决思路:对关键代码块加锁,保证当前执行此临界区的线程仅有一个,使用条件对象来管理处于临界区的线程
锁和条件对象的作用如下:
- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码段的线程
- 锁可以拥有一个或多个相关的条件对象
- 每个条件对象管理那些已经进入被保护的代码段但不能运行的线程
解决方案有 2
种:
- 使用
ReentrantLock
显式加锁 - 使用
synchronized
关键字
ReentrantLock
和
Condition
ReentrantLock
和
Condition
ReentrantLock
用于线程同步操作,Condition
用于线程通信操作
ReentrantLock
从 Java SE 5.0
开始引入了 ReentrantLock
类,基本使用格式如下:
private Lock lock = new ReentrantLock();
public void lockDemo() {
lock.lock();
try {
critical section
} finally {
lock.unlock();
}
}
定义一个 ReentrantLock
对象,在关键代码块前后进行 加锁 和 解锁 操作
Note:使用 try-finally
结构可以保证及时解锁
线程 A
获得了锁,在运行临界区时失去了处理器资源,线程 B
开始运行,因为线程 A
拥有此锁,所以线程 B
会被阻塞,直到线程 A
运行完成后,解锁为止
锁具有可重入性。线程可以访问多个被同一个锁保护的临界区,在锁内部保存一个计数器,计算对锁重复访问的次数,所以每次加锁(lock
)操作后,必须对应一个解锁(unlock
)操作。当计数器为 0
时,线程释放该锁
假定类中含有锁,对于不同的类对象而言,其锁并不相同,线程访问不同的类对象并不会对同一锁进行请求,所以不会造成阻塞操作
Condition
一个锁对象可以有多个条件对象(Condition
),使用方法 newCondition
即可产生一个新的条件对象
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
当线程在临界区中需要满足某一条件才能继续执行时,调用方法 await
,该线程即进入该条件对象的等待集
public void lockDemo() {
lock.lock();
try {
while (...)
condition.await();
} finally {
lock.unlock();
}
}
调用 await
方法后,该线程进入 等待 状态,必须有另一个线程调用同一条件上的 signalAll
方法,才能唤醒所有等待该条件的线程
/**
* Wakes up all waiting threads.
*
* <p>If any threads are waiting on this condition then they are
* all woken up. Each thread must re-acquire the lock before it can
* return from {@code await}.
*
* <p><b>Implementation Considerations</b>
*
* <p>An implementation may (and typically does) require that the
* current thread hold the lock associated with this {@code
* Condition} when this method is called. Implementations must
* document this precondition and any actions taken if the lock is
* not held. Typically, an exception such as {@link
* IllegalMonitorStateException} will be thrown.
*/
void signalAll();
也可以调用 signal
方法随机唤醒一个等待线程,不过这样容易造成死锁
修改上面的 Bank
类,对关键代码块进行同步和通信操作:
public class Bank {
private double[] accounts = null;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < n; i++) {
accounts[i] = initialBalance;
}
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException {
bankLock.lock();
try {
while (accounts[from] < amount)
sufficientFunds.await();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
} finally {
bankLock.unlock();
}
}
public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
} finally {
bankLock.unlock();
}
}
public int size() {
return accounts.length;
}
}
synchronized
从 Java 1.0
开始,每个对象都有一个内部锁。方法使用关键字 synchronized
声明,即使用内部锁保护整个方法
内部锁仅包含单个条件对象,使用 wait
方法来添加线程到等待集中,使用方法 notify / notifyAll
释放等待集中的线程
修改 Bank
类代码如下:
public class Bank {
private double[] accounts = null;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < n; i++) {
accounts[i] = initialBalance;
}
}
public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
while (accounts[from] < amount)
wait();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
notifyAll();
}
public synchronized double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
public int size() {
return accounts.length;
}
}
客户端锁定
参考:java synchronized锁对象,但是当对象引用是null的时候,锁的是什么?
因为每个对象都包含一个内部锁,所以也可使用如下方式同步:
synchronized (obj) {
critical section
}
使用 obj
对象的锁来保护临界区(obj
对象不能为空)