并发锁之一:ReentrantLock重入锁
一、简介
JDK 5.0为开发人员开发高性能的并发应用程序提供了一些很有效的新选择。ReentrantLock被作为 Java 语言中synchronized功能的替代类,它具有相同的内存语义、相同的锁定,但在大量争用条件下却有更好的性能,此外,它还有 synchronized关键字没有提供的其他特性。
由单词意思我们可以知道这是可重入的意思。那么可重入对于锁而言到底意味着什么呢?简单来说,它有一个与锁相关的获取计数器,如果拥有锁的某个线程再次得到锁,那么获取计数器就加1,然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized 的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized 块,就允许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
二、简单使用示例
1 package cn.memedai;
2
3
4 import java.util.concurrent.locks.Lock;
5 import java.util.concurrent.locks.ReentrantLock;
6
7 /**
8 * 锁Demo
9 */
10 public class ReentrantLockDemo {
11
12 //产品类
13 class Depot {
14 private int size;
15 private Lock lock;
16
17 public Depot() {
18 this.size = 0;
19 this.lock = new ReentrantLock();
20 }
21
22 //生产
23 public void produce(int newSize) {
24 lock.lock();
25 try {
26 size += newSize;
27 System.out.println(Thread.currentThread().getName() + "---------" + size);
28 } finally {
29 lock.unlock();
30 }
31 }
32
33 public void consume(int newSize) {
34 lock.lock();
35 try {
36 size -= newSize;
37 System.out.println(Thread.currentThread().getName() + "-----------" + size);
38 } finally {
39 lock.unlock();
40 }
41 }
42 }
43 //生产者
44 class Producer {
45 private Depot depot;
46
47 public Producer(Depot depot) {
48 this.depot = depot;
49 }
50
51 public void produce(final int newSize) {
52 new Thread() {
53 @Override
54 public void run() {
55 depot.produce(newSize);
56 }
57 }.start();
58 }
59 }
60 //消费者
61 class Customer {
62 private Depot depot;
63
64 public Customer(Depot depot) {
65 this.depot = depot;
66 }
67
68 public void consume(final int newSize) {
69 new Thread() {
70 @Override
71 public void run() {
72 depot.consume(newSize);
73 }
74 }.start();
75 }
76 }
77
78 public static void main(String[] args) {
79
80 Depot depot = new ReentrantLockDemo().new Depot();
81 Producer producer = new ReentrantLockDemo().new Producer(depot);
82 Customer customer = new ReentrantLockDemo().new Customer(depot);
83
84 producer.produce(60);
85 producer.produce(120);
86 customer.consume(90);
87 customer.consume(150);
88 producer.produce(110);
89 }
90 }
下面是运行一次的结果:
1 Thread-0---------60
2 Thread-1---------180
3 Thread-3-----------30
4 Thread-2------------60
5 Thread-4---------50
因为每次获取锁的顺序和释放锁的顺序可能都不一样,这样导致的结果也就会有所区别。
当并发量很小的时候,使用synchronized关键字的效率比Lock的效率高一点,而当并发量很高的时候,Lock的性能就会比synchronized关键字高,具体的原因是因为synchronized关键字当竞争激烈的时候就会升级为重量级锁,而重量级锁的效率会变得非常的低下。对于Lock的使用通过lock.lock()方法获取锁,在finally中通过lock.unlock()保证释放锁,如果忘记释放锁,那么这个锁就会一直占用着导致其它线程无法进入执行甚至还会造成死锁导致程序崩溃。
三、实现原理
ReentrantLock实现了Lock接口,首先可以看一下Lock接口定义了哪些方法ReentrantLock又是如何实现的
lock():获取同步状态
unlock():释放同步状态
在研究源码之前,对于ReentrantLock需要了解了解一个概念公平锁和非公平锁:
公平锁:之前的AQS文章说过,当同步队列中首节点释放同步状态后,因为FIFO先进先出队列首先获取同步状态的为其后继节点如果不存在后继节点就获取等待时间最长的正常状态的线程。而这种唤醒的过程就是公平锁,当释放同步状态以后获取同步状态的要么是后继节点要么是等待时间最长的节点。但是由于公平锁的这个特性导致在并发很高的情况下其效率比非公平锁要低。
非公平锁:相对于公平锁而言,非公平锁在释放同步状态以后所有的线程都会进入竞争同步状态,而获取同步状态的线程是随机的不确定的。有可能是等待时间最长的也有可能是等待时间最短的。
了解了这两个概念以后就可以看看RentrantLock底层到底怎么实现的。
构造方法:默认创建的为非公平锁
1 pubic ReentrantLock(){
2 sync = new NofairSync();//默认的构造方法是非公平锁
3 }
1 public ReentrantLock(boolean fair) {
2 sync = fair ? new FairSync() : new NonfairSync();//通过传入boolean类型的值确定到底是公平锁还是非公平锁
3 }
lock():获取同步状态
1 public void lock() {
2 sync.lock();//调用了内部AQS实现类中的方法【有两种实现方式】
3 }
公平锁获取同步状态
1 final void lock() {
2 acquire(1);//调用AQS的中方法,最终需要重点关注的方法为tryAcquire(int arg)
3 }
tryAcquire(int args):公平锁判断是否获取到锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取锁状态
if (c == 0) {
//公平锁原理判断是否有超过了当前线程的等待时间的线程也就是说当前是否有等待时间比获取同步状态的线程长的,典型的FIFO队列
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;
}
}
hasQueuedProcesssors():判断同步队列中是否存在比当前线程等待时间更长的线程
1 public final boolean hasQueuedPredecessors() {
5 Node t = tail; //同步队列尾节点
6 Node h = head; //同步队列头节点
7 Node s;
//头节点和尾节点比较(如果头节点和尾节点重复代表同步对列中只有一个头节点释放以后不需要进行比较)
8 return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
10 }
公平锁的原理就是:在通过CAS设置同步状态之前会先去同步队列中查询是否存在线程比当前的线程的等待时间长的,如果存在就不去改变该线程的状态如果不存在就进行改变获取同步状态。
非公平的获取锁
1 final void lock() {
2 if (compareAndSetState(0, 1))//通过CAS改变状态
3 setExclusiveOwnerThread(Thread.currentThread());//成功就代表获取锁
4 else
5 acquire(1);//走AQS中的方法
6 }
nonfairTryAcquire(int acquires):非公平的获取同步状态
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取同步状态值
if (c == 0) {
if (compareAndSetState(0, acquires)) {//CAS操作改变状态
setExclusiveOwnerThread(current);//成功就获取锁
return true;
}
}
//重入锁原理如果是当前线程就状态值自增
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置状态值
return true;
}
return false;
}
非公平获取锁的实现比公平获取锁的实现要简单很多,它不需要去同步队列中去与其它的节点进行比较,随便谁获取到同步状态
总结:
对于ReentrantLock而言,它只实现了如何代表线程已经获取了同步状态,他不关心获取了同步状态的以后操作而这些操作都是由AQS本身去实现的,这也证明了AQS在并发包中的重要性。再来看看ReentrantLock实现的获取锁,对于非公平锁而言谁获取同步状态都无所谓对于每个线程而言获取同步状态的几率都是一样的而对于公平锁而言其就遵循了FIFO先进先出队列的原则。
unlock():释放同步状态
1 public void unlock() {
2 sync.release(1);//调用AQS中的release方法
3 }
tryRelease(int releases):释放锁的具体方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//通过同步状态值自减
//判断当前释放同步状态的线程是否为获取同步状态的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否完全释放(因为是重入锁必须等待所有的锁释放才算释放)
if (c == 0) {//代表完全释放
free = true;
setExclusiveOwnerThread(null);//获取锁状态的线程为null
}
setState(c);//设置状态
return free;
}
释放锁的过程很简单,每当释放的时候都会进行一次状态自减直到为0的时候代表着这个线程已经完全释放了这个同步状态接下来才会唤醒同步队列中的节点否则代表这个同步状态未释放完全
四、总结
ReentrantLock是Doug Lea大师在Java语言层实现的锁机制,相比于synchronized关键字ReentrantLock使用起来更加的方便,当然synchronized关键字毕竟是官方支撑的,官方也为其优化了很多。具体的还是得依赖场景。
==================================================================================
不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。
==================================================================================