并发编程中经常用到的莫非是这个ReentrantLock这个类,线程获取锁和释放锁。还有一个则是synchronized,常用来多线程控制获取锁机制。
先写一个简单的例子。
package com.multi.thread; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class AQSDemo { public static void main(String[] args) { Lock lock = new ReentrantLock(true); MyThread t1 = new MyThread("t1", lock); MyThread t2 = new MyThread("t2", lock); MyThread t3 = new MyThread("t3", lock); t1.start(); t2.start(); t3.start(); } } class MyThread extends Thread { private Lock lock; public MyThread(String name, Lock lock) { super(name); this.lock = lock; } @Override public void run() { lock.lock(); try { System.out.println(Thread.currentThread() + " is running "); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } } }
这是个简单的用ReentrantLock的代码。
知识点理解:
ReentranctLock:
1) 可重入性:大致意思就是如果一个函数能被安全的重复执行,那么这个函数是可重复的。听起来很绕口。
2)可重入锁:一个线程可以重复的获取它已经拥有的锁。
特性:
1)ReentrantLock可以在不同的方法中使用。
2)支持公平锁和非公平锁概念
static final class NonfairSync extends Sync;(非公平锁)
static final class FairSync extends Sync;(公平锁)
3)支持中断锁,收到中断信号可以释放其拥有的锁。
4)支持超时获取锁:tryLock方法是尝试获取锁,支持获取锁的是带上时间限制,等待一定时间就会返回。
ReentrantLock就先简单说一下AQS(AbstractQueuedSynchronizer)。java.util.concurrent包下很多类都是基于AQS作为基础开发的,Condition,BlockingQueue以及线程池使用的worker都是基于起实现的,其实就是将负杂的繁琐的并发过程封装起来,以便其他的开发工具更容易的开发。其主要通过volatile和Unsafe类的原子操作,来实现阻塞和同步。
AQS是一个抽象类,其他类主要通过重载其tryAcquire(int arg)来获取锁,和tryRelease来释放锁。
AQS不在这里做分析,会有单独的一篇文章来学习AQS。
ReentrantLock类里面主要有三个类,Sync,NonfairSync,FairSync这三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
Sync是ReentrantLock实现公平锁和非公平锁的主要实现,默认情况下ReentrantLock是非公平锁。
Lock lock = new ReentrantLock(true); :true则是公平锁,false就是非公平锁,什么都不传也是非公平锁默认的。
非公平锁:
lock.lock();点进去代码会进入到,ReentranctLock内部类Sync。
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; /** * Performs {@link Lock#lock}. The main reason for subclassing * is to allow fast path for nonfair version. */ abstract void lock(); ......省略。 }
这个抽象类Sync的里有一个抽象方法,lock(),供给NonfairSync,FairSync这两个实现类来实现的。这个是一个模板方法设计模式,具体的逻辑供给子类来实现。
非公平锁的lock的方法,虽然都可以自己看,但是还是粘贴出来,说一下。
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } ......省略 }
其实重点看这个compareAndSetState(0,1),这个其实一个原子操作,是cas操作来获取线程的资源的。其代表的是如果原来的值是0就将其设为1,并且返回true。其实这段代码就是设置private volatile int state;,这个状态的。
其实现原理就是通过Unsafe直接得到state的内存地址然后直接操作内存的。设置成功,就说明已经获取到了锁,如果失败的,则会进入:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
这个方法里,这个过程是先去判断锁的状态是否为可用,如果锁已被持有,则再判断持有锁的线程是否未当前线程,如果是则将锁的持有递增,这也是java层实现可重入性的原理。如果再次失败,则进入等待队列。就是要进去等待队列了AQS有一个内部类,是Node就是用来存放获取锁的线程信息。
AQS的线程阻塞队列是一个双向队列,提供了FiFO的特性,Head节点表示头部,tail表示尾部。
1)节点node,维护一个volatile状态,维护一个prev指向向前一个队列节点,根据前一个节点的状态来判断是否获取锁。
2)当前线程释放的时候,只需要修改自身的状态即可,后续节点会观察到这个volatile状态而改变获取锁。volatile是放在内存中的,共享的,所以前一个节点改变状态后,后续节点会看到这个状态信息。
获取锁失败后就会加入到队列里,但是有一点,不公平锁就是,每个新来的线程来获取所得时候,不是直接放入到队列尾部,而是也去cas修改state状态,看看是否获取锁成功。
总结非公平锁:
首先会尝试改变AQS的状态,改变成功了就获取锁,否则失败后再次通过判断当前的state的状态是否为0,如果为0,就再次尝试获取锁。如果state不为0,该锁已经被其他线程持有了,但是其它线程也可能也是自己啊,所以也要判断一下是否是自己获取线程,如果是则是获取成功,且锁的次数要加1,这是可重入锁,不是则加入到node阻塞队列里。加入到队列后则在for循环中通过判断当前线程状态来决定是否哟啊阻塞。可以看出在加入队列前及阻塞前多次尝试去获取锁,而避免进入线程阻塞,这是因为阻塞、唤醒都需要cpu的调度,以及上下文切换,这是个重量级的操作,应尽量避免。
公平锁:
FairSync类: final void lock() { //先去判断锁的状态,而不是直接去获取 acquire(1); } AQS类: public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } FairSync类: protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //hasQueuedPredecessors判断是否有前节点,如果有就不会尝试去获取锁 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; }
公平锁,主要区别是:什么事都要有个先来后到,先来的有先。获取锁的时候是先看锁是否可用并且是否有节点,就是是否有阻塞队列。有的话,就是直接放入到队列尾部,而不是获取锁。
释放锁:
public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } 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); } setState(c); return free; }
释放锁是很简单的,就是先去改变state这个状态的值,改变后如果状态为0,则说明释放成功了,如果直接可重入了多次,也要释放很多次的锁。
释放过程:
Head节点就是当前持有锁的线程节点,当释放锁时,从头结点的next来看,头结点的下一个节点如果不为null,且waitStatus不大于0,则跳过判断,否则从队尾向前找到最前的一个waitStatus的节点,然后通过LockSupport.unpark(s.thread)唤醒该节点线程。可以看出ReentrantLock的非公平锁只是在获取锁的时候是非公平的,如果进入到等待队列后,在head节点的线程unlock()时,会按照进入的顺序来得到唤醒,保证了队列的FIFO的特性。
参考文章:
http://www.cnblogs.com/leesf456/p/5383609.html
https://github.com/pzxwhc/MineKnowContainer/issues/16