显示锁

显示锁的接口主要函数如下:

public interface Lock {
	void lock();
	void lockInterruptibly() throws InterruptedException;
	boolean tryLock();
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
	void unlock();
	Condition newCondition();
}

下面介绍下每个函数的作用:

lock()/unlock()

就是普通的获取锁和释放锁方法,lock()会阻塞直到成功。

lockInterruptibly()

与lock()的不同是,它可以响应中断,如果被其他线程中断了,则抛出InterruptedException。

tryLock()

只是尝试获取锁,立即返回,不阻塞,如果获取成功,返回true,否则返回false。

tryLock(long time, TimeUnit unit)

先尝试获取锁,如果能成功则立即返回true,否则阻塞等待,但等待的最长时间由指定的参数设置,在等待的同时响应中断,如果发生了中断,抛出InterruptedException,如果在等待的时间内获得了锁,返回true,否则返回false。

newCondition

新建一个条件,一个Lock可以关联多个条件。

相比synchronized,显式锁支持以非阻塞方式获取锁可以响应中断可以限时

synchronized代表一种声明式编程思维,程序员更多的是表达一种同步声明,由Java系统负责具体实现,程序员不知道其实现细节;

显式锁代表一种命令式编程思维,程序员实现所有细节。

声明式编程的好处除了简单,还在于性能,在较新版本的JVM上,ReentrantLock和synchronized的性能是接近的,但Java编译器和虚拟机可以不断优化synchronized的实现,比如自动分析synchronized的使用,对于没有锁竞争的场景,自动省略对锁获取/释放的调用。

简单总结下,能用synchronized就用synchronized,不满足要求时再考虑ReentrantLock

主要的两个实现:ReentrantLock(可重入锁)、ReentrantReadWriteLock(读写锁)。

ReentrantLock使用介绍

其lock/unlock实现了与syn-chronized一样的语义,包括:

  • 可重入,一个线程在持有一个锁的前提下,可以继续获得该锁;
  • 可以解决竞态条件问题;
  • 可以保证内存可见性。

两个构造方法:

public ReentrantLock()
public ReentrantLock(boolean fair)

参数fair表示是否保证公平,不指定的情况下,默认为false,表示不保证公平。

所谓公平是指,等待时间最长的线程优先获得锁。

保证公平会影响性能,一般也不需要,所以默认不保证。

同样,synchronized锁也是不保证公平的。

使用显式锁,一定要记得调用unlock。一般而言,应该将lock之后的代码包装到try语句内,在finally语句内释放锁。

使用tryLock(),可以避免死锁。在持有一个锁获取另一个锁而获取不到的时候,可以释放已持有的锁,给其他线程获取锁的机会,然后重试获取所有锁。

ReentrantLock实现原理

底层实现依赖 CAS 和 LockSupport。

LockSupport的基本方法(java.util.concurrent.locks):

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

public static void park(Object blocker)

park使得当前线程放弃CPU,进入等待状态(WAITING),操作系统不再对它进行调度,什么时候再调度呢?有其他线程对它调用了unpark, unpark使参数指定的线程恢复可运行状态。

park不同于Thread.yield(), yield只是告诉操作系统可以先让其他线程运行,但自己依然是可运行状态,而park会放弃调度资格,使线程进入WAITING状态。

park是响应中断的,当有中断发生时,park会返回,线程的中断状态会被设置。

注意:park可能会无缘无故地返回程序应该重新检查park等待的条件是否满足

AQS:为了复用代码,Java提供了一个抽象类AbstractQueuedSynchronizer(如ReentrantReadWriteLock、Semaphore、CountDownLatch也基于CAS+LockSupport)。

AQS封装了一个状态,给子类提供了查询和设置状态的方法:

private volatile int state;
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)

用于实现锁时,AQS可以保存锁的当前持有线程,提供了方法进行查询和设置:

private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread t)
protected final Thread getExclusiveOwnerThread()

AQS内部维护了一个等待队列,借助CAS方法实现了无阻塞算法进行更新。

ReentrantLock内部使用AQS,有三个内部类:

abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync

Sync是抽象类,NonfairSync是fair为false时使用的类,FairSync是fire为true时使用的类。

ReentrantLock内部有一个Sync成员,在构造方法中默认为new NonfairSync()

ReentrantLock中的基本方法lock/unlock即是对sync变量的简单封装:

public void lock() {
	sync.acquire(1);
}

public void unlock() {
	sync.release(1);
}

保证公平整体性能比较低,低的原因不是这个检查慢,而是会让活跃线程得不到锁,进入等待状态,引起频繁上下文切换,降低了整体的效率。

通常情况下,谁先运行关系不大,而且长时间运行,从统计角度而言,虽然不保证公平,也基本是公平的。需要说明是,即使fair参数为true,ReentrantLock中不带参数的tryLock方法也是不保证公平的,它不会检查是否有其他等待时间更长的线程。

Condition显式条件

显式锁与synchronized相对应,而显式条件与wait/notify相对应。wait/notify与synchronized配合使用,显式条件与显式锁配合使用。

创建条件变量需要通过显式锁,Lock接口定义了创建方法:

Condition newCondition();

Condition表示条件变量,是一个接口,它的定义为:

public interface Condition {
  void await() throws InterruptedException;
  long awaitNanos(long nanosTimeout) throws InterruptedException;
  boolean await(long time, TimeUnit unit) throws InterruptedException;
  boolean awaitUntil(Date deadline) throws InterruptedException;
  void awaitUninterruptibly();  //该方法不会由于中断结束,但当它返回时,如果等待过程中发生了中断,中断标志位会被设置。
  void signal();
  void signalAll();
}

抛出InterruptedException,但中断标志位会被清空。

与Object的wait方法一样,调用await方法前需要先获取锁,如果没有锁,会抛出异常IllegalMonitorStateException。await在进入等待队列后,会释放锁,释放CPU,当其他线程将它唤醒后,或等待超时后,或发生中断异常后,它都需要重新获取锁,获取锁后,才会从await方法中退出。

注意:await返回后,不代表其等待的条件就一定满足了,通常要将await的调用放到一个循环内,只有条件满足后才退出

signal/signalAll与notify/notifyAll一样,调用它们需要先获取锁,如果没有锁,会抛出异常IllegalMonitorStateException。signal与notify一样,挑选一个线程进行唤醒,signalAll与notifyAll一样,唤醒所有等待的线程,但这些线程被唤醒后都需要重新竞争锁,获取锁后才会从await调用中返回。

特别注意:不要将signal/signalAll与notify/notifyAll混淆,notify/notifyAll是Object中定义的方法,Condition对象也有,稍不注意就会误用

#并发编程##java原理#
Java编程原理 文章被收录于专栏

知其然知其所以然,只有了解了底层原理,借助第一性原理,才可以运用自如,成为真大师。 什么是第一性原理? 第一性原理最早由亚里士多德提出,他将其定义为:“事物被已知的第一项前提。” 简单来说,它要求你不要用“类比”去思考(即:因为别人这样做,或者以前这样做,所以我也这样做),克服从众心理(FOMO)和经验偏差,在科技创新、商业决策中找到成本与效率的最优解。

全部评论

相关推荐

哈哈哈,你是老六:百度去年裁员分评不好,赶紧弄点红包
点赞 评论 收藏
分享
03-04 07:14
门头沟学院 C++
何木健一:去啥?你能考虑去就是思想有问题,当然一周到岗一天可以考虑一下😨
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务