前面学习的synchronized是隐式的同步监视器。
从java5开始,java提供了一种功能更加强大的线程同步机制——通过显式定义同步锁对象来实现同步,这种机制下,同步锁由Lock对象来充当。
Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更加灵活的结构,可以具有差别很大的属性,并且支持多个相关的Condition对象。Lock是控制多个线程对共享资源进行访问的工具,通常,锁提供了对共享资源的独占式访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得lock对象。
某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁),Lock、ReadWriteLock是java5提供的两个根接口,并为Lock提供了ReentrantLock(可重入锁)实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。
在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:
class Y
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//......
//定义需要保证线程安全的方法
public void m(){
//加锁
lock.lock();
try{
//需要保证线程安全的代码
//....method body
}
//使用finnaly块来保证释放锁
finally{
lock.unlock();
}
}
}
使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。可以把以前的Account类改为下面的代码,也是线程安全的:
import java.util.concurrent.locks.ReentrantLock;
public class Account {
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
public Account() {}
//构造器
public Account(String accountNo,double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
//因为账户余额不允许随便修改,所以只为balance提供了getter方法
public double getBalance() {
return this.balance;
}
//提供一个线程安全的draw()方法来完成取钱操作
public void draw(double drawMoney) {
//加锁
lock.lock();
try {
//账户余额大于取钱数目
if(balance>=drawMoney) {
//取出钞票
System.out.println(Thread.currentThread().getName() +"取钱成功!吐出钞票:"+drawMoney);
try {
Thread.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}
//修改余额
balance -= drawMoney ;
System.out.println("\t余额为:"+ balance);
}else {
System.out.println(Thread.currentThread().getName()+"取钱失败!余额不足!");
}
}finally {
//修改完后,释放锁
lock.unlock();
}
}
//下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if(this == obj)
return true;
if(obj!=null && obj.getClass() == Account.class) {
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
其他两个类和我之前的博文“多线程学习(四)——线程同步之同步代码块和同步方法”一样,运行结果也是一样。
ReentrantLock具有可重入性,即一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个程序计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显式调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
最后增加一个死锁的概念:
死锁:两个线程相互等待对方释放同步监视器时就会发生死锁,java虚拟机没有监测,也没有采取措施来处理死锁的情况,所以多线程编程时应该采取措施避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
(参考《疯狂Java讲义第3版》)