1 什么是显式锁
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock (可重入锁)
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
reentrant 锁意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
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则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
2 Lock接口提供的方法
lock()方法
平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
tryLock()方法有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。所以一般会在自旋使用tryLock()方法,获取到之后跳出循环。
lockInterruptibly()方法当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
4 Condition接口
4.1 Condition中的方法
Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的 。
public class LockAndCondition {
private Lock lock = new ReentrantLock();
private Condition conditionOne = lock.newCondition();
private Condition conditionTwo = lock.newCondition();
private void conditionAwait() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+":获取锁并将要进入等待");
conditionOne.await();
System.out.println(Thread.currentThread().getName()+":被唤醒");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void conditionSignal() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+":获取锁并将要唤醒等待线程");
conditionOne.signal();
System.out.println(Thread.currentThread().getName()+":唤醒结束");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockAndCondition lockAndCondition = new LockAndCondition();
new Thread(new Runnable() {
@Override
public void run() {
lockAndCondition.conditionAwait();
}
}).start();;
new Thread(new Runnable() {
@Override
public void run() {
lockAndCondition.conditionSignal();
}
}).start();
}
}
执行结果
Thread-0:获取锁并将要进入等待
Thread-1:获取锁并将要唤醒等待线程
Thread-1:唤醒结束
Thread-0:被唤醒
同一个Condition的await()方法只能被自己的signal()/signalAll();方法唤醒,Condition的唤醒具有特定指向性,所以一般建议用signal()方法唤醒。而wait()对于的notify()/notifyAll(),因为notify()是随机唤醒,所以建议用notifyAll()。
4.2 Condition原理分析
ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。下面将分析Condition的实现,主要包括:等待队列、等待和通知
等待队列
等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态
一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下图所示
因为Condition是同步器AbstractQueuedSynchronizer的内部类,他的数据结构和AQS的数据结构之间是有交互的,所以详细内容我会在AQS模块的博客中详解,此处只做简单解读。
3 实现类ReentrantLock(可重入锁)
什么时候选择用 ReentrantLock 代替 synchronized
既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
可重入锁演示
public class TestReentrantLock {
private Lock lock = new ReentrantLock();
private void getReentLock() {
int i = 0;
int j = 0;
try {
for(;;) {
if(lock.tryLock()) {
i++;
System.out.println(Thread.currentThread().getName()+"第"+i+"次获取当前锁");
if(i>=10)
break;
}else {
j++;
if(j>=10)
break;
System.out.println(Thread.currentThread().getName()+"第"+j+"次获取当前锁失败");
}
}
} finally {
System.out.println(Thread.currentThread().getName()+"释放锁");
lock.unlock();
}
}
public static void main(String[] args) {
TestReentrantLock reentrantLock = new TestReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
reentrantLock.getReentLock();
}
}).start();
}
}
运行结果
Thread-0第1次获取当前锁
Thread-0第2次获取当前锁
Thread-0第3次获取当前锁
Thread-0第4次获取当前锁
Thread-0第5次获取当前锁
Thread-0第6次获取当前锁
Thread-0第7次获取当前锁
Thread-0第8次获取当前锁
Thread-0第9次获取当前锁
Thread-0第10次获取当前锁
Thread-0释放锁
由此可见一个线程可多次获取当前锁
注意
可重入锁当前线程每获取一次锁,相应的锁计时器就会加一,同样的在释放锁的时候锁计时器会减一,所以获取锁的次数要和释放锁次数对应起来,否则释放次数少会造成该锁未释放,其他的线程无法获取当前锁,释放次数多会导致报错
4 ReentrantReadWriteLock(读写锁)
- ReentrantReadWriteLock字面意思可以看出是一个读写锁。
- ReentrantReadWriteLock有两种锁,一种是读锁,称为共享锁,一个种是写锁为互斥锁。
读写锁的特性:
- 可重入:允许读锁可重入,写锁可重入,但是写锁可以获得读锁,读锁不能获得写锁。 注意:在锁重入中,读锁不能获取写锁
- 锁降级:允许写锁降低为读锁,就是在锁重入中,写锁重入到了读锁中.称为锁降级.
- 中断锁的获取:在读锁和写锁的获取过程中支持中断
- 支持Condition:写锁提供Condition实现
- 监控:提供确定锁是否被持有等辅助方法
读锁共享
为了展示读写锁的读锁是共享锁特性,我写了两段代码来比较
第一段,用读写锁
/**
*
* @author dongyue
* 读写锁案例
*/
public class TestReentrantReadWriteLock {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private int i = 0;
private void countOne() {
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+":获取锁");
try {
i++;
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
System.out.println(Thread.currentThread().getName()+":释放锁");
}
}
private void countTwo() {
rwLock.readLock().lock();
System.out.println(Thread.currentThread().getName()+":获取锁");
try {
i++;
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
System.out.println(Thread.currentThread().getName()+":释放锁");
}
}
public static void main(String[] args) {
TestReentrantReadWriteLock readWriteLock = new TestReentrantReadWriteLock();
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.countOne();
}
}).start();;
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.countTwo();
}
}).start();
}
}
运行结果
Thread-0:获取锁
Thread-1:获取锁
2
Thread-1:释放锁
2
Thread-0:释放锁
可以看出针对同一个读锁,是线程共享的,1线程没有释放该锁时,2线程就可以拿到该锁,两个线程可以都对i操作,最终i=2
第二段,用重入锁
/**
*
* @author dongyue
* 读写锁案例
*/
public class TestReentrantReadWriteLock {
private Lock lock = new ReentrantLock();
private int i = 0;
private void countOne() {
lock.lock();
System.out.println(Thread.currentThread().getName()+":获取锁");
try {
i++;
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+":释放锁");
}
}
private void countTwo() {
lock.lock();
System.out.println(Thread.currentThread().getName()+":获取锁");
try {
i++;
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+":释放锁");
}
}
public static void main(String[] args) {
TestReentrantReadWriteLock readWriteLock = new TestReentrantReadWriteLock();
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.countOne();
}
}).start();;
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.countTwo();
}
}).start();
}
}
执行结果
Thread-0:获取锁
1
Thread-0:释放锁
Thread-1:获取锁
2
Thread-1:释放锁
可以看出,该锁不是共享锁,而是独占锁,只有当前一个线程将锁释放后,另一个线程才可以拿到。
读写锁互斥
/**
*
* @author dongyue
* 读写锁案例
*/
public class TestReentrantReadWriteLock {
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// private Lock lock = new ReentrantLock();
private int i = 0;
private void countOne() {
rwLock.readLock().lock();
// lock.lock();
System.out.println(Thread.currentThread().getName()+":获取读锁");
try {
i++;
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
// lock.unlock();
System.out.println(Thread.currentThread().getName()+":释放读锁");
}
}
private void countThr() {
rwLock.writeLock().lock();
// lock.lock();
System.out.println(Thread.currentThread().getName()+":获取写锁");
try {
i++;
Thread.sleep(1000);
System.out.println(i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
// lock.unlock();
System.out.println(Thread.currentThread().getName()+":释放写锁");
}
}
public static void main(String[] args) {
TestReentrantReadWriteLock readWriteLock = new TestReentrantReadWriteLock();
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.countOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.countThr();
}
}).start();
}
}
运行结果
Thread-0:获取读锁
1
Thread-0:释放读锁
Thread-1:获取写锁
2
Thread-1:释放写锁
可见读写锁是互斥锁