锁是控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁是允许多个并发线程的访问,如读写锁),在Lock接口出现之前,Java程序靠synchronized关键字实现锁功能的。Java SE5之后,并发包里增加了Lock 接口(以及相关实现类)用来实现锁功能,提供类似的同步功能,缺少synchronized 隐身释放锁的便捷性,但是拥有锁的获取与释放的可操作性、可中断获取锁以及超时获取锁【简单介绍一下超时获取锁,通过tryacquire()获取同步状态成功(根据返回值),且当前节点前驱为头节点,则返回;获取同步状态失败则重新计算是否超时,没超时重新去获取同步状态,超时则放回false,一般情况下不会进入自旋】等多种同步特性。不要将锁的获取过程写在try 快中,因为如果在获取锁的过程时(自定义锁的实现)发生异常,也会导致锁无故释放。
Lock接口一些关键特性
1.尝试非阻塞性获取锁 【如独占式超时获取锁】
2.能被中断的获取锁:当获取到锁的线程被中断时,中断异常将会被抛出,同时释放锁。 【独占式共享式获取锁等都支持】
3.超时获取锁:在指定的时间获取锁,如果截止时间还无法获取锁,则返回
Lock的API
1.void lock();获得后从该方法返回
2.void lockInterruptibly() 可中断的获取锁
3.boolean tryLock():尝试非阻塞方式获取锁,成功获取返回true
4.void unlock():释放锁
Attention:Lock接口的实现基本都是通过聚合一个同步器的子类完成线程访问控制。即同步器是实现锁的关键,利用同步器实现搜索的语义。
同步器
队列同步器简称同步器【abstactQueuedSynchronizer],是用来构建锁或则其他同步组件的基础框架,通过内置的FIFO队列来完成资源获取线程的排队工作。同步器主要使用方式是继承,子类推荐被定义为自定义同步组件的静态内部类。同步器自身没有实现同步接口,仅仅定义获取和释放的方法供自定义同步组件调用,因此可以独占式获取同步状态也可以共享式获取同步状态。
同步器的接口
同步器的设计是基于模板方法模式的,使用者需要继承同步器并重写指定的方法,然后组合在自定义同步组件中,调用同步器提供的模块方法,而这些模块方法进一步调用使用者重写的方法。
同步器重写的方法
方法名称 | 描述 |
---|---|
protected boolean tryAcquire(int arg) | 独占式获取同步状态 |
protected boolean tryRelase(int arg) | 独占式释放同步状态 |
protected boolean tryAcquireShared(int flag) | 共享式获取同步状态 |
protected boolean tryRelaseShare(int arg) | 共享式释放同步状态 |
protected boolean isHeldExclusive() | 当前同步器是否在独占模式下被线程占用 |
同步器提供的模板方法
方法名 | 描述 |
---|---|
void acquire(int arg) | 独占获取同步状态 |
void acquireInterruptibly(int arg) | 同上但是相应中断 |
tryAcquireNanos(int arg,long nanos) | 超时控制 |
同步器提供的模板方法大致分为三类:独占式获取/释放同步状态、共享式获取与释放同步状态、查询同步队列中的现成状态。
队列同步器的实现分析
1.同步队列
同步器依赖内部的同步队列(一个FIFO双向对列),当前线程获取同步状态失败时,同步器将当前线程以及等待状态等信息构造成一个节点加入同步队列,同时会阻塞同步线程,当同步状态释放时候,会把首结点的现场唤醒,使其再次获得同步状态。
1.独占式同步状态的获取与释放
主要完成同步状态获取、结点构造、加入同步队列、以及在同步队列中自旋的相关工作。
在同步状态获取中,通过tryAquire() 中if(compareAndSetSate(0,1))
setExclusiveOwnerThread(Thread.currentThread());
Attention:
Question 1:当多个线程无法获取同步状态需要加入同步队列中,如何保证线程安全性?
Answer:同步器提供一个基于CAS的设置尾结点的方法,他需要传递当前线程认为的尾结点和当前结点,只有设置成功后,当前结点才能与之前尾结点建立关联,具体实现通过addwaiter()方法添加,enq实现节点的添加请求串行化
Question2:被阻塞线程如何被唤醒和推出自旋
Answer:节点进入同步队列以后将进入一个自旋的过程,由acquireQueued()函数进行死循环当条件满足,就可以从这个自旋过程中推出,所谓的条件1)阻塞线程被中断 2)前驱节点出队唤醒当前线程,当前线程前驱结点是头结点,并且获得资源同步状态
2.共享式同步状态的获取与释放
可以设置起始同步状态,并可以根据同步状态值判断是否获取成功
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。其他共享式的访问均被允许。在模板方法中,acquireshare(int arg)方法调用tryAcquiresShared(),当返回值大于等于0,表示能够获得同步状态,。在共享式自旋中,成功获得同步状态并退出自旋的条件是tryAcquiresShare()方法大于等于0.自旋过程中,如果前驱节点为头节点,则尝试获取同步状态。获取成功则退出
3.自定义实例独占锁
public class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
@Override
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases) {
if(getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() { return new ConditionObject(); }
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() { sync.acquire(1); }
@Override
public boolean tryLock() { return sync.tryAcquire(1); }
@Override
public void unlock() { sync.release(1); }
@Override
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}