在多线程应用中,有时多个线程需要对同一个对象进行存取,有可能会产生冲突,导致对象的值发生不希望的改变,这种情况称为竞争条件。
竞争条件的一个例子:
下面代码模拟一个有很多账户的银行,随机产生这些账户之间的存取钱的交易,每个账户有一个线程负责存钱和取钱。
public class Bank {
private final double[] accounts;
/**
* @param n 账户数目
* @param initialBalance 每个账户的初始存款
*/
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
}
/**
* 存款转移的操作
* @param from 出账的账户
* @param to 入账的账户
* @param amount 转移的存款数
*/
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) {
return;
}
System.out.println("current Thread: " + 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());
}
/**
* 获得总钱数
* @return
*/
public double getTotalBalance() {
double sum = 0;
for (double a: accounts) {
sum += a;
}
return sum;
}
public int size() {
return accounts.length;
}
}
public class TranferRunnable implements Runnable {
private Bank bank;
private int fromAccount;
private double maxAmount;
private int DELAY = 10;
/**
* @param b 银行对象
* @param from 转出存款的账户
* @param max 转账的最大数目
*/
public TranferRunnable(Bank b, int from, double max) {
bank = b;
fromAccount = from;
maxAmount = max;
}
@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(Exception e) {
}
}
}
public class UnsyncBankTest {
public static void main(String[] args) {
//10个账户,初始1000块
Bank bank = new Bank(100, 1000);
for (int i = 0; i < 100; i++) {
TranferRunnable r =
new TranferRunnable(bank, i, 1000);
Thread t = new Thread(r);
t.start();
}
}
}
账户的转账是随机的,但是10个账户的总存款应该是不变的100000。在程序运行的某刻停止,控制台输出:
529.43 from 47 to 83 Total Balance: 99526.31
current Thread: Thread[Thread-20,5,main]
显示总额变成了 99526.31而不是预期的100000。这表明程序运行过程中发生了意想不到的错误。
出现这个问题的原因是:transfer方法中的
accounts[to] += amount;
不是原子(不可分割的)操作,这条语句可能被处理如下: 1.将accounts[to]加载到寄存器 2.加上amount的值 3.写回accounts[to] 假设现在有2个线程执行transfer操作,第一个线程执行以上语句,并且进行到了第二步,正准备将计算完的值写回accounts[to],这时被第二个线程打断,第二个线程执行了完整的以上操作,改变了accounts[to]的值并且写回,这时又回到第一个线程执行,它继续执行没有完成的第三步,这个保存的动作擦去了第二个线程做的更改,导致总金额不对。同理accounts[from]的减操作也可能发生这样的情形。
要解决以上的问题,我们需要一个加锁的操作,即让整个transfer方法都在一次完成,中间不能被其他线程打断。 有2种机制解决竞争条件问题,一个是显式锁ReentrantLock类,一个是synchronized关键字。
一.ReentrantLock 基本结构:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
这样保证同一时刻只有一个线程进入临界区。一旦一个线程封锁了对象,其他任何线程都不能通过lock语句,它们调用lock()时,会被阻塞。(注意lock获得的不是方法锁,而是对象锁) lock()方法是请求对象锁,如果请求的对象已经被其他线程占用,则请求的线程会阻塞,有2个非阻塞的方法,分别是 tryLock()和tryLock(long timeout, TimeUnit unit) 这2个方法会在调用后返回boolean值显示是否成功获得对象锁,如果没有获得锁,返回false,而不会导致线程阻塞。第二个方法带时间参数,表示在规定时间内尝试获得对象锁。下面是例子:
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
//这个尝试获取锁后不管成功或者失败都立即返回结果
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
} finally {
if (captured) {
lock.unlock();
}
}
}
public void timed() {
boolean captured = false;
try {
//这个2秒内尝试获得锁,2秒后去做其他事
captured = lock.tryLock(2,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("tryLock(2,TimeUnit.SECONDS): " + captured);
}finally {
if (captured) {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
final AttemptLocking al = new AttemptLocking();
al.untimed();
al.timed();
new Thread() {
{setDaemon(true);}
public void run() {
al.lock.lock();
System.out.println("acquired");
}
}.start();
Thread.sleep(1000);
al.untimed();
al.timed();
}
}
运行程序的输出: tryLock(): true
tryLock(2,TimeUnit.SECONDS): true
acquired
tryLock(): false
tryLock(2,TimeUnit.SECONDS): false
main方法中,前2次调用untimed和timed都成功获得了锁,因为2个方法是主线程中顺序执行的,并且执行的最后都释放了锁。后面新启动了一个线程,这个新线程获得了锁但是没有释放,所以后来的trylock都返回false 一个对象被一个线程封锁,它的所有需要获得lock的操作都不能被其他线程调用,但是这个线程自己可以调用其他需要获得锁的方法(成为可重入的),此时锁的记数会加1,稍微修改一下上面的例子:
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
//这个会一直阻塞直到获得锁
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
System.out.println("count:"+lock.getHoldCount());
testCount();
System.out.println("count:"+lock.getHoldCount());
} finally {
if (captured) {
lock.unlock();
System.out.println("count:"+lock.getHoldCount());
}
}
}
public void testCount() {
lock.lock();
}
public static void main(String[] args) throws InterruptedException {
final AttemptLocking al = new AttemptLocking();
al.untimed();
al.lock.unlock();
System.out.println("count:"+al.lock.getHoldCount());
}
}
主线程调用untimed方法获得了锁,此时锁计数为1,untimed方法中调用testCount方法,因为是同一个线程持有对象,testCount也获得了锁,此时锁计数变为2。随后相继释放lock,计数变为0。
条件变量Condition: 稍微修改一下上面transfer的代码,实现这样的功能:如果本次要取出的钱比账户的存款多,则等到账户中的存款足够之后进行。 transfer方法中要改动的代码:
if (accounts[from] < amount) {
return;
}
一种方案:
public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
//wait...
Thread.sleep(1000);
}
System.out.println("current Thread: " + 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());
}
catch (Exception e) {
}
finally {
bankLock.unlock();
}
}
如果此账户没有足够余额(accounts[from] < amount),他会等待直到另外一个线程向账户注入资金,如果在while语句处加上断点,发现是个死循环,为什么呢? 原因在于这个线程刚刚获得了锁,其它线程无法访问bank对象的这个transfer方法,所以无法向账户转入资金,这样就会一直等待,形成死循环。 解决的方法是使用Condition类,称作条件对象,也叫条件变量。 每个条件变量都可以设置名称,以和其他条件变量区分。
public class Bank {
private final double[] accounts;
private ReentrantLock bankLock = new ReentrantLock();
private Condition sufficientFunds = bankLock.newCondition();
如果transfer方法发现余额不足,他调用sufficientFunds.await();这行代码的效果是让当前线程阻塞,并且放弃锁,进入等待集。当锁空闲可以用时,这个调用await的线程并不立刻获得锁,而是依赖于其他线程调用signalAll方法。当另外一个线程调用transfer转账时,他调用sufficientFunds.signalAll().这行代码重新激活因为这个条件而等待的所有线程,将他们从等待集中移出,他们将再次成为可运行的线程。同时,他们试图重新进入原来占用的对象并从await方法的调用返回,获得锁并且从被阻塞的地方继续执行。 通常,对await的调用应该在如下形式的循环体中:
while (!ok) {
condition.await();
}
当一个线程调用await时,他没有办法自己重新激活自己,需要其他线程调用signalAll,如果没有其他线程调用,他则永远不再运行,产生死锁现象。
理论上,调用signalAll的时机应该是:在对象的状态向有利于等待线程的方向改变时调用。
比如,当一个账户余额发生改变时,等待的线程应该有机会检查余额,所以,当完成存款时,调用signalAll。
public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
//wait...
sufficientFunds.await();
}
//转账的操作
System.out.println("current Thread: " + 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());
//转账完成后调用signalAll
sufficientFunds.signalAll();
}
catch (Exception e) {
}
finally {
bankLock.unlock();
}
}
重新运行以上修改的程序,查看输出
……
505.60 from 98 to 63 Total Balance: 100000.00
current Thread: Thread[Thread-19,5,main]
221.53 from 19 to 31 Total Balance: 100000.00
current Thread: Thread[Thread-86,5,main]
769.38 from 86 to 77 Total Balance: 100000.00
current Thread: Thread[Thread-57,5,main]
901.26 from 57 to 9 Total Balance: 100000.00
current Thread: Thread[Thread-60,5,main]
147.30 from 60 to 14 Total Balance: 100000.00
current Thread: Thread[Thread-81,5,main]
983.62 from 81 to 15 Total Balance: 100000.00
……
账户总额一直是正确的。这样就用ReentrantLock和Condition完成了线程同步,避免了竞争条件。
二.synchronized关键字
Java中的每一个对象都有1个内部锁,如果一个方法调用synchronized关键字声明。要调用该方法,线程必须获得内部的对象锁。
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
等价于:
public synchronized void m() {
//.....
}
相对于Condition的signalAll和await。内部锁也有一个条件对象,但是是用wait方法添加线程到等待集,用notify/nitifyAll方法结束等待线程的阻塞状态
注意wait,notify,notifyAll方法是Object类的final方法,Condition不能重载他们,所以Condition命名2个方法为await和signalAll,实际他们的作用是等价的。
上面transfer的同步方法可以修改如下:
public synchronized void transfer(int from, int to, double amount) {
while (accounts[from] < amount) {
//wait...
wait();
}
//转账的操作
System.out.println("current Thread: " + 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());
//转账完成后调用signalAll
notifyAll();
}
同一个对象的synchronized方法不允许一个以上的线程同时运行,当一个线程获得synchronized方法的锁后,此对象的其他synchronized方法也不能被别的线程调用。
public class TestSynchronized {
public static void main(String[] args) {
final TestSynchronized testSynchronized = new TestSynchronized();
Thread thread1 = new Thread (){
@Override
public void run() {
testSynchronized.printThread();
}
};
Thread thread2 = new Thread (){
@Override
public void run() {
testSynchronized.printThread();
}
};
Thread thread3 = new Thread (){
@Override
public void run() {
testSynchronized.printThread2();
}
};
thread1.start();
thread2.start();
thread3.start();
}
public synchronized void printThread() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread());
}
}
public synchronized void printThread2() {
for (int i = 0; i < 5; i++) {
System.out.println("synchronized-method2:" + Thread.currentThread());
}
}
}
输出:
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
synchronized-method2:Thread[Thread-2,5,main]
synchronized-method2:Thread[Thread-2,5,main]
synchronized-method2:Thread[Thread-2,5,main]
synchronized-method2:Thread[Thread-2,5,main]
synchronized-method2:Thread[Thread-2,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
同步控制块:如果为了防止多个线程同时访问方法内部的部分代码而不是整个方法,可以用如下方式分离代码:
synchronized(syncObj) {
}
这样分离出的代码叫做临界区或者同步控制块。在线程进入此段代码前,必须得到syncObj对象的锁,以获得排他性访问。
以对于一个对象的实例,以下这2种写法调用后作用是一样的:
public synchronized void m() {
//.....
}
public void m() {
synchronized(this) {
//.....
}
}
对于一个类的静态方法,加上synchronized关键字后,这个类的静态方法只能同时由一个线程执行
public class TestSynchronized2 {
public static void main(String[] args) {
Thread thread1 = new Thread (){
@Override
public void run() {
TestSynchronized2.printThread();
}
};
thread1.start();
Thread thread2 = new Thread (){
@Override
public void run() {
TestSynchronized2.printThread();
}
};
thread2.start();
}
public synchronized static void printThread() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread());
}
}
}
输出:
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
作静态synchronized方法
public synchronized static void printThread() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread());
}
}
相当于:
public static void printThread() {
synchronized (TestSynchronized2.class) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread());
}
}
}
他锁住的不是这个类的一个实例,而是整个类的class对象。
synchronized还可以在其他对象上进行同步:
public class TestSynchronized3 {
private Object lockObject = new Object();
public static void main(String[] args) {
final TestSynchronized3 testSynchronized3 = new TestSynchronized3();
//线程1
new Thread(){
@Override
public void run() {
testSynchronized3.printT1();
}
}.start();
//线程2
new Thread(){
@Override
public void run() {
testSynchronized3.printT2();
}
}.start();
//线程3
new Thread(){
@Override
public void run() {
TestSynchronized3.printT3();
}
}.start();
}
public void printT1() {
synchronized(lockObject) {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread());
}
}
}
public synchronized void printT2() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread());
}
}
public static synchronized void printT3() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread());
}
}
}
结果:
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
Thread[Thread-1,5,main]
Thread[Thread-2,5,main]
Thread[Thread-1,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
Thread[Thread-2,5,main]
这3个方法的执行并不是按照调用顺序,因为调用它们的线程请求/持有的锁不是同一个对象,一个是obj,一个是class的实例,一个是class对象。
三.比较ReentrantLock和synchronized
synchronized获得的内部锁存在一定的局限:
1.不能中断一个正在试图获得锁的线程
2.试图获得锁时不能像trylock那样设定超时时间
3.每个锁只有单一的条件,不像condition那样可以设置多个
java核心技术上给出的使用以上2种方案之一的考虑因素:
1. 如果synchronized关键字适合程序,尽量使用它,可以减少代码出错的几率和代码数量
2.如果特别需要Lock/Condition结构提供的独有特性时,才使用他们
3.许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁