1、显示锁ReentrantLock
ReentrantLock实现了Lock接口,Lock接口的定义如下,
从接口的定义可以看出,ReentrantLock负责获取锁和释放锁。获取锁意味着进入了同步块,释放锁意味着退出同步块。同时ReentrantLock还提供了可重入的加锁语义,所以可以看出显示锁和内置锁synchronized基本一致。
那为什么还需要显示锁呢?
1)内置锁的局限性。一个正在等待的线程无法响应中断。
2)显示锁实现了锁获取的公平性和非公平性两种模式。
3)非块结构的加锁,相比于内置锁,更加灵活。
2、公平锁和非公平锁
什么是公平锁?
公平锁就好像是坐公交车排队一样,有先来后到的顺序。所有需要获取锁的线程进行排队,每次锁被释放,都从队列中将最早加入的线程取出,进行唤醒操作。每次有线程需要获取锁,都必须被加入到等待的队列中。
什么是非公平锁?
非公平锁和公平锁相对立。非公平锁允许插队,也就是一个线程需要获取锁的时候,会进行判断,如果这时候正好锁被释放或者没有被获取,那么这个线程就可以正常的获取锁。如果这时候锁还没有被释放,那么这个线程就需要进入到等待的队列中。
有人可能会问问什么要有非公平锁呢?让所有的线程按照先来后到的顺序获取锁不是很好吗。在竞争激烈的情况下,非公平锁的性能要高于公平锁的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已经被线程A持有,因此B被挂起。当A释放锁的时候,B被唤醒,因此会再次尝试获取这个锁。与此同时,如果C也请求这个锁,那么C很可能会在B被唤醒之前获得、使用以及释放这个锁。这样就充分利用了缝隙间的时间,B获取锁的时刻没有推迟,C更早地获取了锁,并且提高了吞吐量。
在默认情况下,ReentrantLock采用的是非公平的锁。
当采用公平锁的时候,需要设置参数为true
ReentrantLock rtl = new ReentrantLock(true);
测试程序:
public class ReentrantLockTest {
public static void main(String[] args) {
int count = 100;
final ReentrantLock rtl = new ReentrantLock(true);
final CountDownLatch cdl = new CountDownLatch(count * 1000);
int i = 0;
long start = System.currentTimeMillis();
while (i < count) {
new Thread(new Runnable() {
@Override
public void run() {
int j = 0;
while ( j < 1000){
rtl.lock();
cdl.countDown();
rtl.unlock();
j++;
}
}
}).start();
i++;
}
try {
cdl.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() - start);
}
}
性能数据(ms):
改变线程数大小count分别为50,100,150,200, 250,对于每个count运行5次数据。
count = 50
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
公平锁 | 303 | 308 | 321 | 312 | 308 | 310.4 |
非公平锁 | 23 | 15 | 16 | 16 | 19 | 17.8 |
count = 100
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
公平锁 | 617 | 702 | 702 | 620 | 617 | 651.6 |
非公平锁 | 25 | 28 | 28 | 26 | 27 | 26.8 |
count = 150
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
公平锁 | 1023 | 1021 | 1013 | 1012 | 1011 | 1016 |
非公平锁 | 40 | 74 | 41 | 39 | 40 | 46.8 |
count = 200
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
公平锁 | 1638 | 1638 | 1522 | 1992 | 1992 | 1756.4 |
非公平锁 | 55 | 65 | 54 | 50 | 50 | 54.8 |
count = 250
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
公平锁 | 1877 | 1661 | 1928 | 1962 | 1948 | 1875.2 |
非公平锁 | 63 | 63 | 62 | 61 | 69 | 63.6 |
下面是性能图表,x轴取50,100,150,200,250,
红线的y轴取x轴对应的公平锁的平均值
蓝线的y轴取x轴对应的非公平锁的平均值
上面的图通过jfreechart生成,代码如下:
import java.awt.Font;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartFrame;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
public class XYLine {
public static void main(String[] args) {
StandardChartTheme mChartTheme = new StandardChartTheme("CN");
mChartTheme.setLargeFont(new Font("黑体", Font.BOLD, 20));
mChartTheme.setExtraLargeFont(new Font("宋体", Font.PLAIN, 15));
mChartTheme.setRegularFont(new Font("宋体", Font.PLAIN, 15));
ChartFactory.setChartTheme(mChartTheme);
XYSeriesCollection mCollection = GetCollection();
JFreeChart mChart = ChartFactory.createXYLineChart(
"折线图",
"count",
"time",
mCollection,
PlotOrientation.VERTICAL,
true,
true,
false);
ChartFrame mChartFrame = new ChartFrame("折线图", mChart);
mChartFrame.pack();
mChartFrame.setVisible(true);
}
public static XYSeriesCollection GetCollection()
{
XYSeriesCollection mCollection = new XYSeriesCollection();
XYSeries mSeriesFirst = new XYSeries("非公平锁");
mSeriesFirst.add(50D, 17.8D);
mSeriesFirst.add(100D, 26.8D);
mSeriesFirst.add(150D, 46.8D);
mSeriesFirst.add(200D, 54.8D);
mSeriesFirst.add(250D, 63.6D);
XYSeries mSeriesSecond = new XYSeries("公平锁");
mSeriesSecond.add(50D, 310.4D);
mSeriesSecond.add(100D, 651.6D);
mSeriesSecond.add(150D, 1016D);
mSeriesSecond.add(200D, 1756.4D);
mSeriesSecond.add(250D, 1875.2D);
mCollection.addSeries(mSeriesFirst);
mCollection.addSeries(mSeriesSecond);
return mCollection;
}
}
3、轮训锁和定时锁
可定时锁和可轮询锁的是由tryLock方法实现的,与无条件的锁模式相比,它具有更加完善的错误恢复机制。如果不能获得所需要的锁,那么可以使用可定时的或者可轮询的锁方式获取,从而使你重新获得控制权。
比如这样:
while ( true ){
if ( lock.tryLock() ){
}
}
那么tryLock()函数的功能是什么?
仅在调用时锁为空闲状态才获取该锁。
如果锁可用,则获取锁,并立即返回值
true
。如果锁不可用,则此方法将立即返回值 false
。
通常对于那些不是必须获取锁的操作可能有用。
上面的例子如果,tryLock返回false,表示不能获得锁,那么会进入下一次循环。这样的好处是,线程不会休眠。我们知道休眠的唤醒是需要资源消耗的。
4、ReentrantLock分析
和前面提到的FutureTask,CountDownLatch,AtomicLong相类似,ReentrantLock实现也是采用了Concurrent库中的AQS框架实现。
根据AQS中的建议,在ReentrantLock中首先实现了一个抽象类,这个抽象类实现了AbstractQueuedSynchronizer,来看看类的定义,
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
// AQS的内部实现。公平锁和非公平锁的基础框架
abstract static class Sync extends AbstractQueuedSynchronizer {
这个类为之后实现公平锁和非公平锁的实现,提供了一个基础的框架。在里面实现了公平锁和非公平锁需要的公共函数,比如tryRelease方法。同时定义了一个抽象的函数lock,这个函数需要公平锁和非公平锁自己实现。
首先来看看nonfairTryAcquire方法的实现,这个方法会在非公平锁中被调用,用来实现tryAcquire方法。
// 非公平锁的try acquire
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state
int c = getState();
// 如果c=0
if (c == 0) {
// 由于是非公平锁,所以允许抢占,只要CAS操作正确就可以获取锁
if (compareAndSetState(0, acquires)) {
// 互斥锁机制,设置拥有锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果不是0,表示已经有线程获得了锁,这时只需要判断当前线程和拥有锁的线程是不是同一个
else if (current == getExclusiveOwnerThread()) {
// 设置下一个状态,state表示获取锁重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置状态,volatile变量
setState(nextc);
return true;
}
return false;
}
1)获取当前线程
2)获取状态
3)比较状态是否是0
a、如果状态是0,表示没有线程获取当前锁。由于是非公平锁,所以接下来只需要通过CAS操作就可以判断是否能够获取锁。
b、如果状态不是0,表示已经有线程获取了当前锁。然后需要判断当前线程是否和持有锁的线程一样,也就是判断是否可以重入锁。
然后看一下公有的tryRelease方法的实现,
// try release
protected final boolean tryRelease(int releases) {
// 获取c
int c = getState() - releases;
// 当前线程和拥有锁的线程不是一个,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 设置free,表示不能释放
boolean free = false;
if (c == 0) {
// c = 0的时候,设置free可以释放
free = true;
// 互斥锁机制,设置拥有锁的线程为null
setExclusiveOwnerThread(null);
}
// 更新状态
setState(c);
// 返回值
return free;
}
由于非公平锁和公平锁采用的获取锁的方式不一致,因此分别针对上面的Sync提供了不同的实现。
来看看非公平锁的实现,
// 非公平锁的实现
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
// 非公平锁的lock
final void lock() {
// 非公平锁属于抢占模式,也就是当一个线程刚刚释放锁,另一个线程立刻请求锁的时候,即使在等待队列中还有其他线程
// 等待锁,这个线程也会获取到锁,而不是进入等待队列
// CAS得到锁
if (compareAndSetState(0, 1))
// 互斥锁机制,设置拥有锁的线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 基于队列的等待机制
acquire(1);
}
// 非公平锁的try acquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
继续看看公平锁的实现,
// 公平锁的实现
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 公平锁的lock
final void lock() {
// 基于队列的等待机制
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 公平锁的try acquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 公平锁和非公平锁的try acquire实现基本上一样,不同的是公平锁需要判断是否有等待队列,因为公平锁是不允许抢占的
// 当状态为0时,表示已经没有线程获得锁
// 如果有等待队列的时候,该锁因该由队列中的线程获取锁,当前这个线程不可以获取锁
// 如果没有等待队列的时候,该线程可以获得锁。
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁和非公平锁在实现lock的不同点是,前者只需要调用acquire去获取锁,如果获取不到就进入队列挂起。而非公平锁,会在开始调用CAS操作判断是否可以获取锁,如果可以则修改State为1,同时更新获取锁的当前线程。
在实现tryAcquire不同的是,在实现公平锁的时候,会判断队列的大小。这个在代码的注释里也有详细的介绍。
5、显示锁和内置锁synchronized之间的选择
首先来看看性能差别,
还是上面的程序,线程数count分别为1500,2000,2500,3000,3500
使用显示锁ReentrantLock(非公平锁)
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
1500 | 388 | 431 | 420 | 397 | 360 | 399.2 |
2000 | 360 | 591 | 568 | 538 | 537 | 518.8 |
2500 | 696 | 683 | 744 | 769 | 647 | 707.8 |
3000 | 906 | 1070 | 1070 | 832 | 832 | 942 |
3500 | 832 | 1037 | 1043 | 1049 | 1040 | 1000.2 |
使用内置锁synchronized
将线程内容修改为
public void run() {
int j = 0;
while (j < 1000) {
// rtl.lock();
synchronized (cdl) {
cdl.countDown();
}
// rtl.unlock();
j++;
}
}
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | 平均 | |
1500 | 671 | 648 | 690 | 645 | 665 | 663.8 |
2000 | 1026 | 878 | 873 | 1074 | 1014 | 973 |
2500 | 1156 | 1174 | 1061 | 1155 | 1061 | 1121.4 |
3000 | 1353 | 1282 | 1282 | 1601 | 1348 | 1373.2 |
3500 | 1490 | 1561 | 1461 | 1669 | 1470 | 1530.2 |
来看看性能对比图,
根据测试程序可以看出显示锁的性能略高于内置锁。由于测试场景较为简单,因此这个数据仅作为参考。
1)显示锁要比内置锁更为危险,因为如果忘记在finally块中调用unlock,会导致线程死锁。
2)显示锁可以实现定时的,可轮询,可中断的锁获取方式,以及非块状的方式。更具灵活性
3)从上面的性能数据来说,显示锁的性能要比内置锁要高。