LockSupport是JUC并发包下的一个工具类,它的底层是由Unsafe类实现的,它是基于Unsafe类的park()和unpark()方法包装并扩展功能实现的一个工具类,它主要用于对线程的挂起和唤醒操作,它是线程安全的,底层由CAS算法实现。通过它的名字我们也可以知道,它是锁的支持类,后面的锁的实现都是基于它实现的。
park()方法和wait()方法
先谈谈wait()和Unsafe类的park()方法,LockSupport的park()方法是对Unsafe类的park()方法的包装,所以我们需要先理解底层的实际挂起线程的park()方法。
Object类的wait()源码:
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
Unsafe类的park()源码:
public native void park(boolean isAbsolute, long time);
**park()方法的许可证:**上述两种挂起线程的方法底层都是native方法,我们知道wait()方法可以让一个线程挂起并释放锁资源,前提是必须在synchronize关键字内调用,在别的地方调用会抛出 java.lang.IllegalMonitorStateException异常。而park()方法只是挂起线程并不涉及到锁,park()方法会关联一个许可证,这个许可证的获取方式是调用LockSupport的unpark()方法,并且把希望唤醒的线程对象传入,接下来有两种情况:一种是线程直接调用park()方法,此时它因为没有许可证而被挂起,然后调用unpark()方法并把这个线程对象作为参数,此时线程会被唤醒;另一种情况是先调用unpark()方法,传入该线程对象,再调用这个线程对象的park()方法,它在被阻塞之后会直接返回。许可证被赋予之后只能使用一次。
**许可证的抽象理解:**许可证的机制可以理解为一道关卡,所有线程到了这里都会停下来,它们会一直等待许可证的发放,此时有许可证的线程可以通过,许可证使用过后就会被销毁,分发许可证的方法是unpark()。于是,这里总共涉及到了两样事物,一个是关卡,也就是无参的park()方法;另一个是许可证的分发,也就是unpark()方法;
**许可证的意义:**许可证的存在使得park()方法和unpark()方法真正意义上成为并发包的基础,它可以构成锁,park()方法使得没有许可证的线程无法进入相应代码块,它会使得无关线程挂起,防止它们争夺资源,导致死锁。它和synchronize的设计效果是相同的,区别是synchronize是获取和释放对象锁,而park()与unpark()是获取许可证实现“通行”。park()与unpark()的发展前景更好,可以基于它设计线程的队列,例如后面要讲到的AQS队列,在这两个方法或者AQS队列的基础上实现各种不同功能的锁。
LockSupport的park()方法:
然后再来看看LockSupport的park()方法,它底层也调用了Unsafe的park()方法,源码如下:
LockSupport.park();
public static void park() {
UNSAFE.park(false, 0L);
}
我知道unsafe的park方法需要两个参数的传入。true表示绝对时间,一般在使用的时候需要获取当前时间加上希望的偏移时间(ms)来作为第二项的参数;false表示相对时间,表示方法调用多少时间之后返回(ns)。在LockSupport中对park做了封装,默认参数为0,表示一直被挂起。
LockSupport的unpark()方法:
unpark方法调用的也就是Unsafe的unpark方法,需要有线程传入才有效。
LockSupport.unpark(current);
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
**其他返回情况:**被挂起的线程也可能被一些别的情况唤醒,包括小概率的非正常唤醒,以及被中断唤醒。这里介绍中断唤醒,当一个线程被挂起的时候,被别的线程调用了它的interrupt()方法,它就会被唤醒,为了区别线程的唤醒原因,一般在唤醒后判断中断标志,如果不希望线程被中断唤醒,可以设计while()循环,唤醒后根据中断标志决定是退出循环还是重新挂起。如果只希望被中断唤醒,那么while的循环条件可以是while(!Thread.currentThread().isInterrupted)只有被唤醒后标志为true才会离开循环,否则继续挂起。总之,需要考虑到被中断唤醒这种情况。
LockSupport.parkNanos(nanos)方法:
挂起指定时间的方法,如果线程持有许可证,那么挂起后直接返回;如果线程没有许可证,挂起nanos时间后自动返回。
LockSupport.park(blocker)方法:
测试Domo:如下两段测试分别是无参park方法和传入this的park方法。
public class Test {
public static void main(String[] args) {
Test test=new Test();
test.test();
}
public void test(){
LockSupport.park(); //LockSupport.park(this);
}
}
输出截图:
后者输出多了一段wait时间,即调用了park方法后的内部信息。为什么呢?我们来分析源码:
如下代码中在调用Unsafe的方法中设置了blocker,在被唤醒后会清除这个bolcker对象,初步推测这是个线程的成员变量,所以可以获取内部信息并且在使用完后要释放内存。最后调用的方法中传入了当前线程和blocker实例(传入的Test对象),
parkBlockerOffset在LockSupport类中有一个静态变量是用来保存传入的变量引用的。通过它可以保存更多堆栈信息,可以得知线程被阻塞后发生的事情。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
LockSupport.parkNanos(blocker, nanos)方法:
它和上者的区别的是它是一个有时间的阻塞方法,同时传入bolcker记录堆栈信息。
LockSupport.parkUntil(blocker, deadline);方法:
这个方法是指指定截止时间,到某个时间后自动唤醒,同样它也传入了一个blocker对象。底层Unsafe方法的park参数是true,表示绝对时间。
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
它是一个抽象类,它的作用是通过FIFO(先进先出)队列的形式对线程对资源的访问顺序进行管理,并维护一些参数记录信息。通过它可以实现各种类型的锁,因为它的一些方法没有具体实现,需要交给子类去完成。一般来说JUC包中的锁都是继承自它,然后实现功能的。
AQS的参数:
public class NoReentrantLock implements Lock,java.io.Serializable{
/**
* 内部帮助类
* @author mayifan
*
*/
public static class Sync extends AbstractQueuedSynchronizer{
/**
* 是否锁已经被持有
*/
protected boolean isHeldExclusively() {
return getState()==1;
}
/**
* 尝试获取锁
*/
protected boolean tryAcquire(int arg) {
assert arg==1;//判断数据合法性,如果是1,继续运行,如果是其他的,则抛出异常
if(compareAndSetState(0, 1)){
setExclusiveOwnerThread(Thread.currentThread());//设置持有者为当前线程
return true;
}
return false;
}
/**
* 尝试释放锁
*/
protected boolean tryRelease(int arg) {
assert arg==1;
if(getState()==0||Thread.currentThread()!=getExclusiveOwnerThread()){
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
/**
* 提供条件变量接口
*/
Condition newCondition(){
return new ConditionObject();
}
}
private final Sync sync=new Sync();//实例化一个同步器
/**
* 加锁
*/
public void lock() {
sync.acquire(1);
}
/**
* 加锁(会对中断响应)
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 尝试加锁
*/
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**
* 尝试加锁(失败则挂起time时间,)
*/
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
/**
* 解锁
*/
public void unlock() {
sync.release(1);
}
/**
* 获取条件变量
*/
public Condition newCondition() {
return sync.newCondition();
}
/**
* 判断是否被锁
* @return
*/
public boolean isLocked(){
return sync.isHeldExclusively();
}
}
**分析该锁:**该锁的设计思路是先在内部写一个内部类,一个同步器,同步器重写了AQS的一些方法,以实现不可重入锁的功能,其中state只能在0和1之间转化,如果一个线程已经获取了锁,再次调用lock()方法,返回false,设置失败。因为它是一个锁,实现了锁Lock的接口,即便sync已经可以实现所有功能,但是还是需要把它适配为一个锁,对外呈现的也是锁的基本方法。其中的lockInterruptibly()方法表示会对中断做出响应,我们来看源码:
进入这个方法后会判断线程的中断标志,如果为true,会抛出异常。如果为false,即没有被中断过,则继续下面的代码,先尝试获取资源,如果失败,就放入队列(此时中断状态是false的)。接下来该线程会不断尝试,先是判断它是不是在队列首位同时成功获取资源,获取成功就会返回,一旦获取失败就会在下面的parkAndCheckInterrupt()方法中挂起,如果它被中断,在判断中断标志后返回一个true,接着抛出异常,如果不是中断唤醒它,它会继续循环尝试。抛出的异常就是对中断的相应。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
用上面的锁实现一个生产消费者模型:
public class Test {
final static NoReentrantLock lock=new NoReentrantLock();
final static Condition notFull=lock.newCondition();
final static Condition notEmpty=lock.newCondition();
final static Queue<String> queue=new LinkedBlockingQueue<String>();
final static int queueSize=10;
public static void main(String[] args) {
//生产者线程
Thread producer=new Thread(new Runnable() {
public void run() {
while(true){
lock.lock();//获取独占锁
try{
//如果队列满了,则等待
while(queue.size()==queueSize){
notEmpty.await();//存入notEmpty的队列
}
//添加元素到队列
queue.add("ele");
Thread.sleep(200);
System.out.println("添加一个元素到队列,队列当前长度为:"+queue.size());
//唤醒消费线程,消费者在notFull中
notFull.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
});
//消费者线程
Thread consumer=new Thread(new Runnable() {
public void run() {
while(true){
lock.lock();
try{
if(queue.size()==0){
notFull.await();
}
queue.poll();
Thread.sleep(200);
System.out.println("从队列移除一个元素,队列当前长度为:"+queue.size());
notEmpty.signalAll();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
});
//启动线程
producer.start();
consumer.start();
}
}
输出:
**模型分析:**上面这个模型用到了之前设计的独占锁,还用到了两个条件变量,这两个条件变量分别存放被阻塞的生产者和消费者线程。基本的思路是当队列满的时候生产者会阻塞自己,把自己加入到notEmpty条件变量的队列,当它被唤醒的时候,它会被转移到AQS队列中准备获取锁,拿到锁就可以工作了,它被唤醒可以说明消费者已经移除了部分元素,于是它继续往生产队列中添加元素,然后通知所有消费者工作,消费者会从队列中唤醒,进入AQS队列准备获取锁,循环往复。上面的代码中出现了生产者和消费者竞争锁的现象,出现了连续被某一方拿到锁的情况,值得肯定的是,数据没有异常,整个过程是线程安全的。
实现过了和ReentrentLock类似的NoReentrentLock,理解ReentrentLock就很容易了。它和NoReentrentLock的区别是此锁是可重入的,重入之后state加一,释放一次state减一。如果当前线程没有持有该锁而调用了释放锁的方法,就会抛出异常。二者的相同之处在于,它们都是独占的,锁被占用时,其他线程不能得到该锁。关于这个锁的应用,比如可以用它来实现一个线程安全的List,在List的每一部分代码前后加锁就可以了,至于为什么连读取的代码也要加锁?是为了避免读取时数据被修改而导致错误。
上述独占锁用于读少写多的情况,而这个读写锁更多地应用于读多写少的情况。读写锁依然可以利用AQS的state参数来实现,利用它的高16位记录读锁的获取次数,低16位记录写锁的可重入次数,这样就可以对读锁和写锁分别判断了。写锁是独占锁,一旦有线程修改state低16位从0到1,并设置其为获取写锁的线程,那么其他线程获取读锁和写锁都会失败,直到它释放写锁,如果写锁没有被占用,线程对读锁的获取一般都会成功,只要总数不超过读锁获取次数的上限就可以了。
几个参数:
firstReader记录第一个获取读锁的线程,firstReaderHoldCount记录第一个获取到读锁的线程的获取读锁的可重入次数,cachedHoldCounter记录最后一个获取读锁线程的可重入次数。另外每个线程内部都有一个readHolder变量,它是线程私有的,它存放除了第一个线程外其他线程获取读锁的可重入次数。另外state的高十六位记录读锁被获取的总次数,这个数字表示的当前的一个值,会随着锁的获取与释放而变化。
读写锁中的同步器:
读写锁中和其他锁一样,都有实现同步器,同步器Sync继承了AbstractQueuedSynchronizer。然后有两个同步器子类继承了Sync,一个是非公平的同步器,一个是公平的同步器。
非公平同步器:它对于竞争写锁的线程的判断永远返回false,这个方法如果返回true表示它应该被挂起,返回false表示不应该被强制挂起,因为这个方法一直返回false,所以这个线程对锁的竞争与其在队列中的位置无关,它有可能会比先于它进入队列的线程先获得锁,这就是非公平的。非公平体现在没有维护先来先得的规则。
公平同步器:区别是它调用了hasQueuedPredecessors();方法,它会判断它有无前驱结点,如果有,返回true,它会挂起自己,主动退出对锁的竞争。
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
}
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
ReentrentReadWriteLock中的读锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
在new读写锁的时候会同时实例化同步器,读锁和写锁,把同步器传入读锁和写锁的构造方法中,它们调用的方法都是在同步器中对AQS重写后的方法。写锁的原理和独占锁相同,我们来看看读锁的源码:
它的构造方法传入了读写锁的引用,把读写锁使用的同步器是引用给它。
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
lock()方法:(过程分析见代码中的注释)
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
//获取当前状态值
Thread current = Thread.currentThread();
int c = getState();
//判断写锁是否被占用,如果被占用则返回
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//获取读锁计数值
int r = sharedCount(c);
//尝试获取锁,多线程下只有一个线程会成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//把当前线程作为第一个获取读锁的线程
firstReader = current;
//记录可重入次数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
//第二次进入就累加了
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//获取失败,自旋重试,会有机会获得锁,受同时竞争锁的线程数制约
return fullTryAcquireShared(current);
}
void lockInterruptibly()方法:这个方法中线程如果被中断返回会抛出异常。
boolean tryLock()方法:尝试获取锁,获取失败返回false,不会被阻塞。如果该线程已经获得了读锁,则简单增加state的高16位并返回true。
释放锁的尝试释放锁的部分代码如下,这段代码的预计结果是使得state的高16位减一,实现释放锁,如果多个线程在做这件事,可能会设置失败,那么这时,需要不断自旋重试,直到成功,这里是一个死循环。
protected final boolean tryReleaseShared(int unused) {
..............
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
**关于实现List线程安全的demo优化:**之前提到用独占锁实现List的线程安全,在修改时就不能读取了。如果使用读写锁就会更加灵活一些,比如为读取数据的代码用lock.writeLock();加锁,为修改数据的代码用lock.readLock();加锁。这就实现了多线程读的效果,在读多写少的情况下效率很高。
这种锁实现了三种模式的读写控制,在获取锁的时候会返回一个Long类型的戳记stamp,在释放锁的时候需要传入那个返回的戳记,获取锁失败时返回的戳记是0。它的读写模式都是不可重入的。
三种读写模式的锁: