在Java多线程编程里一个重要的概念是锁定,如果一个资源是多个线程所共享的,为了保证数据的完整性,在进行事务性操作时需要将共享资源锁定,这样才可以保证在进行事务操作时只有一个线程能对资源进行操作,从而保证数据的一致性和完整性。在Java 5.0之前,锁定的功能是由Synchronized关键字来实现的,这样做存在的问题有:
1)每次只能对一个对象进行锁定。若需要锁定多个对象,编程就比较麻烦,一不小心就会出现死锁现象;
2)如果线程因拿不到锁而进入等待状况,是没有办法将其打断的。
在Java 5.0之后,出现两种锁的工具可以用,下图是这两个工具的接口及其实现:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICdzFWRoRXdvN1LclHdpZXYyd2LcBzNvwVZ2x2bzNXak9CX90TQNNkRrFlQKBTSvwFbslmZvwFMwQzLcVmepNHdu9mZvwFVywUNMZTY18CX052bm9CX90zdihGatVme5EjW2hXbZZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DMwUTN0gDM5EDOwUDM0EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
1、Lock接口
ReentrantLock是Lock的具体实现类,Lock提供了下面一些方法:
i) lock() :请求锁定,如果锁已经被别的线程锁定,调用此方法的线程被阻断进入等待状态;
ii) tryLock() :如果锁没有被别的线程锁定,则进入锁定状态,并返回true;若锁已经被锁定,则返回false,不进入等待状态。此方法还可带时间参数,如果锁在方法执行时已被锁定,线程将继续等待给定的时间(参数),若还不行才返回false。
iii) unlock() :取消锁定,注意Lock是不会自动取消锁定的,编程时必须要手动解锁。
例子:
Lock lock = new ReentrantLock();
public void accessSharedReource() {
lock.lock();
try {
// 对共享资源进行操作
} finally {
// 一定记着要把锁解除掉,锁本身不会自动解锁的
lock.unlock();
}
}
2、ReadWriteLock接口
为了提高效率,有些共享资源允许同时进行多个读操作,但只允许一个写的操作,比如一个文件,只要其内容不变,则可以让多个线程同时读,不必做排他的锁定,排他的锁定只有在写的时候才需要,以保证别的线程不会看到数据不完整的文件。ReadWriteLock可满足这种需要,其内置两个Lock,一个是读的Lock,一个是写的Lock。多个线程可同时得到读的Lock,但只有一个线程可以得到写的Lock,而且写的Lock被锁定后,任何线程都不能得到Lock(包括读的Lock)。提供的方法有:
i) readLock() :返回一个读的lock;
ii) writeLock() :返回一个写的lock,该lock是排他的。
例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
ReadWriteLock lock = new ReentrantReadWriteLock();
public String read() {
// 得到readLock,并锁定
Lock readLock = lock.readLock();
readLock.lock();
try {
// 做读的工作
return "have read something";
} finally {
// 释放锁
readLock.unlock();
}
}
public String write() {
// 得到writeLock,并锁定
Lock writeLock = lock.writeLock();
writeLock.lock();
try {
// 做写的工作
return "have writen something";
} finally {
// 释放锁
writeLock.unlock();
}
}
}
需要注意的是:ReadWriteLock提供了一个高效的锁定机制,但最终程序的运行效率是和程序的设计息息相关的,比如说如果读的线程和写的线程同时在等待,要考虑是先发放读的lock还是写发放写的lock。如果写发生的频率不高,而且快,则可以考虑先给写的lock。还要考虑的问题是如果一个写正在等待读完成,此时一个新的读进来,是否要给这个新的读发锁,如果发了,则可能导致写的线程等待很长时间。等等此类的问题在编程时都要给予充分的考虑。
3、Condition接口
有时候线程取得lock后需要在一定条件下才能做某些工作,比如说经典的Producer和Consumer问题,Consumer必须在篮子里有苹果的时候才能吃苹果,否则它必须暂时放弃对篮子的锁定,等到Producer向篮子里放了苹果后再拿来吃。而Producer必须等篮子空了才能往里面放苹果,否则它也需要暂时解锁,等Consumer把苹果吃完了才能向篮子里放苹果。在Java 5.0之前,这种功能是由Object类wait(),notify()和notifyAll()等方法实现的,在5.0之后,这些功能集中到了Condition这个接口来实现,它提供以下方法:
i) await() :使调用此方法的线程放弃锁定,进入睡眠直到被打断或被唤醒;
ii) signal() :唤醒一个等待的线程;
iii)signalAll() :唤醒所有等待的线程。
例子:
package threadLock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionTest {
Lock lock = new ReentrantLock();
// 产生Condition对象
Condition producer = lock.newCondition();
Condition consumer = lock.newCondition();
boolean available = false;
public void produce() {
lock.lock();
try {
if(available) {
consumer.await(); // 放弃lock进入睡眠
}
// 生产苹果
System.out.println("Apple produced.");
available = true;
producer.signal(); // 发信号唤醒等待这个Condition的进程
} catch (InterruptedException e) {
System.out.println("Produce process be interrupted.");
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
if(!available) {
producer.await();
}
// 吃苹果
System.out.println("Apple consumed.");
available = false;
consumer.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.lock();
}
}
}