我们之前介绍过synchronized关键字实现程序的原子性操作,它的内部也是一种加锁和解锁机制,是一种声明式的编程方式,我们只需要对方法或者代码块进行声明,Java内部帮我们在调用方法之前和结束时加锁和解锁。而我们本篇将要介绍的显式锁是一种手动式的实现方式,程序员控制锁的具体实现,虽然现在越来越趋向于使用synchronized直接实现原子操作,但是了解了Lock接口的具体实现机制将有助于我们对synchronized的使用。本文主要涉及以下一些内容:
一、接口Lock的基本组成成员
Lock 位于java.util.concurrent.locks包下,源码如下:
public interface Lock { void lock(); void lockInterruptibly() boolean tryLock(); boolean tryLock(long time, TimeUnit unit) void unlock(); Condition newCondition(); }
其中,
void lock();:调用该方法将获得一个锁的入口
lockInterruptibly():该方法也是去获得一个锁,但是它是响应中断的,一旦在获取的过程中遭遇中断将抛出 InterruptedException。
boolean tryLock();:该方法尝试着去获得一个锁,如果获取失败将返回false,并不会阻塞当前线程
boolean tryLock(long time, TimeUnit unit):尝试着去获取一个锁,如果获取失败,将阻塞等待指定的时间,期间如果能够获得锁将返回true,否则返回false,响应中断请求。
void unlock();:释放一个锁
Condition newCondition();:条件变量,留待下篇文章学习
二、可重入锁ReentrantLock的基本使用
ReentrantLock是接口 Lock的一个最主要的实现类,不仅实现了Lock中的基本的加锁释放锁的方法,还扩展了自己的方法。它有两个构造方法:
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
参数 fair用于保证锁机制的公平策略,公平的策略会是的等待时间越长的线程优先获得锁。保证公平必然会降低性能,所以ReentrantLock默认并不保证公平。我们用ReentrantLock来实现对程序的原子操作:
public class MyThread extends Thread{ private static Lock lock = new ReentrantLock(); public static int count; @Override public void run() { try { Thread.sleep((int)Math.random()*100); lock.lock(); count++; lock.unlock(); } catch (InterruptedException e) { e.printStackTrace(); } } }
当我们在主程序中启动一百个线程随机唤醒对count进行加一时,无论运行多少次,结果都是一百,也就是说我们的ReentrantLock是可以为我们保证原子操作的。
ReentrantLock还有一个特性就是可以重入性,即在本身获得某个锁的前提下可以随意进入被该锁锁住的其他方法,对于一个锁可以重复进入。除此之外,ReentrantLock还具有一些其他的有关锁信息的方法:
三、深入ReentrantLock的实现原理
ReentrantLock依赖CAS和LockSupport来实现,LockSupport有点像工具类,它主要提供两类方法,park和unpark。
调用park方法会使得当前线程丢失CPU使用权,从Runnable状态转变为Waiting状态。而unpark方法则反过来让Waiting状态的某个线程转变状态为Runnable,等待操作系统调度。parkNanos和parkUntil是和时间相关的两个park的变种,parkNanos指定线程要等待的时间,parkUntil则指定线程要等待到什么时候,这个时间是一个绝对时间,相对于纪元的毫秒数。
Java的并发包中有很多并发工具,ReentrantReadWriteLock,Semaphore,CountDownLatch,ReentrantLock等。这些工具有很多的共同特性,于是Java为我们抽象了一个类AbstractQueuedSynchronizer(AQS)来表示这些工具的共性。ReentrantLock是其的一个实现类,内部有三个内部类:
abstract static class Sync extends AbstractQueuedSynchronizer{ //...... }
static final class NonfairSync extends Sync{ //........... }
static final class FairSync extends Sync { //............. }
Sync 继承了AQS并对其中的大部分代码进行了简单的实现,FairSync 和NonfairSync 是针对公平策略而定义的,如果构造ReentrantLock的时候指定公平的策略,那么其内部的所有方法都依赖这个FairSync ,否则就全部依赖NonfairSync。接着看ReentrantLock的构造函数:
private final Sync sync; public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
两个构造方法最终会对sync进行初始化,而sync的将在后续的方法中起到相当大的作用。我们先看lock方法的具体实现:
public void lock() { sync.lock(); }
ReentrantLock的lock方法调用的sync的lock方法,而在sync中的lock方法是一个抽象的方法,也就是说这个方法的具体实现在子类中,我们看NonfairSync中的实现:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
AQS中有一个整型类型的State变量,它用于标识当前锁被持有的次数,该值为0表示当前锁没有被任何线程持有。compareAndSetState是AQS中的方法,该方法调用了unsafe.compareAndSwapInt方法以CAS方式对State进行了更新,如果state的值为0,说明该锁并没有被任何线程持有,那么当前线程将持有该锁并将state的值赋为1。
这就完成了获取的动作,一旦后续的线程尝试访问临界区代码,在前面的线程没有释放锁之前,将会调用 acquire(1)。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire还是调用了AQS中的实现,
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { 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; }
第一个if判断,想要持有的锁是否被持有(虽然之前判断过了,但是有可能在我们调用nonfairTryAcquire方法的期间,之前的线程释放了该锁),如果未被任何线程持有,那么将直接持有该锁。
第二个if判断,如果当前锁的持有者就是当前线程,表示这是同线程的重入操作,于是增加锁定次数并设置state的值。
整个方法结束之后,如果当前线程获得了锁,都将返回true,否则都会返回false。而如果tryAcquire方法返回true,那么整个acquire方法也将结束,否则就说明当前线程并没有通过锁,需要被阻塞。那么就会调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
addWaiter方法将当前线程包裹成一个Node结点,添加到AQS内部所维护的一个等待队列并返回该Node结点。最后调用acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
该方法首先会去获得node的前一个结点,判断如果是head结点,那么说明当前的node结点是整个等待队列上的第一个等待的结点。于是让它尝试着去获得锁,如果能够获得锁,将从等待队列中清除它并返回。
如果发现当前结点前面还有等待的结点或者尝试获取锁失败,那么将会调用shouldParkAfterFailedAcquire方法判断该结点锁对应的线程是否需要被unpark阻塞,并最终调用LockSupport.park(this)阻塞当前线程。
在第一个线程持有该锁的前提下,成功阻塞了第二个线程。这大概就是整个lock方法的调用链流程。
接下来看看unlock的具体实现,
public void unlock() { sync.release(1); }
这是ReentrantLock中对AQS的unlock的具体实现,调用了sync的release方法,这个方法是其父类AQS中的方法:
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
tryRelease被sync重写,具体代码如下:
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; }
首先判断如果当前线程并不是锁的当前持有者,抛出异常(不持有该锁自然不能释放该锁)。如果c等于0则表示,当前锁只被持有一次,也就是当前线程并没有多次重入该锁,于是将该锁的持有者设置为null,表示未被任何线程持有。如果c不等于0,那么说明该锁被当前线程重入多次,于是对state减一并设置state的值。最终如果返回true则说明该锁被释放了,否则说明当前线程依然持有该锁。
回到release方法,如果tryRelease(arg)返回true,那么方法体会判断当前等待队列是否有结点在等待该锁,如果有则调用unparkSuccessor(h)方法唤醒等待队列上的第一个等待的结点线程并返回true。
这里有一个细节,其实所有未能获得锁的线程都被阻塞在方法中:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //******等待线程唤醒的起始位置********// interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
未能获得锁的线程被方法parkAndCheckInterrupt阻塞了,所以当我们在unlock中调用unpark唤醒一个等待队列上的线程结点时,线程将从此处重新进入死循环尝试去获取锁。如果能够获得锁,将从等待队列中移除自己,并返回,否则再次被阻塞等待唤醒。
整个unlock方法的执行流程也已经大致介绍完成,最后我们看看可重入锁ReentrantLock和synchronized的一些对比。
四、ReentrantLock对比synchronized
synchronized更倾向于一种声明式的编程方式,我们在方法前使用synchronized修饰,Java会自动为我们实现其内部的细节,什么时候加锁,什么时候释放锁都是它负责的。
而对于我们的ReentrantLock重入锁来说,需要我们自己手动的去加锁和释放锁,对于逻辑的要求更高,也相对更难。
而随着jvm版本的更新和优化,ReentrantLock和synchronized在性能上的差别在逐渐缩小,所以一般建议使用synchronized而尽量避免复杂难操作的ReentrantLock。
对于显式锁的基本情况大致介绍如上,如有错误之处,望指出!
忍者必须死34399账号登录版 最新版v1.0.138v2.0.72
下载勇者秘境oppo版 安卓版v1.0.5
下载忍者必须死3一加版 最新版v1.0.138v2.0.72
下载绝世仙王官方正版 最新安卓版v1.0.49
下载Goat Simulator 3手机版 安卓版v1.0.8.2
Goat Simulator 3手机版是一个非常有趣的模拟游
Goat Simulator 3国际服 安卓版v1.0.8.2
Goat Simulator 3国际版是一个非常有趣的山羊模
烟花燃放模拟器中文版 2025最新版v1.0
烟花燃放模拟器是款仿真的烟花绽放模拟器类型单机小游戏,全方位
我的世界动漫世界 手机版v友y整合
我的世界动漫世界模组整合包是一款加入了动漫元素的素材整合包,
我的世界贝爷生存整合包 最新版v隔壁老王
我的世界MITE贝爷生存整合包是一款根据原版MC制作的魔改整