(五) 同步
多数实际的多线程应用中,两个或两个以上的线程需要共享同一数据的存取。
1.竞争条件的一个例子
synchronized(lock){ critical section}
?
关于客户端锁定(client-side locking)。
客户端锁定是非常脆弱的,通常不推荐使用
例如:考虑Vector类,它的方法(get/set方法)是同步的。现在假定在Vector<Double>中存取银行余额
public void transfer(Vector<Double> account, int from, int to, int amout){??? ??? //Error
??? accounts.set(from, account.get(from) - amount);
??? accounts.set(to, account.get(to) + amount);
}
这是错的,在第一次使用set方法后,线程可能在transfer中被剥夺运行权。
7.监视器的概念
(1) 监视器是只包含私有域的类。
(2) 每个监视器类的对象有一个相关的锁。
(3) 使用该锁对所有方法进行加锁。及调用obj.method()时,obj对象的锁在方法调用开始时自动获得的,并且方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
(4) 该锁可以有任意多个相关条件。
在Java中的实现 : Java中每个对象有一个内部锁。如果一个方法用synchronized方法声明,表示就是一个监视器方法,通过调用wait/notify/notifyAll来访问条件变量。
事实上Java与监视器的不同点导致线程安全性下降 : 域不要求必须是private;方法不要求必须是synchronized;内部锁对客户是可用的。
8.Volatile域
volatile关键字为实例域的同步访问提供了一种免责机制,如果声明一个域为volatile,那么编译器和虚拟机就知道该线程是可能被另一个线程并发更新的。
e.g. private volatile boolean done;
volatile变量不能提供原子性,例如,方法
public void flipDone(){done = !done} ??? //不能确保改变域中的值。
?
9.死锁
1.得到锁的线程,在未释放线程时出现异常,没有释放锁。
2.释放锁的线程调用signal或notify,只对一个线程进行解锁,并且该线程不能进行运行(比如调用了wait/await)
Java中没有任何办法可以避免或打破这种死锁。
10.锁测试与超时
因为使用Lock可能导致死锁,所以应该更加谨慎的申请锁,tryLock方法试图申请一个锁,在成功之后返回true,否则立即返回false。
e.g.
if(myLock.tryLock()){
?? ?//now the thread owns the lock
?? ?try{
?? ??? ?... ...
?? ?}finally{
?? ??? ?myLock.unlock();
?? ?}
}
调用tryLock时,可以使用超时参数
e.g. myLock.tryLock(100, TimeUnit.MILLISECONDS)
TimeUnit是一个枚举类型,取值包括SECONDS(秒)、MILLISECONDS(毫秒)、MICROSECOND(微妙)和NANOSECONDS(纳秒)
lock和tryLock的比较
lock : lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么lock方法就无法终止。
tryLock : 调用带有用超时参数的tryLock,如果线程在等待期间期间被中断,将抛出InterruptedException异常。该特性允许程序打破死锁。
lockInterruptibly方法 : 相当于一个超时设置为无限的tryLock方法。
在等待一个条件时,也可以提供一个超时,如果一个线程被另一个线程通过调用signalAll或者signal激活,或者超时时限到了,或者线程被中断,那么await方法将返回。如果等待的线程被中断,await
e.g. myCondition.await(1000, TimeUnit.MILLISECONDS)
如果等待的线程被中断,await方法将抛出一个InterruptedException异常,如果希望在这种情况下线程继续等待(可能不太合理),可以使用awaitUninterruptibly方法代替await。
11.读/写锁
ReentrantReadWriteLock类,如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,读写锁是十分有用的。在这种情况下,允许对读者线程共享访问是合理的。读者线程与写者线程是互斥的。
使用读/写锁的必要步骤:
(1)构造一个ReentrantReadWriteLock对象
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
(2)抽取读写锁
private Lock readLock = rwLock.readLock();
private Lock writeLock = rwLock.writeLock();
(3)对所有的访问者加读锁
public double getTotalBalance(){
?? ?readLock.lock();
?? ?try{
?? ??? ?... ...
?? ?}finally{
?? ??? ?readLock.unlock();
?? ?}
}
(4)对所有的修改者加读写锁
public void transfer(... ...){
?? ?writeLock.lock();
?? ?try{
?? ??? ?... ...
?? ?}finally{
?? ??? ?writeLock.unlock();
?? ?}
}
注意:ReentrantReadWriteLock没有实现Lock接口,而是实现的ReadWriteLock接口
?