Java并发编程之显式锁机制

news/2024/11/26 13:47:54/

一、接口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;@Overridepublic void run() {try {Thread.sleep((int)Math.random()*100);lock.lock();count++;lock.unlock();} catch (InterruptedException e) {e.printStackTrace();}}
}

复制

当我们在主程序中启动一百个线程随机唤醒对count进行加一时,无论运行多少次,结果都是一百,也就是说我们的ReentrantLock是可以为我们保证原子操作的。

ReentrantLock还有一个特性就是可以重入性,即在本身获得某个锁的前提下可以随意进入被该锁锁住的其他方法,对于一个锁可以重复进入。除此之外,ReentrantLock还具有一些其他的有关锁信息的方法:

  • public int getHoldCount():表示当前线程持有该锁的数量
  • public boolean isHeldByCurrentThread():判断锁是否为当前线程持有
  • public boolean isLocked():判断锁是否为任意一个线程持有,如果有则返回true,否则返回false
  • public final boolean hasQueuedThreads():判断该锁上是否有线程进行等待
  • public final int getQueueLength():返回当前等待队列的长度,也就是等待进入该锁的线程个数

三、深入ReentrantLock的实现原理      ReentrantLock依赖CAS和LockSupport来实现,LockSupport有点像工具类,它主要提供两类方法,park和unpark。

  • public static void park()
  • public static void parkNanos(long nanos)
  • public static void parkUntil(long deadline)
  • public static void unpark(Thread thread)

调用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());elseacquire(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) // overflowthrow 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 GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

复制

该方法首先会去获得node的前一个结点,判断如果是head结点,那么说明当前的node结点是整个等待队列上的第一个等待的结点。于是让它尝试着去获得锁,如果能够获得锁,将从等待队列中清除它并返回。

如果发现当前结点前面还有等待的结点或者尝试获取锁失败,那么将会调用shouldParkAfterFailedAcquire方法判断该结点锁对应的线程是否需要被park阻塞,并最终调用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 GCfailed = 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。


http://www.ppmy.cn/news/434272.html

相关文章

客户端与服务器的理解

一.上网的目的 上网的本质目的:通过互联网的形式来获取和消费资源 二.服务器 上网过程中&#xff0c;负责存放和对外提供资源的电脑&#xff0c;叫做服务器 1.服务器对外提供了哪些资源 ①网页中的常见资源 2.数据也是资源 网页中的数据&#xff0c;也是服务器对外提供的一种…

客户端和服务端的区别

客户端 总得来说就是流浪者使用的计算机&#xff0c;用来给服务器发送请求 服务端 是存放网页文件的计算机 客户端与服务端的关系 1、定义不同&#xff1a; 客户端&#xff1a;客户端&#xff08;Client&#xff09;或称为用户端&#xff0c;是指与服务器相对应&#xff0…

服务端开发

一个完整的 APP 项目一般包含以下几个板块&#xff1a; &#xff08;1&#xff09;服务器端&#xff1a;编写接口协议文档&#xff0c;服务器环境架设&#xff08;国内一般都是用阿里云服务器&#xff0c;国 外一般用亚马逊&#xff09;&#xff0c;设计数据库和编写API接口。…

客户端和服务端区别

客户端&#xff1a;上网用的浏览器&#xff1b;APP 服务端&#xff1a;电脑 客户端用的语言&#xff1a;脚本语言&#xff0c;如html、CSS和JavaScript代码 服务端语言&#xff1a;ASP.NET、Ruby on Rails 或 Java 举例&#xff1a;上网搜寻资料时&#xff0c;浏览器向请求…

服务器端基础

JSP入门 第1关&#xff1a;搭建你的第一个Web服务器 <%page language"java" contentType"text/html; charsetUTF-8" pageEncoding"UTF-8"%> <!DOCTYPE html PUBLTC "-//NBC//DTD HTML 4.01 Transitional//EN""http://…

客户端和服务器的关系

原文链接: link. 例子&#xff1a; 你在看一群美女/帅哥的图片&#xff0c;当然我默认在网站上浏览的&#xff0c;那么你电脑上的互联网浏览器就是客户端&#xff0c;而运行着靓图的电脑就是服务端。你的浏览器通过互联网将请求发送给靓图的服务器&#xff0c;服务器接着就会把…

客户端与服务器端通信

关系图 客户端逻辑 【发送】逻辑管理器 &#xff08;处理逻辑发送指令&#xff09;→指令解析管理器&#xff08;根据协议xml解析指令成二进制数据&#xff09;→把二进制数据传给服务器 【接收】服务器发送二进制数据到客户端→指令解析管理器&#xff08;根据协议xml解析二…