Lock
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\locks中),它包含以下方法
//尝试获取锁,获取成功则返回,否则阻塞当前线程
void lock();
//尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,获取锁成功则返回true,否则返回false
boolean tryLock();
//尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
//释放锁
void unlock();
//返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
Condition newCondition();
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
synchronized的缺陷
synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
2、线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,严重影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。
再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是采用synchronized关键字来实现同步的话,就会导致一个问题:如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。
Lock提供了比synchronized更多的功能。但是要注意以下几点:
1、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2、Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
ReentrantLock
ReentrantLock,意思是“可重(chong)入锁”,是唯一实现了Lock接口的类。
所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。可以理解为一个锁可以被持有这个锁的人多次加锁,当然加多少次锁就需要解多少次。直到所有锁都解完其他人才能加锁。
所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞。可以理解为一个锁只能被持有这个锁的人加锁一次,必须释放这个锁后才能继续加锁。
使用方法
ReentrantLock()默认构造方法是非公平锁,公平锁可以使用ReentrantLock(true)。多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
//根据不同的实现Lock接口类的构造函数得到一个锁对象
Lock l = new ReentrantLock();
l.lock(); //获取锁位于try块的外面
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
注意,加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。
Lock l = new ReentrantLock();
try {
l.lockInterruptibly();//获取锁失败时不会执行finally块中的unlock语句
try{
//处理任务
}finally{
l.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
所以,一般情况下通过tryLock来获取锁时是这样使用的:
Lock lock = new ReentrantLock();
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
e.printStackTrace();
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
具体使用方法
public class TestLock {
//这里的锁要设置为全员变量,切勿设置为局部变量
static Lock l = new ReentrantLock();
static int threadNum=3;
public static void main(String str[]){
for(int i=1;i<=threadNum;i++){
new Thread(new LockJob()).start();
}
}
static class LockJob implements Runnable{
@Override
public void run() {
l.lock(); //获取锁位于try块的外面
try {
System.out.println(Thread.currentThread().getName()+"得到了锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+"释放了锁");
l.unlock();
}
}
}
}
运行结果
Thread-0得到了锁
Thread-0释放了锁
Thread-2得到了锁
Thread-2释放了锁
Thread-1得到了锁
Disconnected from the target VM, address: '127.0.0.1:56245', transport: 'socket'
Thread-1释放了锁
Condition
lockInterruptibly(),tryLock和上面使用类似,单独说一下Condition条件锁。Condition有5个方法,通过方法名很容易明白其意思。
void await() throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
其中await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒”读线程”;当从缓冲区读出数据之后,唤醒”写线程”;并且当缓冲区满的时候,”写线程”需要等待;当缓冲区为空时,”读线程”需要等待。
如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒”读线程”时,不可能通过notify()或notifyAll()明确的指定唤醒”读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
下面模拟往容量为5的队列进行50次增删操作,队列的容量始终保持在0-5之间。
public class TestCondition {
//这里的锁要设置为全员变量,切勿设置为局部变量
static Lock lock = new ReentrantLock();
final static Condition insertCondition=lock.newCondition();
final static Condition deleteCondition=lock.newCondition();
static int num=50;
static AtomicInteger AtomicInteger=new AtomicInteger(0);
static ConcurrentLinkedQueue<String> concurrentLinkedQueue=new ConcurrentLinkedQueue();
static CountDownLatch countDownLatch=new CountDownLatch(10);
public static void main(String str[]){
for(int i=1;i<=num;i++){
//50次增删操作
new Thread(new insertJob()).start();
new Thread(new deleteJob()).start();
}
}
static class insertJob implements Runnable{
@Override
public void run() {
lock.lock();
try{
while (concurrentLinkedQueue.size()==5){
System.out.println(Thread.currentThread().getName()+":当前容器已满,等待有元素删除时继续执行!");
//当队列的元素等于5时,容器已经达到最大值无法继续进行插入操作
insertCondition.await();
}
AtomicInteger.getAndIncrement();
concurrentLinkedQueue.add("我是元素");
Thread.sleep(100);
countDownLatch.countDown();
//容器新增元素后,唤醒删除锁,表示可以进行删除操作;
System.out.println(Thread.currentThread().getName()+"元素插入成功,当前队列大小:"+concurrentLinkedQueue.size());
deleteCondition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
static class deleteJob implements Runnable{
@Override
public void run() {
lock.lock();
try{
while (concurrentLinkedQueue.size()==0){
//当队列的元素等于0时,容器为空无法继续进行删除操作
System.out.println(Thread.currentThread().getName()+":当前容器已空,等待有元素插入时继续执行!");
deleteCondition.await();
}
concurrentLinkedQueue.poll();
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"元素移除成功,当前队列大小:"+concurrentLinkedQueue.size());
//容器删除元素后,唤醒增加锁,表示可以进行插入操作;
insertCondition.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
小结
Lock是AQS(AbstractQueuedSynchronizer队列同步器)自己维护当前等待资源的队列,AQS会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。
Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列,两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:
- 线程1调用reentrantLock.lock时,线程被加入到AQS的等待队列中。
- 线程1Condition.await()方法被调用时,该线程从AQS中移除,对应操作是锁的释放。接着马上被加入到Condition的等待队列中,以为着该线程需要signal信号。
- 线程2因为线程1释放锁的关系被唤醒,并判断可以获取锁,于是线程2获取锁,并被加入到AQS的等待队列中。
- 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候线程1 并没有被唤醒。
- signal方法执行完毕,线程2调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
- 一直到释放所整个过程执行完毕。
可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。
signal方法只是将Condition的Node节点修改了状态,并没有唤醒线程。要将修改状态后的Node唤醒,一种是再次调用await(),一种是调用unlock()。这两个方法内部都会执行release方法对队列里的Node解除阻塞。